Compare commits

...

80 Commits

Author SHA1 Message Date
23358fc708 fix: temporary trigger test
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 8m49s
Monorepo Pipeline / 🧹 Lint (push) Successful in 9m13s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Failing after 51s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 1m34s
Monorepo Pipeline / 🏗️ Build (push) Successful in 6m58s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 20s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 18s
Monorepo Pipeline / 🚀 Release (push) Successful in 3m58s
2026-02-13 14:38:01 +01:00
73ea958655 chore: remove [skip ci] from version sync and update image tag 2026-02-13 14:31:30 +01:00
f2035d79dd chore: automate re-push in pre-push hook
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 56s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m9s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m59s
Monorepo Pipeline / 🚀 Release (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-13 14:28:55 +01:00
f514349ccf chore: sync versions to v1.8.2 [skip ci] 2026-02-13 14:27:22 +01:00
a71f86560b chore: fix @mintel/directus-extension-toolkit build and update eslint ignores
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 27s
Monorepo Pipeline / 🧪 Test (push) Successful in 57s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m1s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m35s
Monorepo Pipeline / 🚀 Release (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-13 14:21:39 +01:00
de8314732d chore: fix remaining build script syntax errors in extensions
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 1m5s
Monorepo Pipeline / 🏗️ Build (push) Failing after 2m49s
Monorepo Pipeline / 🧪 Test (push) Successful in 3m0s
Monorepo Pipeline / 🚀 Release (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-13 12:15:30 +01:00
bdf7773310 chore: finalize 'meaningful' sync hook and pipeline stabilization
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🧹 Lint (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🧪 Test (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
2026-02-13 12:15:01 +01:00
a25e4aa1d4 chore: stabilize pipeline, fix extension build scripts, and finalize version sync hook
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧹 Lint (push) Has started running
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🧪 Test (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
2026-02-13 12:14:27 +01:00
ecc2163b8e chore: remove redundant version sync from pre-push hook
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 47s
Monorepo Pipeline / 🏗️ Build (push) Failing after 3m27s
Monorepo Pipeline / 🧪 Test (push) Successful in 4m33s
Monorepo Pipeline / 🚀 Release (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-13 12:08:58 +01:00
af02378d29 chore: sync versions
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 4s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m10s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🧪 Test (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 🧹 Lint (push) Has been cancelled
2026-02-13 12:05:14 +01:00
f8847a7a10 feat(next-feedback): refine selector filters for tailwind and dynamic classes
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 8s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m59s
Monorepo Pipeline / 🧪 Test (push) Successful in 4m5s
Monorepo Pipeline / 🧹 Lint (push) Successful in 5m8s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 20s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 18s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 20s
Monorepo Pipeline / 🚀 Release (push) Successful in 8m3s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 10m25s
2026-02-13 12:03:33 +01:00
117b23db1e feat(next-feedback): improve selector precision with @medv/finder and fix client/server boundary 2026-02-13 12:03:11 +01:00
d6f9a24823 chore: sync versions to 1.8.0 2026-02-12 22:05:20 +01:00
422e4fccba feat(cloner): add cloner-library and finalize pdf-library rename 2026-02-12 22:04:40 +01:00
57ec4d7544 chore: bump versions 2026-02-12 21:47:55 +01:00
a4d021c658 feat(pdf): rename acquisition-library to pdf-library and update package name to @mintel/pdf
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 13s
Monorepo Pipeline / 🏗️ Build (push) Failing after 11s
Monorepo Pipeline / 🧪 Test (push) Failing after 25s
Monorepo Pipeline / 🚀 Release (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-12 21:46:45 +01:00
269d19bbef fix(acquisition): finalize extension build and components
- Fixed IndustrialCard export in SharedUI.
- Successfully built all extensions including acquisition-library.
- Verified sitemap and briefing module updates.
2026-02-12 21:27:39 +01:00
30ff08c66d fix(acquisition): standardize bundling and externalize React/PDF dependencies
- Added JSX support and correctly externalized react/pdf dependencies in esbuild.
- Fixed acquisition-library exports by removing missing DINLayout reference.
- Standardized extension entry points across all modules.
2026-02-12 21:26:30 +01:00
81deaf447f fix(acquisition): standardize bundling and externalize React/PDF dependencies
- Added JSX support to esbuild configuration.
- Externalized react, react-dom, and @react-pdf/renderer to avoid redundant bundling.
- Updated acquisition-library exports for modular PDF generation.
2026-02-12 21:24:15 +01:00
a0ebc58d6d fix(directus): resolve extension visibility and registration failures
- Corrected module_bar settings to restore custom extension visibility in UI.
- Fixed 'fs' dynamic require in acquisition endpoint by externalizing Node.js built-ins.
- Standardized local environment branding to AT Mintel.
2026-02-12 21:20:28 +01:00
7498c24c9a fix(directus): resolve login failures and standardize project branding
- Fixed project isolation bypass (identity shadowing) by prefixing database service name.
- Standardized health check paths and protocols in docker-compose.yml.
- Resolved extension SyntaxError caused by duplicate banner injections in build scripts.
- Migrated extension build system to clean esbuild-based bundles (removing shims).
- Updated sync-directus.sh for project-prefixed service name.
- Synchronized latest production data and branding (AT Mintel).
2026-02-12 19:21:53 +01:00
efba82337c refactor(acquisition): change build output to dist/ directory
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 59s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m38s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m22s
Monorepo Pipeline / 🚀 Release (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-12 12:47:06 +01:00
c083b309fb fix(acquisition): add missing dependencies to acquisition-library and fix build failure
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m0s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m6s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m23s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🚀 Release (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-12 12:15:43 +01:00
eb8bf60408 fix(infra): use dynamic container detection for registry maintenance
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 20s
Monorepo Pipeline / 🧪 Test (push) Successful in 49s
Monorepo Pipeline / 🧹 Lint (push) Successful in 1m55s
Monorepo Pipeline / 🏗️ Build (push) Failing after 2m1s
Monorepo Pipeline / 🚀 Release (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-12 11:16:28 +01:00
a3819490ac chore(package): standardize formatting and cleanup temporary logs
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 26s
Monorepo Pipeline / 🧪 Test (push) Successful in 52s
Monorepo Pipeline / 🏗️ Build (push) Has started running
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 🧹 Lint (push) Has been cancelled
2026-02-12 11:14:58 +01:00
1127954fea feat(cms): final restoration of extension logic and monorepo stabilization
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 55s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m32s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m50s
Monorepo Pipeline / 🚀 Release (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-12 02:11:53 +01:00
fa0b133012 feat(cms): restore extension logic and stabilize build pipeline
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 45s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m47s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m57s
Monorepo Pipeline / 🚀 Release (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
- restored People Manager and Acquisition Manager logic from git history
- standardized build scripts for directus extensions
- added mmintel user and enabled extensions in cms settings
- updated sync script for robust artifact distribution
2026-02-12 01:14:13 +01:00
1b40baebd4 fix(infra): use SHA detection and better logging in wait-for-upstream.sh
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 44s
Monorepo Pipeline / 🧪 Test (push) Successful in 2m24s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m43s
Monorepo Pipeline / 🚀 Release (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-11 23:25:47 +01:00
316c03869a fix(gatekeeper): enhance logging and stabilize upstream polling
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 6m38s
Monorepo Pipeline / 🧹 Lint (push) Successful in 7m14s
Monorepo Pipeline / 🏗️ Build (push) Successful in 10m24s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 1m39s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 2m7s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 2m8s
Monorepo Pipeline / 🚀 Release (push) Successful in 2m18s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 6m58s
2026-02-11 22:49:16 +01:00
63d2acfab5 feat(infra): add wait-for-upstream script for smart dependencies
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 47s
Monorepo Pipeline / 🧪 Test (push) Successful in 39s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m49s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
2026-02-11 22:41:47 +01:00
bdeae0aca6 chore(gatekeeper): bump to 1.7.11 for fix
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 47s
Monorepo Pipeline / 🧪 Test (push) Successful in 40s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m51s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
2026-02-11 22:35:04 +01:00
47c70a16f1 fix(gatekeeper): trim auth inputs and prioritize access code to prevent autofill traps
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Has been cancelled
Monorepo Pipeline / 🧪 Test (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
2026-02-11 22:32:17 +01:00
b96d44bf6d chore: finalize version updates for v1.7.10
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧪 Test (push) Successful in 45s
Monorepo Pipeline / 🧹 Lint (push) Successful in 1m12s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m52s
Monorepo Pipeline / 🚀 Release (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-11 16:55:17 +01:00
73b60f14a9 chore: release clean base image 1.7.10
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 1m3s
Monorepo Pipeline / 🧪 Test (push) Successful in 41s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m19s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 16s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 2m14s
Monorepo Pipeline / 🚀 Release (push) Successful in 2m43s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 21s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 32s
2026-02-11 16:32:16 +01:00
b3f43c421f chore: manual version bump to 1.7.9
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 57s
Monorepo Pipeline / 🧪 Test (push) Successful in 50s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m49s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 57s
Monorepo Pipeline / 🚀 Release (push) Successful in 2m7s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 4m4s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 3m1s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 34s
2026-02-11 16:02:51 +01:00
a2339f7106 fix: make directus extension build scripts more resilient
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 45s
Monorepo Pipeline / 🧪 Test (push) Successful in 52s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m50s
Monorepo Pipeline / 🚀 Release (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-11 15:58:58 +01:00
e83a76f111 chore: trigger CI build after disk cleanup
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 45s
Monorepo Pipeline / 🧹 Lint (push) Successful in 1m4s
Monorepo Pipeline / 🏗️ Build (push) Failing after 38s
Monorepo Pipeline / 🚀 Release (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-11 15:56:16 +01:00
0096c18098 fix(infra): correct registry data path and enable untagged manifest deletion
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 27s
Monorepo Pipeline / 🧪 Test (push) Failing after 24s
Monorepo Pipeline / 🏗️ Build (push) Failing after 22s
Monorepo Pipeline / 🧹 Lint (push) Successful in 1m4s
Monorepo Pipeline / 🚀 Release (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-11 12:39:50 +01:00
3284931f84 chore(next-utils): respect skip flags in refinements and publish v1.7.15
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 46s
Monorepo Pipeline / 🧪 Test (push) Successful in 55s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m48s
Monorepo Pipeline / 🚀 Release (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-11 01:32:01 +01:00
28517a3558 chore(next-utils): introduce withMintelRefinements and publish v1.7.14
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Has started running
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🧪 Test (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
2026-02-11 01:31:16 +01:00
3b9f10ec98 chore(next-utils): convert to ESM-only and publish v1.7.13
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 42s
Monorepo Pipeline / 🧹 Lint (push) Successful in 1m0s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m47s
Monorepo Pipeline / 🚀 Release (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-11 01:26:53 +01:00
65fd248993 chore(next-utils): fix generic propagation in createMintelDirectusClient and publish v1.7.12
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 45s
Monorepo Pipeline / 🧪 Test (push) Successful in 54s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m46s
Monorepo Pipeline / 🚀 Release (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-11 01:22:12 +01:00
ebd9ab132c chore(next-utils): add explicit return type to validateMintelEnv and publish v1.7.11
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 45s
Monorepo Pipeline / 🧪 Test (push) Successful in 58s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
2026-02-11 01:20:57 +01:00
ddaeb2c3ca chore(next-utils): rebuild with generic types and publish v1.7.10
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 42s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Monorepo Pipeline / 🧹 Lint (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
2026-02-11 01:20:03 +01:00
ad1a8c4fbf feat(next-utils): support generic schema in directus client
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 34s
Monorepo Pipeline / 🧪 Test (push) Successful in 43s
Monorepo Pipeline / 🧹 Lint (push) Successful in 1m6s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m50s
Monorepo Pipeline / 🚀 Release (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-11 01:00:34 +01:00
013b0259b2 fix(pipeline): use POSIX sh compatible logic for release prioritization
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 44s
Monorepo Pipeline / 🧪 Test (push) Successful in 40s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m47s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
2026-02-11 00:33:06 +01:00
d5a9a3bce4 chore: refine release prioritization logic and bump v1.7.8
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 1m0s
Monorepo Pipeline / 🧪 Test (push) Successful in 39s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m23s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 19s
Monorepo Pipeline / 🚀 Release (push) Successful in 2m42s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 2m17s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 16s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 5m29s
2026-02-11 00:31:03 +01:00
b9fd583ac4 chore: fix pipeline hang by disabling broken caching and using corepack
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 57s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 🧪 Test (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
2026-02-11 00:29:10 +01:00
bfdbaba0d0 chore: implement release prioritization and streamline setup for speed
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 1m2s
Monorepo Pipeline / 🧪 Test (push) Successful in 58s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
2026-02-11 00:22:31 +01:00
4ea9cbc551 fix(next-utils): use natural type inference for validateMintelEnv to fix unknown type errors
Some checks failed
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Monorepo Pipeline / 🧹 Lint (push) Has been cancelled
Monorepo Pipeline / 🧪 Test (push) Has been cancelled
Monorepo Pipeline / 📦 Install & Sync (push) Has been cancelled
2026-02-11 00:17:46 +01:00
d8c1a38c0d chore: optimize pipeline for speed and parallelize QA jobs
Some checks failed
Monorepo Pipeline / 🧹 Lint (push) Has been cancelled
Monorepo Pipeline / 🧪 Test (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 📦 Install & Sync (push) Has been cancelled
2026-02-11 00:14:55 +01:00
b65b9a7fb2 fix(next-utils): finalize type safety for validateMintelEnv and fix pre-push hook
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Failing after 2m7s
Monorepo Pipeline / 🚀 Release (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-11 00:10:38 +01:00
858c7bbc39 fix(next-utils): use z.extend() for robust type inference in validateMintelEnv
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Successful in 2m33s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 33s
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
2026-02-11 00:04:14 +01:00
149123ef90 fix(next-utils): restore optional argument with robust types to satisfy linter
Some checks failed
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 🧪 Quality Assurance (push) Has been cancelled
2026-02-11 00:02:53 +01:00
6bc49d1c52 fix(next-utils): make validateMintelEnv generic for better type safety
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Failing after 2m58s
Monorepo Pipeline / 🚀 Release (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-10 23:57:18 +01:00
52ffe49019 feat(next-utils): make directus client environment-aware and standardize base env schema
All checks were successful
Monorepo Pipeline / 🧪 Quality Assurance (push) Successful in 2m10s
Monorepo Pipeline / 🚀 Release (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-10 23:44:27 +01:00
73fa292528 fix: remove klz from workspace 2026-02-10 21:39:48 +01:00
f2c0a4581c chore: sync versions
All checks were successful
Monorepo Pipeline / 🧪 Quality Assurance (push) Successful in 2m6s
Monorepo Pipeline / 🚀 Release (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-10 00:39:34 +01:00
367c4d8404 fix: cms schema 2026-02-10 00:35:26 +01:00
587c88980f chore: release next-config v1.7.0
All checks were successful
Monorepo Pipeline / 🧪 Quality Assurance (push) Successful in 2m8s
Monorepo Pipeline / 🚀 Release (push) Successful in 1m51s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 17s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 2m18s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 16s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 5m34s
2026-02-10 00:29:02 +01:00
fcdfdb4588 chore: release next-config v1.6.1
All checks were successful
Monorepo Pipeline / 🧪 Quality Assurance (push) Successful in 2m12s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 18s
Monorepo Pipeline / 🚀 Release (push) Successful in 2m17s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 2m13s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 17s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 5m27s
2026-02-10 00:27:59 +01:00
6bbaa8d105 chore: cms sync 2026-02-10 00:26:13 +01:00
eccc084441 chore: cms sync commands 2026-02-10 00:13:42 +01:00
da6b8aba64 fix: cms sync
All checks were successful
Monorepo Pipeline / 🧪 Quality Assurance (push) Successful in 2m43s
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-10 00:03:27 +01:00
290097b4e6 chore: fix linter
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Has been cancelled
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
2026-02-10 00:02:26 +01:00
45894cce34 chore: fix linter
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Failing after 57s
Monorepo Pipeline / 🚀 Release (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-09 23:59:22 +01:00
7195906da0 chore: fix linter
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Failing after 42s
Monorepo Pipeline / 🚀 Release (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-09 23:54:58 +01:00
dcb466f53b chore: fix husky
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Failing after 1m3s
Monorepo Pipeline / 🚀 Release (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-09 23:44:34 +01:00
14089766ea feat: infra cms
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Failing after 1m5s
Monorepo Pipeline / 🚀 Release (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-09 23:33:45 +01:00
6ecabe4a04 chore: sync lock file
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Failing after 9s
Monorepo Pipeline / 🚀 Release (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-09 23:26:38 +01:00
b205220bde fix: cli compatibility with nextjs 16
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Failing after 14s
Monorepo Pipeline / 🚀 Release (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-09 23:15:50 +01:00
3d5a802c6e chore: release packages
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Failing after 1m42s
Monorepo Pipeline / 🚀 Release (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-09 23:05:15 +01:00
b5d1272f85 fix: customer manager
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Failing after 2m15s
Monorepo Pipeline / 🚀 Release (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-09 23:04:21 +01:00
e152fb8171 fix: sync versions 2026-02-09 22:50:28 +01:00
d7cec1fa0e fix: docker images
All checks were successful
Monorepo Pipeline / 🧪 Quality Assurance (push) Successful in 4m4s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 29s
Monorepo Pipeline / 🚀 Release (push) Successful in 3m44s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 4m40s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 17s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 7m15s
2026-02-09 22:37:19 +01:00
67c2af958a fix: docker images
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Successful in 4m1s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 33s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Failing after 21s
Monorepo Pipeline / 🐳 Build Build-Base (push) Failing after 45s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 27s
Monorepo Pipeline / 🚀 Release (push) Successful in 3m43s
2026-02-09 22:26:16 +01:00
015e295370 fix: pipeline 2026-02-09 22:21:22 +01:00
c9952bfd1d fix: pipeline
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Successful in 6m14s
Monorepo Pipeline / 🚀 Release (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 Production Runtime (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
2026-02-09 22:16:07 +01:00
f9aaf3712e fix: pipeline
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Failing after 7m22s
Monorepo Pipeline / 🚀 Release (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-09 22:02:58 +01:00
d2bbfe3b40 fix: pipeline 2026-02-09 21:58:04 +01:00
134 changed files with 14494 additions and 8783 deletions

View File

@@ -0,0 +1,82 @@
---
description: How to manage and deploy Directus CMS infrastructure changes.
---
# Directus CMS Infrastructure Workflow
This workflow ensures "Industrial Grade" consistency and stability across local, testing, and production environments for the `at-mintel` Directus CMS.
## 1. Local Development Lifecycle
### Starting the CMS
To start the local Directus instance with extensions:
```bash
cd packages/cms-infra
npm run up
```
### Modifying Schema
1. **Directus UI**: Make your changes directly in the local Directus Admin UI (Collections, Fields, Relations).
2. **Take Snapshot**:
```bash
cd packages/cms-infra
npm run snapshot:local
```
This updates `packages/cms-infra/schema/snapshot.yaml`.
3. **Commit**: Commit the updated `snapshot.yaml`.
## 2. Deploying Schema Changes
### To Local Environment (Reconciliation)
If you pull changes from Git and need to apply them to your local database:
```bash
cd packages/cms-infra
npm run schema:apply:local
```
> [!IMPORTANT]
> This command automatically runs `scripts/cms-reconcile.sh` to prevent "Field already exists" errors by registering database columns in Directus metadata first.
### To Production (Infra)
To deploy the local snapshot to the production server:
```bash
cd packages/cms-infra
npm run schema:apply:infra
```
This script:
1. Syncs built extensions via rsync.
2. Injects the `snapshot.yaml` into the remote container.
3. Runs `directus schema apply`.
4. Restarts Directus to clear the schema cache.
## 3. Data Synchronization
### Pulling from Production
To update your local environment with production data and assets:
```bash
cd packages/cms-infra
npm run sync:pull
```
### Pushing to Production
> [!CAUTION]
> This will overwrite production data. Use with extreme care.
```bash
cd packages/cms-infra
npm run sync:push
```
## 4. Extension Management
When modifying extensions in `packages/*-manager`:
1. Extensions are automatically built and synced when running `npm run up`.
2. To sync manually without restarting the stack:
```bash
cd packages/cms-infra
npm run build:extensions
```
## 5. Troubleshooting "Field already exists"
If `schema:apply` fails with "Field already exists", run:
```bash
./scripts/cms-reconcile.sh
```
This script ensures the database state matches Directus's internal field registry (`directus_fields`).

View File

@@ -1,5 +0,0 @@
---
"@mintel/mail": minor
---
Initial release of the branded email system package.

View File

@@ -0,0 +1,7 @@
---
"@mintel/monorepo": patch
"acquisition-manager": patch
"feedback-commander": patch
---
fix: make directus extension build scripts more resilient

View File

@@ -1,7 +1,7 @@
node_modules node_modules
.next .next
.git .git
.npmrc # .npmrc is allowed as it contains the registry template
dist dist
build build
out out

36
.env Normal file
View File

@@ -0,0 +1,36 @@
# Project
IMAGE_TAG=v1.8.2
PROJECT_NAME=at-mintel
PROJECT_COLOR=#82ed20
GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582
# Authentication
GATEKEEPER_PASSWORD=mintel
AUTH_COOKIE_NAME=mintel_gatekeeper_session
# Host Config (Local)
TRAEFIK_HOST=at-mintel.localhost
DIRECTUS_HOST=cms.localhost
# Next.js
NEXT_PUBLIC_BASE_URL=http://at-mintel.localhost
# Directus
DIRECTUS_URL=http://cms.localhost
DIRECTUS_KEY=F9IIfahEjPq6NZhKyRLw516D8GotuFj79EGK7pGfIWg=
DIRECTUS_SECRET=OZfxMu8lBxzaEnFGRKreNBoJpRiRu58U+HsVg2yWk4o=
CORS_ENABLED=true
CORS_ORIGIN=true
LOG_LEVEL=debug
DIRECTUS_ADMIN_EMAIL=mmintel@mintel.me
DIRECTUS_ADMIN_PASSWORD=Tim300493.
DIRECTUS_DB_NAME=directus
DIRECTUS_DB_USER=directus
DIRECTUS_DB_PASSWORD=mintel-db-pass
# Sentry / Glitchtip
SENTRY_DSN=
# Analytics (Umami)
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js

View File

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

4
.eslintignore Normal file
View File

@@ -0,0 +1,4 @@
**/index.js
**/dist/**
packages/cms-infra/extensions/**
packages/cms-infra/extensions/**

View File

@@ -0,0 +1,44 @@
name: 🏥 Server Maintenance
on:
schedule:
- cron: '0 3 * * *' # Every day at 3:00 AM
workflow_dispatch: # Allow manual trigger
jobs:
maintenance:
name: 🧹 Prune & Clean
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: 🚀 Execute Maintenance via SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
# Run the prune script on the host
# We transfer the script and execute it to ensure it matches the repo version
scp packages/infra/scripts/prune-registry.sh root@${{ secrets.SSH_HOST }}:/tmp/prune-registry.sh
ssh root@${{ secrets.SSH_HOST }} "bash /tmp/prune-registry.sh && rm /tmp/prune-registry.sh"
- name: 🔔 Notification - Success
if: success()
run: |
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=🏥 Maintenance Complete" \
-F "message=Server-Wartung erfolgreich ausgeführt.\nRegistry & Docker Ressourcen bereinigt." \
-F "priority=2" || true
- name: 🔔 Notification - Failure
if: failure()
run: |
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=❌ Maintenance FAILED" \
-F "message=Die automatische Server-Wartung ist fehlgeschlagen!\nBitte Logs prüfen." \
-F "priority=8" || true

View File

@@ -2,6 +2,8 @@ name: Monorepo Pipeline
on: on:
push: push:
branches:
- '**'
tags: tags:
- 'v*' - 'v*'
@@ -10,43 +12,124 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
qa: prioritize:
name: 🧪 Quality Assurance name: ⚡ Prioritize Release
runs-on: docker runs-on: docker
container: container:
image: catthehacker/ubuntu:act-latest image: catthehacker/ubuntu:act-latest
steps:
- name: 🛑 Cancel Redundant Runs
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
REF: ${{ github.ref }}
REF_NAME: ${{ github.ref_name }}
EVENT: ${{ github.event_name }}
run: |
echo "🔎 Debug: Event=$EVENT, Ref=$REF, RefName=$REF_NAME, RunId=$RUN_ID"
case "$REF" in
refs/tags/v*)
echo "🚀 Release detected ($REF_NAME). Cancelling non-tag runs..."
# Fetch all runs
RUNS=$(curl -s -H "Authorization: token $GITEA_TOKEN" "https://git.infra.mintel.me/api/v1/repos/$REPO/actions/runs")
# Identify runs to cancel: in_progress/queued, NOT this run, and NOT a tag run
echo "$RUNS" | jq -c '.workflow_runs[] | select(.status == "in_progress" or .status == "queued") | select(.id | tostring != "'$RUN_ID'")' | while read -r run; do
ID=$(echo "$run" | jq -r '.id')
RUN_REF=$(echo "$run" | jq -r '.ref')
TITLE=$(echo "$run" | jq -r '.display_title')
case "$RUN_REF" in
refs/tags/v*)
echo "⏭️ Skipping parallel release run $ID ($TITLE) on $RUN_REF"
;;
*)
echo "🛑 Cancelling redundant branch run $ID ($TITLE) on $RUN_REF..."
curl -X POST -s -H "Authorization: token $GITEA_TOKEN" "https://git.infra.mintel.me/api/v1/repos/$REPO/actions/runs/$ID/cancel"
;;
esac
done
;;
*)
echo " Regular push. No prioritization needed."
;;
esac
lint:
name: 🧹 Lint
needs: prioritize
if: always() && !cancelled() && (needs.prioritize.result == 'success' || needs.prioritize.result == 'skipped')
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node_version: 20 node_version: 20
cache: 'pnpm' - name: Enable pnpm
run: corepack enable && corepack prepare pnpm@10.2.0 --activate
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts --no-color
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Lint - name: Lint
run: pnpm lint run: pnpm lint
test:
name: 🧪 Test
needs: prioritize
if: always() && !cancelled() && (needs.prioritize.result == 'success' || needs.prioritize.result == 'skipped')
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node_version: 20
- name: Enable pnpm
run: corepack enable && corepack prepare pnpm@10.2.0 --activate
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts --no-color
- name: Test - name: Test
run: pnpm test run: pnpm test
build:
name: 🏗️ Build
needs: prioritize
if: always() && !cancelled() && (needs.prioritize.result == 'success' || needs.prioritize.result == 'skipped')
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node_version: 20
- name: Enable pnpm
run: corepack enable && corepack prepare pnpm@10.2.0 --activate
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts --no-color
- name: Build - name: Build
run: pnpm build run: pnpm build
release: release:
name: 🚀 Release name: 🚀 Release
needs: qa needs: [lint, test, build]
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
runs-on: docker runs-on: docker
container: container:
@@ -59,30 +142,24 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node_version: 20 node_version: 20
cache: 'pnpm' - name: Enable pnpm
run: corepack enable && corepack prepare pnpm@10.2.0 --activate
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts --no-color
- name: 🏷️ Sync Versions (if Tagged)
run: pnpm sync-versions
- name: 🏷️ Release Packages (Tag-Driven) - name: 🏷️ Release Packages (Tag-Driven)
run: | run: |
echo "🏷️ Tag detected [${{ github.ref_name }}], performing sync release..." echo "🏷️ Tag detected [${{ github.ref_name }}], performing sync release..."
pnpm sync-versions
pnpm release:tag pnpm release:tag
build-images: build-images:
name: 🐳 Build ${{ matrix.name }} name: 🐳 Build ${{ matrix.name }}
needs: qa needs: [lint, test, build]
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
runs-on: docker runs-on: docker
container: container:

37
.husky/pre-push Executable file
View File

@@ -0,0 +1,37 @@
# Check if we are pushing a tag
while read local_ref local_sha remote_ref remote_sha
do
if [[ "$remote_ref" == refs/tags/v* ]]; then
TAG=${remote_ref#refs/tags/}
echo "🏷️ Tag detected: $TAG, ensuring versions are synced..."
# Run sync script
pnpm sync-versions "$TAG"
# Check for changes in relevant files
SYNC_FILES="package.json packages/*/package.json apps/*/package.json .env.example"
CHANGES=$(git status --porcelain $SYNC_FILES)
if [[ -n "$CHANGES" ]]; then
echo "📝 Version sync made changes. Integrating into tag..."
# Stage and commit
git add $SYNC_FILES
git commit -m "chore: sync versions to $TAG" --no-verify
# Force update the local tag to point to the new commit
git tag -f "$TAG" > /dev/null
echo "✅ Tag $TAG has been updated locally with synced versions."
echo "🚀 Auto-pushing updated tag..."
# 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
else
echo "✨ Versions already in sync for $TAG."
fi
fi
done

56
Dockerfile.template Normal file
View File

@@ -0,0 +1,56 @@
# Stage 1: Builder
FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder
WORKDIR /app
# Clean the workspace in case the base image is dirty
RUN rm -rf ./*
# Arguments for build-time configuration
ARG NEXT_PUBLIC_BASE_URL
ARG NEXT_PUBLIC_TARGET
ARG DIRECTUS_URL
ARG NPM_TOKEN
# Environment variables for Next.js build
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
ENV DIRECTUS_URL=$DIRECTUS_URL
ENV SKIP_RUNTIME_ENV_VALIDATION=true
ENV CI=true
# Enable pnpm
RUN corepack enable
# Copy lockfile and manifest for dependency installation caching
COPY pnpm-lock.yaml package.json .npmrc* ./
# Install dependencies with cache mount
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) && \
pnpm install --frozen-lockfile
# Copy source code
COPY . .
# Build application
RUN pnpm build
# Stage 2: Runner
FROM registry.infra.mintel.me/mintel/runtime:latest AS runner
WORKDIR /app
ENV HOSTNAME="0.0.0.0"
ENV PORT=3000
ENV NODE_ENV=production
# Copy standalone output and static files
# Adjust paths if using a monorepo structure (e.g., /app/apps/web/public)
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/.next/cache ./.next/cache
USER nextjs
CMD ["node", "server.js"]

View File

@@ -80,3 +80,4 @@ Client websites scaffolded via the CLI use a **tag-based deployment** strategy:
- **Git Tag `v*.*.*`**: Deploys to the `production` environment. - **Git Tag `v*.*.*`**: Deploys to the `production` environment.
See the [`@mintel/infra`](packages/infra/README.md) package for detailed template documentation. See the [`@mintel/infra`](packages/infra/README.md) package for detailed template documentation.

View File

@@ -1,3 +0,0 @@
import { nextConfig } from "@mintel/eslint-config/next";
export default nextConfig;

View File

@@ -1,6 +1,6 @@
{ {
"name": "sample-website", "name": "sample-website",
"version": "0.1.1", "version": "1.8.2",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -8,15 +8,9 @@
"dev:local": "mintel dev --local", "dev:local": "mintel dev --local",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "eslint src/",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests", "test": "vitest run --passWithNoTests",
"cms:bootstrap": "mintel directus bootstrap",
"cms:push:testing": "mintel directus sync push testing",
"cms:pull:testing": "mintel directus sync pull testing",
"cms:push:staging": "mintel directus sync push staging",
"cms:pull:staging": "mintel directus sync pull staging",
"cms:push:prod": "mintel directus sync push production",
"cms:pull:prod": "mintel directus sync pull production", "cms:pull:prod": "mintel directus sync pull production",
"pagespeed:test": "mintel pagespeed" "pagespeed:test": "mintel pagespeed"
}, },
@@ -24,8 +18,8 @@
"@mintel/next-utils": "workspace:*", "@mintel/next-utils": "workspace:*",
"@mintel/observability": "workspace:*", "@mintel/observability": "workspace:*",
"@mintel/next-observability": "workspace:*", "@mintel/next-observability": "workspace:*",
"@sentry/nextjs": "^8.55.0", "@sentry/nextjs": "10.38.0",
"next": "15.1.6", "next": "16.1.6",
"next-intl": "^4.8.2", "next-intl": "^4.8.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0"

View File

@@ -0,0 +1,19 @@
version: 1
directus: 11.15.1
vendor: postgres
collections: []
fields: []
systemFields:
- collection: directus_activity
field: timestamp
schema:
is_indexed: true
- collection: directus_revisions
field: activity
schema:
is_indexed: true
- collection: directus_revisions
field: parent
schema:
is_indexed: true
relations: []

View File

View File

@@ -0,0 +1 @@
S9WsV

View File

@@ -1,17 +1,18 @@
services: services:
app: app:
build: build:
context: . context: ./apps/sample-website
dockerfile: Dockerfile dockerfile: Dockerfile
args: args:
NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL:-http://localhost:3000} NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL:-http://localhost:3000}
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${NEXT_PUBLIC_UMAMI_WEBSITE_ID} NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${NEXT_PUBLIC_UMAMI_WEBSITE_ID}
NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${NEXT_PUBLIC_UMAMI_SCRIPT_URL} NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${NEXT_PUBLIC_UMAMI_SCRIPT_URL}
NEXT_PUBLIC_TARGET: ${TARGET:-development} NEXT_PUBLIC_TARGET: ${TARGET:-development}
DIRECTUS_URL: ${DIRECTUS_URL:-http://directus:8055}
restart: always restart: always
networks: networks:
- infra - infra
environment:
- DIRECTUS_URL=${DIRECTUS_URL:-http://directus:8055}
env_file: env_file:
- .env - .env
ports: ports:
@@ -22,7 +23,13 @@ services:
- "traefik.http.services.sample-website.loadbalancer.server.port=3000" - "traefik.http.services.sample-website.loadbalancer.server.port=3000"
directus: directus:
image: registry.infra.mintel.me/mintel/directus:latest image: registry.infra.mintel.me/mintel/directus:${IMAGE_TAG:-latest}
healthcheck:
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8055/server/health" ]
interval: 30s
timeout: 10s
retries: 3
start_period: 5s
restart: always restart: always
networks: networks:
- infra - infra
@@ -34,7 +41,7 @@ services:
ADMIN_EMAIL: ${DIRECTUS_ADMIN_EMAIL:-admin@mintel.me} ADMIN_EMAIL: ${DIRECTUS_ADMIN_EMAIL:-admin@mintel.me}
ADMIN_PASSWORD: ${DIRECTUS_ADMIN_PASSWORD:-mintel-admin} ADMIN_PASSWORD: ${DIRECTUS_ADMIN_PASSWORD:-mintel-admin}
DB_CLIENT: 'pg' DB_CLIENT: 'pg'
DB_HOST: 'directus-db' DB_HOST: 'at-mintel-directus-db'
DB_PORT: '5432' DB_PORT: '5432'
DB_DATABASE: ${DIRECTUS_DB_NAME:-directus} DB_DATABASE: ${DIRECTUS_DB_NAME:-directus}
DB_USER: ${DIRECTUS_DB_USER:-directus} DB_USER: ${DIRECTUS_DB_USER:-directus}
@@ -46,12 +53,13 @@ services:
volumes: volumes:
- ./directus/uploads:/directus/uploads - ./directus/uploads:/directus/uploads
- ./directus/extensions:/directus/extensions - ./directus/extensions:/directus/extensions
- ./directus/schema:/directus/schema
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.sample-website-directus.rule=Host(`${DIRECTUS_HOST:-cms.sample-website.localhost}`)" - "traefik.http.routers.sample-website-directus.rule=Host(`${DIRECTUS_HOST:-cms.sample-website.localhost}`)"
- "traefik.http.services.sample-website-directus.loadbalancer.server.port=8055" - "traefik.http.services.sample-website-directus.loadbalancer.server.port=8055"
directus-db: at-mintel-directus-db:
image: postgres:15-alpine image: postgres:15-alpine
restart: always restart: always
networks: networks:

View File

@@ -5,9 +5,13 @@ export default [
{ {
ignores: [ ignores: [
"packages/cms-infra/extensions/**", "packages/cms-infra/extensions/**",
"packages/customer-manager/index.js", "**/index.js",
"**/*.db", "**/*.db",
"**/build/**", "**/build/**",
"**/data/**",
"**/reference/**",
"**/dist/**",
"**/.next/**",
], ],
}, },
...baseConfig, ...baseConfig,

View File

@@ -5,11 +5,20 @@
"scripts": { "scripts": {
"build": "pnpm -r build", "build": "pnpm -r build",
"dev": "pnpm -r dev", "dev": "pnpm -r dev",
"lint": "pnpm -r lint", "lint": "pnpm -r --filter='./packages/**' --filter='./apps/**' lint",
"test": "pnpm -r test", "test": "pnpm -r test",
"changeset": "changeset", "changeset": "changeset",
"version-packages": "changeset version", "version-packages": "changeset version",
"sync-versions": "tsx scripts/sync-versions.ts", "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: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": "./scripts/sync-extensions.sh && 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",
"release": "pnpm build && changeset publish", "release": "pnpm build && changeset publish",
"release:tag": "pnpm build && pnpm -r publish --no-git-checks --access public", "release:tag": "pnpm build && pnpm -r publish --no-git-checks --access public",
"prepare": "husky" "prepare": "husky"
@@ -27,7 +36,7 @@
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.2",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-plugin-next": "^0.0.0", "@next/eslint-plugin-next": "16.1.6",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"happy-dom": "^20.4.0", "happy-dom": "^20.4.0",
@@ -45,5 +54,12 @@
"pino": "^10.3.1", "pino": "^10.3.1",
"pino-pretty": "^13.1.3", "pino-pretty": "^13.1.3",
"require-in-the-middle": "^8.0.1" "require-in-the-middle": "^8.0.1"
},
"version": "1.8.2",
"pnpm": {
"overrides": {
"next": "16.1.6",
"@sentry/nextjs": "10.38.0"
}
} }
} }

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,27 @@
{
"name": "acquisition-manager",
"description": "Custom High-Fidelity Acquisition Management for Directus",
"icon": "account_balance_wallet",
"version": "1.8.2",
"type": "module",
"keywords": [
"directus",
"directus-extension",
"directus-extension-module"
],
"directus:extension": {
"type": "module",
"path": "index.js",
"source": "src/index.ts",
"host": "*",
"name": "Acquisition Manager"
},
"scripts": {
"build": "directus-extension build && cp -f dist/index.js index.js 2>/dev/null || true",
"dev": "directus-extension build -w"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"vue": "^3.4.0"
}
}

View File

@@ -0,0 +1,19 @@
import { defineModule } from "@directus/extensions-sdk";
import ModuleComponent from "./module.vue";
export default defineModule({
id: "acquisition-manager",
name: "Acquisition",
icon: "auto_awesome",
routes: [
{
path: "",
component: ModuleComponent,
},
{
path: ":id",
component: ModuleComponent,
props: true
}
],
});

View File

@@ -0,0 +1,445 @@
<template>
<private-view title="Acquisition Manager">
<template #navigation>
<v-list nav>
<v-list-item @click="showAddLead = true" clickable>
<v-list-item-icon><v-icon name="add" color="var(--theme--primary)" /></v-list-item-icon>
<v-list-item-content>
<v-text-overflow text="Neuen Lead anlegen" />
</v-list-item-content>
</v-list-item>
<v-divider />
<v-list-item
v-for="lead in leads"
:key="lead.id"
:active="selectedLeadId === lead.id"
class="lead-item"
clickable
@click="selectLead(lead.id)"
>
<v-list-item-icon>
<v-icon :name="getStatusIcon(lead.status)" :color="getStatusColor(lead.status)" />
</v-list-item-icon>
<v-list-item-content>
<v-text-overflow :text="getCompanyName(lead)" />
</v-list-item-content>
</v-list-item>
</v-list>
</template>
<template #title-outer:after>
<v-notice v-if="notice" :type="notice.type" @close="notice = null" dismissible>
{{ notice.message }}
</v-notice>
</template>
<div class="content-wrapper">
<v-notice type="success" style="margin-bottom: 16px;">
DEBUG: Module Version 1.1.0 - Native Build - {{ new Date().toISOString() }}
</v-notice>
<div v-if="!selectedLead" class="empty-state">
<v-info title="Lead auswählen" icon="auto_awesome" center>
Wähle einen Lead in der Navigation aus oder
<v-button x-small @click="showAddLead = true">registriere einen neuen Lead</v-button>.
</v-info>
</div>
<template v-else>
<header class="header">
<div class="header-left">
<h1 class="title">{{ getCompanyName(selectedLead) }}</h1>
<p class="subtitle">
<v-icon name="language" x-small />
<a :href="selectedLead.website_url" target="_blank" class="url-link">
{{ selectedLead.website_url.replace(/^https?:\/\//, '') }}
</a>
&middot; Status: {{ selectedLead.status.toUpperCase() }}
</p>
</div>
<div class="header-right">
<v-button
v-if="selectedLead.status === 'new'"
secondary
:loading="loadingAudit"
@click="runAudit"
>
<v-icon name="settings_suggest" left />
Audit starten
</v-button>
<template v-if="selectedLead.status === 'audit_ready'">
<v-button secondary :loading="loadingEmail" @click="sendAuditEmail">
<v-icon name="mail" left />
Audit E-Mail
</v-button>
<v-button :loading="loadingPdf" @click="generatePdf">
<v-icon name="picture_as_pdf" left />
PDF Erstellen
</v-button>
</template>
<v-button v-if="selectedLead.audit_pdf_path" secondary icon v-tooltip.bottom="'PDF öffnen'" @click="openPdf">
<v-icon name="open_in_new" />
</v-button>
<v-button
v-if="selectedLead.audit_pdf_path"
primary
:loading="loadingEmail"
@click="sendEstimateEmail"
>
<v-icon name="send" left />
Angebot senden
</v-button>
</div>
</header>
<div class="sections">
<div class="main-info">
<div class="form-grid">
<div class="field">
<span class="label">Kontaktperson</span>
<div v-if="selectedLead.contact_person" class="value person-link" @click="goToPerson(selectedLead.contact_person)">
{{ getPersonName(selectedLead.contact_person) }}
</div>
<div v-else class="value text-subdued">Keine Person verknüpft</div>
</div>
<div class="field">
<span class="label">E-Mail (Legacy)</span>
<div class="value">{{ selectedLead.contact_email || '—' }}</div>
</div>
<div class="field full">
<span class="label">Briefing / Fokus</span>
<div class="value text-block">{{ selectedLead.briefing || 'Kein Briefing hinterlegt.' }}</div>
</div>
</div>
</div>
<v-divider />
<div v-if="selectedLead.ai_state" class="ai-observations">
<h3 class="section-title">AI Observations & Estimation</h3>
<div class="metrics">
<v-info label="Projekt-Modus" :value="selectedLead.ai_state.projectType || 'Unbekannt'" />
<v-info label="Seitenanzahl" :value="selectedLead.ai_state.sitemap?.length || '0'" />
</div>
<v-table
v-if="selectedLead.ai_state.sitemap"
:headers="[ { text: 'Seite', value: 'title' }, { text: 'URL', value: 'url' } ]"
:items="selectedLead.ai_state.sitemap"
class="observation-table"
>
<template #[`item.title`]="{ item }">
<span class="page-title">{{ item.title }}</span>
</template>
<template #[`item.url`]="{ item }">
<span class="page-url">{{ item.url }}</span>
</template>
</v-table>
</div>
</div>
</template>
</div>
<!-- Drawer: New Lead -->
<v-drawer
v-model="showAddLead"
title="Neuen Lead registrieren"
icon="person_add"
@cancel="showAddLead = false"
>
<div class="drawer-content">
<div class="form-section">
<div class="field">
<span class="label">Organisation / Firma (Zentral)</span>
<v-select
v-model="newLead.company"
:items="companyOptions"
placeholder="Bestehende Firma auswählen..."
/>
</div>
<div class="field">
<span class="label">Organisation / Firma (Legacy / Neu)</span>
<v-input v-model="newLead.company_name" placeholder="z.B. Schmidt GmbH" autofocus />
</div>
<div class="field">
<span class="label">Website URL</span>
<v-input v-model="newLead.website_url" placeholder="https://..." />
</div>
<div class="field">
<span class="label">Ansprechpartner</span>
<v-input v-model="newLead.contact_name" placeholder="Vorname Nachname" />
</div>
<div class="field">
<span class="label">E-Mail Adresse</span>
<v-input v-model="newLead.contact_email" placeholder="email@beispiel.de" type="email" />
</div>
<div class="field">
<span class="label">Briefing / Fokus</span>
<v-textarea v-model="newLead.briefing" placeholder="Besonderheiten für das Audit..." />
</div>
<div class="field">
<span class="label">Kontaktperson (Optional)</span>
<v-select
v-model="newLead.contact_person"
:items="peopleOptions"
placeholder="Person auswählen..."
/>
</div>
</div>
<div class="drawer-actions">
<v-button primary block :loading="savingLead" @click="saveLead">Lead speichern</v-button>
</div>
</div>
</v-drawer>
</private-view>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useApi } from '@directus/extensions-sdk';
const api = useApi();
const leads = ref<any[]>([]);
const selectedLeadId = ref<string | null>(null);
const loadingAudit = ref(false);
const loadingPdf = ref(false);
const loadingEmail = ref(false);
const showAddLead = ref(false);
const savingLead = ref(false);
const notice = ref<{ type: string; message: string } | null>(null);
const newLead = ref({
company_name: '', // Legacy
company: null,
website_url: '',
contact_name: '', // Legacy
contact_email: '', // Legacy
contact_person: null,
briefing: '',
status: 'new'
});
const companies = ref<any[]>([]);
const people = ref<any[]>([]);
const companyOptions = computed(() =>
companies.value.map(c => ({
text: c.name,
value: c.id
}))
);
const peopleOptions = computed(() =>
people.value.map(p => ({
text: `${p.first_name} ${p.last_name}`,
value: p.id
}))
);
function getCompanyName(lead: any) {
if (lead.company) {
return typeof lead.company === 'object' ? lead.company.name : (companies.value.find(c => c.id === lead.company)?.name || lead.company_name);
}
return lead.company_name;
}
function getPersonName(id: string | any) {
if (!id) return '';
if (typeof id === 'object') return `${id.first_name} ${id.last_name}`;
const person = people.value.find(p => p.id === id);
return person ? `${person.first_name} ${person.last_name}` : id;
}
function goToPerson(id: string) {
// Logic to navigate to people manager or open details
notice.value = { type: 'info', message: `Navigiere zu Person: ${id}` };
}
const selectedLead = computed(() => leads.value.find(l => l.id === selectedLeadId.value));
onMounted(fetchData);
async function fetchData() {
const [leadsResp, peopleResp, companiesResp] = await Promise.all([
api.get('/items/leads', {
params: {
sort: '-date_created',
fields: '*.*'
}
}),
api.get('/items/people', { params: { sort: 'last_name' } }),
api.get('/items/companies', { params: { sort: 'name' } })
]);
leads.value = leadsResp.data.data;
people.value = peopleResp.data.data;
companies.value = companiesResp.data.data;
if (!selectedLeadId.value && leads.value.length > 0) {
selectedLeadId.value = leads.value[0].id;
}
}
async function fetchLeads() {
await fetchData();
}
function selectLead(id: string) {
selectedLeadId.value = id;
}
async function runAudit() {
if (!selectedLeadId.value) return;
loadingAudit.value = true;
try {
await api.post(`/acquisition/audit/${selectedLeadId.value}`);
notice.value = { type: 'success', message: 'Audit erfolgreich gestartet!' };
await fetchLeads();
} catch (e: any) {
notice.value = { type: 'danger', message: `Fehler beim Audit: ${e.message}` };
} finally {
loadingAudit.value = false;
}
}
async function sendAuditEmail() {
if (!selectedLeadId.value) return;
loadingEmail.value = true;
try {
await api.post(`/acquisition/audit-email/${selectedLeadId.value}`);
notice.value = { type: 'success', message: 'Audit E-Mail versendet!' };
await fetchLeads();
} catch (e: any) {
notice.value = { type: 'danger', message: `Fehler beim Versenden: ${e.message}` };
} finally {
loadingEmail.value = false;
}
}
async function generatePdf() {
if (!selectedLeadId.value) return;
loadingPdf.value = true;
try {
await api.post(`/acquisition/estimate/${selectedLeadId.value}`);
notice.value = { type: 'success', message: 'Angebot (PDF) wurde generiert!' };
await fetchLeads();
} catch (e: any) {
notice.value = { type: 'danger', message: `Fehler bei PDF Generierung: ${e.message}` };
} finally {
loadingPdf.value = false;
}
}
async function sendEstimateEmail() {
if (!selectedLeadId.value) return;
loadingEmail.value = true;
try {
await api.post(`/acquisition/estimate-email/${selectedLeadId.value}`);
notice.value = { type: 'success', message: 'Angebot erfolgreich versendet!' };
await fetchLeads();
} catch (e: any) {
notice.value = { type: 'danger', message: `Fehler beim Versenden: ${e.message}` };
} finally {
loadingEmail.value = false;
}
}
function openPdf() {
if (!selectedLead.value?.audit_pdf_path) return;
window.open(`${window.location.origin}/assets/${selectedLead.value.audit_pdf_path}`, '_blank');
}
async function saveLead() {
if (!newLead.value.company_name && !newLead.value.company) {
notice.value = { type: 'danger', message: 'Firma oder Firmenname erforderlich.' };
return;
}
savingLead.value = true;
try {
const payload = {
id: crypto.randomUUID(),
...newLead.value
};
await api.post('/items/leads', payload);
notice.value = { type: 'success', message: 'Lead erfolgreich registriert!' };
showAddLead.value = false;
await fetchLeads();
selectedLeadId.value = payload.id;
newLead.value = {
company_name: '',
company: null,
website_url: '',
contact_name: '',
contact_email: '',
contact_person: null,
briefing: '',
status: 'new'
};
} catch (e: any) {
notice.value = { type: 'danger', message: `Fehler beim Speichern: ${e.message}` };
} finally {
savingLead.value = false;
}
}
function getStatusIcon(status: string) {
switch(status) {
case 'new': return 'fiber_new';
case 'auditing': return 'hourglass_empty';
case 'audit_ready': return 'check_circle';
case 'contacted': return 'mail_outline';
default: return 'help_outline';
}
}
function getStatusColor(status: string) {
switch(status) {
case 'new': return 'var(--theme--primary)';
case 'auditing': return 'var(--theme--warning)';
case 'audit_ready': return 'var(--theme--success)';
case 'contacted': return 'var(--theme--secondary)';
default: return 'var(--theme--foreground-subdued)';
}
}
</script>
<style scoped>
.content-wrapper { padding: 32px; height: 100%; display: flex; flex-direction: column; overflow-y: auto; }
.lead-item { cursor: pointer; }
.header { margin-bottom: 24px; display: flex; justify-content: space-between; align-items: flex-end; }
.header-right { display: flex; gap: 12px; }
.title { font-size: 24px; font-weight: 800; margin-bottom: 4px; color: var(--theme--foreground); }
.subtitle { color: var(--theme--foreground-subdued); font-size: 14px; display: flex; align-items: center; gap: 8px; }
.url-link { color: inherit; text-decoration: none; border-bottom: 1px solid transparent; }
.url-link:hover { border-bottom-color: currentColor; }
.empty-state { height: 100%; display: flex; align-items: center; justify-content: center; }
.sections { display: flex; flex-direction: column; gap: 32px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
.field { display: flex; flex-direction: column; gap: 8px; }
.field.full { grid-column: span 2; }
.label { font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--theme--foreground-subdued); letter-spacing: 0.5px; }
.value { font-size: 15px; color: var(--theme--foreground); }
.text-block { line-height: 1.6; white-space: pre-wrap; background: var(--theme--background-subdued); padding: 16px; border-radius: 8px; }
.ai-observations { display: flex; flex-direction: column; gap: 16px; }
.section-title { font-size: 16px; font-weight: 700; color: var(--theme--foreground); margin-bottom: 8px; }
.metrics { display: flex; gap: 32px; margin-bottom: 16px; }
.observation-table { border: 1px solid var(--theme--border); border-radius: 8px; overflow: hidden; }
.page-title { font-weight: 600; }
.page-url { font-family: var(--family-monospace); font-size: 12px; color: var(--theme--foreground-subdued); }
.drawer-content { padding: 24px; display: flex; flex-direction: column; gap: 32px; }
.form-section { display: flex; flex-direction: column; gap: 20px; }
.drawer-actions { margin-top: 24px; display: flex; flex-direction: column; gap: 12px; }
:deep(.v-list-item) { cursor: pointer !important; }
</style>

View File

@@ -0,0 +1,49 @@
import { build } from 'esbuild';
import { resolve, dirname } from 'path';
import { mkdirSync } from 'fs';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const entryPoint = resolve(__dirname, 'src/index.ts');
const outfile = resolve(__dirname, 'dist/index.js');
try {
mkdirSync(dirname(outfile), { recursive: true });
} catch (e) { }
console.log(`Building from ${entryPoint} to ${outfile}...`);
build({
entryPoints: [entryPoint],
bundle: true,
platform: 'node',
target: 'node18',
outfile: outfile,
jsx: 'automatic',
loader: {
'.tsx': 'tsx',
'.ts': 'ts',
'.js': 'js',
},
external: ["@react-pdf/renderer", "react", "react-dom", "jsdom", "jsdom/*", "jquery", "jquery/*", "canvas", "fs", "path", "os", "http", "https", "zlib", "stream", "util", "url", "net", "tls", "crypto"],
plugins: [{
name: 'mock-canvas',
setup(build) {
build.onResolve({ filter: /^canvas/ }, args => ({ path: args.path, namespace: 'mock-canvas' }));
build.onLoad({ filter: /.*/, namespace: 'mock-canvas' }, () => ({ contents: 'export default {};', loader: 'js' }));
}
}, {
name: 'mock-jsdom',
setup(build) {
build.onResolve({ filter: /^jsdom/ }, args => ({ path: args.path, namespace: 'mock-jsdom' }));
build.onLoad({ filter: /.*/, namespace: 'mock-jsdom' }, () => ({ contents: 'export default {};', loader: 'js' }));
}
}]
}).then(() => {
console.log("Build succeeded!");
}).catch((e) => {
console.error("Build failed:", e);
process.exit(1);
});

View File

@@ -0,0 +1,27 @@
{
"name": "acquisition",
"version": "1.8.2",
"type": "module",
"directus:extension": {
"type": "endpoint",
"path": "dist/index.js",
"source": "src/index.ts",
"host": "^11.0.0"
},
"scripts": {
"build": "node build.mjs",
"dev": "node build.mjs --watch"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"@mintel/pdf": "workspace:*",
"@mintel/mail": "workspace:*",
"esbuild": "^0.25.0",
"typescript": "^5.6.3"
},
"dependencies": {
"jquery": "^3.7.1",
"react": "^19.2.4",
"react-dom": "^19.2.4"
}
}

View File

@@ -0,0 +1,176 @@
import { defineEndpoint } from "@directus/extensions-sdk";
import { AcquisitionService, PdfEngine } from "@mintel/pdf/server";
import { render, SiteAuditTemplate, ProjectEstimateTemplate } from "@mintel/mail";
import { createElement } from "react";
import * as path from "path";
import * as fs from "fs";
export default defineEndpoint((router, { services, env }) => {
const { ItemsService, MailService } = services;
router.get("/ping", (req, res) => res.send("pong"));
router.post("/audit/:id", async (req: any, res: any) => {
const { id } = req.params;
const leadsService = new ItemsService("leads", { schema: req.schema, accountability: req.accountability });
try {
const lead = await leadsService.readOne(id);
if (!lead) return res.status(404).send({ error: "Lead not found" });
await leadsService.updateOne(id, { status: "auditing" });
const acqService = new AcquisitionService(env.OPENROUTER_API_KEY);
const result = await acqService.runFullSequence(lead.website_url, lead.briefing, lead.comments);
await leadsService.updateOne(id, {
status: "audit_ready",
ai_state: result.state,
audit_context: JSON.stringify(result.usage),
});
res.send({ success: true, result });
} catch (error: any) {
console.error("Audit failed:", error);
await leadsService.updateOne(id, { status: "new", comments: `Audit failed: ${error.message}` });
res.status(500).send({ error: error.message });
}
});
router.post("/audit-email/:id", async (req: any, res: any) => {
const { id } = req.params;
const { ItemsService, MailService } = services;
const leadsService = new ItemsService("leads", { schema: req.schema, accountability: req.accountability });
const peopleService = new ItemsService("people", { schema: req.schema, accountability: req.accountability });
const companiesService = new ItemsService("companies", { schema: req.schema, accountability: req.accountability });
const mailService = new MailService({ schema: req.schema, accountability: req.accountability });
try {
const lead = await leadsService.readOne(id, { fields: ["*", "company.*", "contact_person.*"] });
if (!lead || !lead.ai_state) return res.status(400).send({ error: "Lead or Audit not ready" });
let recipientEmail = lead.contact_email;
let companyName = lead.company?.name || lead.company_name;
if (lead.contact_person) {
recipientEmail = lead.contact_person.email || recipientEmail;
if (lead.contact_person.company) {
const personCompany = await companiesService.readOne(lead.contact_person.company);
companyName = personCompany?.name || companyName;
}
}
if (!recipientEmail) return res.status(400).send({ error: "No recipient email found" });
const auditHighlights = [
`Projekt-Typ: ${lead.ai_state.projectType === "website" ? "Website" : "Web App"}`,
...(lead.ai_state.sitemap || []).slice(0, 3).map((item: any) => `Potenzial in: ${item.category}`),
];
const html = await render(createElement(SiteAuditTemplate, {
companyName: companyName,
websiteUrl: lead.website_url,
auditHighlights
}));
await mailService.send({
to: recipientEmail,
subject: `Analyse Ihrer Webpräsenz: ${companyName}`,
html
});
await leadsService.updateOne(id, {
status: "contacted",
last_contacted_at: new Date().toISOString(),
});
res.send({ success: true });
} catch (error: any) {
console.error("Audit Email failed:", error);
res.status(500).send({ error: error.message });
}
});
router.post("/estimate/:id", async (req: any, res: any) => {
const { id } = req.params;
const leadsService = new ItemsService("leads", { schema: req.schema, accountability: req.accountability });
try {
const lead = await leadsService.readOne(id);
if (!lead || !lead.ai_state) return res.status(400).send({ error: "Lead or AI state not found" });
const pdfEngine = new PdfEngine();
const filename = `estimate_${id}_${Date.now()}.pdf`;
const storageRoot = env.STORAGE_LOCAL_ROOT || "./storage";
const outputPath = path.join(storageRoot, filename);
await pdfEngine.generateEstimatePdf(lead.ai_state, outputPath);
await leadsService.updateOne(id, {
audit_pdf_path: filename,
});
res.send({ success: true, filename });
} catch (error: any) {
console.error("PDF Generation failed:", error);
res.status(500).send({ error: error.message });
}
});
router.post("/estimate-email/:id", async (req: any, res: any) => {
const { id } = req.params;
const leadsService = new ItemsService("leads", { schema: req.schema, accountability: req.accountability });
const peopleService = new ItemsService("people", { schema: req.schema, accountability: req.accountability });
const companiesService = new ItemsService("companies", { schema: req.schema, accountability: req.accountability });
const mailService = new MailService({ schema: req.schema, accountability: req.accountability });
try {
const lead = await leadsService.readOne(id, { fields: ["*", "company.*", "contact_person.*"] });
if (!lead || !lead.audit_pdf_path) return res.status(400).send({ error: "PDF not generated" });
let recipientEmail = lead.contact_email;
let companyName = lead.company?.name || lead.company_name;
if (lead.contact_person) {
recipientEmail = lead.contact_person.email || recipientEmail;
if (lead.contact_person.company) {
const personCompany = await companiesService.readOne(lead.contact_person.company);
companyName = personCompany?.name || companyName;
}
}
if (!recipientEmail) return res.status(400).send({ error: "No recipient email found" });
const html = await render(createElement(ProjectEstimateTemplate, {
companyName: companyName,
}));
const storageRoot = env.STORAGE_LOCAL_ROOT || "./storage";
const attachmentPath = path.join(storageRoot, lead.audit_pdf_path);
await mailService.send({
to: recipientEmail,
subject: `Ihre Projekt-Schätzung: ${companyName}`,
html,
attachments: [
{
filename: `Angebot_${companyName}.pdf`,
content: fs.readFileSync(attachmentPath)
}
]
});
await leadsService.updateOne(id, {
status: "contacted",
last_contacted_at: new Date().toISOString(),
});
res.send({ success: true });
} catch (error: any) {
console.error("Estimate Email failed:", error);
res.status(500).send({ error: error.message });
}
});
});

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/cli", "name": "@mintel/cli",
"version": "1.0.1", "version": "1.8.2",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://npm.infra.mintel.me" "registry": "https://npm.infra.mintel.me"
@@ -28,4 +28,4 @@
"@types/prompts": "^2.4.4", "@types/prompts": "^2.4.4",
"@mintel/tsconfig": "workspace:*" "@mintel/tsconfig": "workspace:*"
} }
} }

View File

@@ -231,7 +231,7 @@ program
"pagespeed:test": "mintel pagespeed", "pagespeed:test": "mintel pagespeed",
}, },
dependencies: { dependencies: {
next: "15.1.6", next: "16.1.6",
react: "^19.0.0", react: "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"@mintel/next-utils": "workspace:*", "@mintel/next-utils": "workspace:*",

View File

@@ -0,0 +1,41 @@
import { build } from 'esbuild';
import { resolve, dirname } from 'path';
import { mkdirSync } from 'fs';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const entryPoints = [
resolve(__dirname, 'src/index.ts')
];
try {
mkdirSync(resolve(__dirname, 'dist'), { recursive: true });
} catch (e) { }
console.log(`Building entry point...`);
build({
entryPoints: entryPoints,
bundle: true,
platform: 'node',
target: 'node18',
outdir: resolve(__dirname, 'dist'),
format: 'esm',
loader: {
'.ts': 'ts',
'.js': 'js',
},
external: ["playwright", "crawlee", "axios", "cheerio", "fs", "path", "os", "http", "https", "url", "stream", "util", "child_process"],
}).then(() => {
console.log("Build succeeded!");
}).catch((e) => {
if (e.errors) {
console.error("Build failed with errors:");
e.errors.forEach(err => console.error(` ${err.text} at ${err.location?.file}:${err.location?.line}`));
} else {
console.error("Build failed:", e);
}
process.exit(1);
});

View File

@@ -0,0 +1,30 @@
{
"name": "@mintel/cloner",
"version": "1.8.2",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "node build.mjs",
"dev": "node build.mjs --watch"
},
"devDependencies": {
"esbuild": "^0.25.0",
"typescript": "^5.6.3",
"@types/node": "^22.0.0"
},
"dependencies": {
"playwright": "^1.40.0",
"crawlee": "^3.7.0",
"axios": "^1.6.0",
"cheerio": "^1.0.0-rc.12"
}
}

View File

@@ -0,0 +1,93 @@
import axios from "axios";
import fs from "node:fs";
import path from "node:path";
export interface AssetMap {
[originalUrl: string]: string;
}
export class AssetManager {
private userAgent: string;
constructor(userAgent: string = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36") {
this.userAgent = userAgent;
}
public sanitizePath(rawPath: string): string {
return rawPath
.split("/")
.map((p) => p.replace(/[^a-z0-9._-]/gi, "_"))
.join("/");
}
public async downloadFile(url: string, assetsDir: string): Promise<string | null> {
if (url.startsWith("//")) url = `https:${url}`;
if (!url.startsWith("http")) return null;
try {
const u = new URL(url);
const relPath = this.sanitizePath(u.hostname + u.pathname);
const dest = path.join(assetsDir, relPath);
if (fs.existsSync(dest)) return `./assets/${relPath}`;
const res = await axios.get(url, {
responseType: "arraybuffer",
headers: { "User-Agent": this.userAgent },
timeout: 15000,
validateStatus: () => true,
});
if (res.status !== 200) return null;
if (!fs.existsSync(path.dirname(dest)))
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.writeFileSync(dest, Buffer.from(res.data));
return `./assets/${relPath}`;
} catch {
return null;
}
}
public async processCssRecursively(
cssContent: string,
cssUrl: string,
assetsDir: string,
urlMap: AssetMap,
depth = 0,
): Promise<string> {
if (depth > 5) return cssContent;
const urlRegex = /(?:url\(["']?|@import\s+["'])([^"'\)]+)["']?\)?/gi;
let match;
let newContent = cssContent;
while ((match = urlRegex.exec(cssContent)) !== null) {
const originalUrl = match[1];
if (originalUrl.startsWith("data:") || originalUrl.startsWith("blob:"))
continue;
try {
const absUrl = new URL(originalUrl, cssUrl).href;
const local = await this.downloadFile(absUrl, assetsDir);
if (local) {
const u = new URL(cssUrl);
const cssPath = u.hostname + u.pathname;
const assetPath = new URL(absUrl).hostname + new URL(absUrl).pathname;
const rel = path.relative(
path.dirname(this.sanitizePath(cssPath)),
this.sanitizePath(assetPath),
);
newContent = newContent.split(originalUrl).join(rel);
urlMap[absUrl] = local;
}
} catch {
// Ignore
}
}
return newContent;
}
}

View File

@@ -0,0 +1,184 @@
import { chromium, Browser, BrowserContext, Page } from "playwright";
import fs from "node:fs";
import path from "node:path";
import axios from "axios";
import { AssetManager, AssetMap } from "./AssetManager.js";
export interface PageClonerOptions {
outputDir: string;
userAgent?: string;
}
export class PageCloner {
private options: PageClonerOptions;
private assetManager: AssetManager;
private userAgent: string;
constructor(options: PageClonerOptions) {
this.options = options;
this.userAgent = options.userAgent || "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36";
this.assetManager = new AssetManager(this.userAgent);
}
public async clone(targetUrl: string): Promise<string> {
const urlObj = new URL(targetUrl);
const domainSlug = urlObj.hostname.replace("www.", "");
const domainDir = path.resolve(this.options.outputDir, domainSlug);
const assetsDir = path.join(domainDir, "assets");
if (!fs.existsSync(assetsDir)) fs.mkdirSync(assetsDir, { recursive: true });
let pageSlug = urlObj.pathname.split("/").filter(Boolean).join("-");
if (!pageSlug) pageSlug = "index";
const htmlFilename = `${pageSlug}.html`;
console.log(`🚀 INDUSTRIAL CLONE: ${targetUrl}`);
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
userAgent: this.userAgent,
viewport: { width: 1920, height: 1080 },
});
const page = await context.newPage();
const urlMap: AssetMap = {};
const foundAssets = new Set<string>();
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);
let 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 { }
}
}
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 { }
}
}
}
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(/<script\b[^>]*>([\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("</head>");
if (headEnd > -1) {
const stabilityCss = `\n<style>* { transition: none !important; animation: none !important; scroll-behavior: auto !important; } [data-aos], .reveal, .lazypath, .lazy-load, [data-src] { opacity: 1 !important; visibility: visible !important; transform: none !important; clip-path: none !important; } img, video, iframe { max-width: 100%; display: block; } a { pointer-events: none; cursor: default; } </style>`;
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();
}
}
}

View File

@@ -0,0 +1,123 @@
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;
}
export class WebsiteCloner {
private options: WebsiteClonerOptions;
constructor(options: WebsiteClonerOptions) {
this.options = {
maxRequestsPerCrawl: 100,
maxConcurrency: 3,
...options
};
}
public async clone(targetUrl: string, outputDirName?: string): Promise<string> {
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 });
console.log(`🚀 Starting perfect recursive clone of ${targetUrl}...`);
console.log(`📂 Output: ${baseOutputDir}`);
const requestQueue = await RequestQueue.open();
await requestQueue.addRequest({ url: targetUrl });
const crawler = new PlaywrightCrawler({
requestQueue,
maxRequestsPerCrawl: this.options.maxRequestsPerCrawl,
maxConcurrency: this.options.maxConcurrency,
async requestHandler({ request, enqueueLinks, log }) {
const url = request.url;
log.info(`Capturing ${url}...`);
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;
}
});
},
});
await crawler.run();
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);
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);
}
}
return fileList;
}
}

View File

@@ -0,0 +1,3 @@
export * from "./AssetManager.js";
export * from "./PageCloner.js";
export * from "./WebsiteCloner.js";

View File

@@ -0,0 +1,17 @@
{
"extends": "../tsconfig/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"emitDeclarationOnly": true,
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"noEmit": false
},
"include": [
"src/**/*"
]
}

View File

BIN
packages/cms-infra/database/data.db Normal file → Executable file

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,30 @@
{
"name": "acquisition-manager",
"description": "Custom High-Fidelity Acquisition Management for Directus",
"icon": "account_balance_wallet",
"version": "1.7.12",
"type": "module",
"keywords": [
"directus",
"directus-extension",
"directus-extension-module"
],
"files": [
"dist"
],
"directus:extension": {
"type": "module",
"path": "index.js",
"source": "src/index.ts",
"host": "*",
"name": "Acquisition Manager"
},
"scripts": {
"build": "directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)",
"dev": "directus-extension build -w"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"vue": "^3.4.0"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,29 +1,30 @@
{ {
"name": "customer-manager", "name": "customer-manager",
"description": "Custom High-Fidelity Customer & Company Management for Directus", "description": "Custom High-Fidelity Customer & Company Management for Directus",
"icon": "supervisor_account", "icon": "supervisor_account",
"version": "1.0.0", "version": "1.7.12",
"keywords": [ "type": "module",
"directus", "keywords": [
"directus-extension", "directus",
"directus-extension-module" "directus-extension",
], "directus-extension-module"
"files": [ ],
"dist" "files": [
], "dist"
"directus:extension": { ],
"type": "module", "directus:extension": {
"path": "index.js", "type": "module",
"source": "src/index.ts", "path": "index.js",
"host": "*", "source": "src/index.ts",
"name": "Customer Manager" "host": "*",
}, "name": "Customer Manager"
"scripts": { },
"build": "directus-extension build", "scripts": {
"dev": "directus-extension build -w" "build": "directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)",
}, "dev": "directus-extension build -w"
"devDependencies": { },
"@directus/extensions-sdk": "11.0.2", "devDependencies": {
"vue": "^3.4.0" "@directus/extensions-sdk": "11.0.2",
} "vue": "^3.4.0"
} }
}

View File

@@ -1,29 +1,30 @@
{ {
"name": "feedback-commander", "name": "feedback-commander",
"description": "Custom High-Fidelity Feedback Management Extension for Directus", "description": "Custom High-Fidelity Feedback Management Extension for Directus",
"icon": "view_kanban", "icon": "view_kanban",
"version": "1.0.0", "version": "1.7.12",
"keywords": [ "type": "module",
"directus", "keywords": [
"directus-extension", "directus",
"directus-extension-module" "directus-extension",
], "directus-extension-module"
"files": [ ],
"index.js" "files": [
], "dist"
"directus:extension": { ],
"type": "module", "directus:extension": {
"path": "index.js", "type": "module",
"source": "src/index.ts", "path": "index.js",
"host": "*", "source": "src/index.ts",
"name": "Feedback Commander" "host": "*",
}, "name": "Feedback Commander"
"scripts": { },
"build": "directus-extension build", "scripts": {
"dev": "directus-extension build -w" "build": "directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)",
}, "dev": "directus-extension build -w"
"devDependencies": { },
"@directus/extensions-sdk": "11.0.2", "devDependencies": {
"vue": "^3.4.0" "@directus/extensions-sdk": "11.0.2",
} "vue": "^3.4.0"
} }
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,30 @@
{
"name": "people-manager",
"description": "Custom High-Fidelity People Management for Directus",
"icon": "person",
"version": "1.7.12",
"type": "module",
"keywords": [
"directus",
"directus-extension",
"directus-extension-module"
],
"files": [
"dist"
],
"directus:extension": {
"type": "module",
"path": "index.js",
"source": "src/index.ts",
"host": "*",
"name": "People Manager"
},
"scripts": {
"build": "directus-extension build && (cp -f dist/index.js index.js 2>/dev/null || true)",
"dev": "directus-extension build -w"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"vue": "^3.4.0"
}
}

View File

@@ -1,11 +1,17 @@
{ {
"name": "@mintel/cms-infra", "name": "@mintel/cms-infra",
"version": "1.0.0", "version": "1.8.2",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"up": "docker compose up -d", "up": "npm run build:extensions && docker compose up -d",
"down": "docker compose down", "down": "docker compose down",
"logs": "docker compose logs -f" "logs": "docker compose logs -f",
} "build:extensions": "../../scripts/sync-extensions.sh",
} "schema:apply:local": "../../scripts/cms-apply.sh local",
"schema:apply:infra": "../../scripts/cms-apply.sh infra",
"snapshot:local": "../../scripts/cms-snapshot.sh",
"sync:push": "../../scripts/sync-directus.sh push infra",
"sync:pull": "../../scripts/sync-directus.sh pull infra"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
{
"name": "company-manager",
"description": "Central Company Management for Directus",
"icon": "business",
"version": "1.8.2",
"type": "module",
"keywords": [
"directus",
"directus-extension",
"directus-extension-module"
],
"directus:extension": {
"type": "module",
"path": "index.js",
"source": "src/index.ts",
"host": "*",
"name": "Company Manager"
},
"scripts": {
"build": "directus-extension build && cp -f dist/index.js index.js 2>/dev/null || true",
"dev": "directus-extension build -w"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"vue": "^3.4.0"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,29 +1,27 @@
{ {
"name": "customer-manager", "name": "customer-manager",
"description": "Custom High-Fidelity Customer & Company Management for Directus", "description": "Custom High-Fidelity Customer & Company Management for Directus",
"icon": "supervisor_account", "icon": "supervisor_account",
"version": "1.0.0", "version": "1.8.2",
"keywords": [ "type": "module",
"directus", "keywords": [
"directus-extension", "directus",
"directus-extension-module" "directus-extension",
], "directus-extension-module"
"files": [ ],
"dist" "directus:extension": {
], "type": "module",
"directus:extension": { "path": "index.js",
"type": "module", "source": "src/index.ts",
"path": "index.js", "host": "*",
"source": "src/index.ts", "name": "Customer Manager"
"host": "*", },
"name": "Customer Manager" "scripts": {
}, "build": "directus-extension build && cp -f dist/index.js index.js 2>/dev/null || true",
"scripts": { "dev": "directus-extension build -w"
"build": "directus-extension build", },
"dev": "directus-extension build -w" "devDependencies": {
}, "@directus/extensions-sdk": "11.0.2",
"devDependencies": { "vue": "^3.4.0"
"@directus/extensions-sdk": "11.0.2", }
"vue": "^3.4.0" }
}
}

View File

@@ -127,6 +127,15 @@
<v-input v-model="employeeForm.email" placeholder="E-Mail Adresse" type="email" /> <v-input v-model="employeeForm.email" placeholder="E-Mail Adresse" type="email" />
</div> </div>
<div class="field">
<span class="label">Zentrale Person (Verknüpfung)</span>
<v-select
v-model="employeeForm.person"
:items="peopleOptions"
placeholder="Person aus dem People Manager auswählen..."
/>
</div>
<v-divider v-if="isEditingEmployee" /> <v-divider v-if="isEditingEmployee" />
<div v-if="isEditingEmployee" class="field"> <div v-if="isEditingEmployee" class="field">
@@ -158,7 +167,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'; import { ref, onMounted, nextTick, computed } from 'vue';
import { useApi } from '@directus/extensions-sdk'; import { useApi } from '@directus/extensions-sdk';
const api = useApi(); const api = useApi();
@@ -183,6 +192,7 @@ const employeeForm = ref({
first_name: '', first_name: '',
last_name: '', last_name: '',
email: '', email: '',
person: null,
temporary_password: '' temporary_password: ''
}); });
@@ -192,14 +202,22 @@ const tableHeaders = [
{ text: 'Zuletzt eingeladen', value: 'last_invited', sortable: true } { text: 'Zuletzt eingeladen', value: 'last_invited', sortable: true }
]; ];
async function fetchCompanies() { const people = ref<any[]>([]);
const res = await api.get('/items/companies', {
params: { const peopleOptions = computed(() =>
fields: ['id', 'name'], people.value.map(p => ({
sort: 'name', text: `${p.first_name} ${p.last_name} (${p.email})`,
}, value: p.id
}); }))
companies.value = res.data.data; );
async function fetchData() {
const [companiesResp, peopleResp] = await Promise.all([
api.get('/items/companies', { params: { sort: 'name', fields: ['id', 'name'] } }),
api.get('/items/people', { params: { sort: 'last_name' } })
]);
companies.value = companiesResp.data.data;
people.value = peopleResp.data.data;
} }
async function selectCompany(company: any) { async function selectCompany(company: any) {
@@ -209,7 +227,7 @@ async function selectCompany(company: any) {
const res = await api.get('/items/client_users', { const res = await api.get('/items/client_users', {
params: { params: {
filter: { company: { _eq: company.id } }, filter: { company: { _eq: company.id } },
fields: ['*'], fields: ['*', 'person.*'],
sort: 'first_name', sort: 'first_name',
}, },
}); });
@@ -273,6 +291,7 @@ async function openEditEmployee(item: any) {
first_name: item.first_name || '', first_name: item.first_name || '',
last_name: item.last_name || '', last_name: item.last_name || '',
email: item.email || '', email: item.email || '',
person: item.person?.id || item.person || null,
temporary_password: item.temporary_password || '' temporary_password: item.temporary_password || ''
}; };
isEditingEmployee.value = true; isEditingEmployee.value = true;
@@ -288,7 +307,8 @@ async function saveEmployee() {
await api.patch(`/items/client_users/${employeeForm.value.id}`, { await api.patch(`/items/client_users/${employeeForm.value.id}`, {
first_name: employeeForm.value.first_name, first_name: employeeForm.value.first_name,
last_name: employeeForm.value.last_name, last_name: employeeForm.value.last_name,
email: employeeForm.value.email email: employeeForm.value.email,
person: employeeForm.value.person
}); });
notice.value = { type: 'success', message: 'Mitarbeiter aktualisiert!' }; notice.value = { type: 'success', message: 'Mitarbeiter aktualisiert!' };
} else { } else {
@@ -296,7 +316,8 @@ async function saveEmployee() {
first_name: employeeForm.value.first_name, first_name: employeeForm.value.first_name,
last_name: employeeForm.value.last_name, last_name: employeeForm.value.last_name,
email: employeeForm.value.email, email: employeeForm.value.email,
company: selectedCompany.value.id company: selectedCompany.value.id,
person: employeeForm.value.person
}); });
notice.value = { type: 'success', message: 'Mitarbeiter angelegt!' }; notice.value = { type: 'success', message: 'Mitarbeiter angelegt!' };
} }
@@ -343,7 +364,7 @@ function formatDate(dateStr: string) {
} }
onMounted(() => { onMounted(() => {
fetchCompanies(); fetchData();
}); });
</script> </script>

View File

@@ -0,0 +1,31 @@
{
"name": "@mintel/directus-extension-toolkit",
"version": "1.8.2",
"description": "Shared toolkit for Directus extensions in the Mintel ecosystem",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/index.js"
}
},
"scripts": {
"build": "vite build",
"dev": "vite build --watch"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"@vitejs/plugin-vue": "^6.0.4",
"typescript": "^5.0.0",
"vite": "^5.0.0",
"vue": "^3.4.0"
},
"peerDependencies": {
"@directus/extensions-sdk": "*",
"vue": "^3.4.0"
}
}

View File

@@ -0,0 +1,102 @@
<template>
<private-view :title="title">
<template #navigation>
<slot name="navigation" />
</template>
<template #title-outer:after>
<v-notice v-if="notice" :type="notice.type" @close="$emit('close-notice')" dismissible>
{{ notice.message }}
</v-notice>
</template>
<div class="mintel-manager-layout">
<div v-if="isEmpty" class="empty-state">
<v-info :title="emptyTitle" :icon="emptyIcon" center>
<slot name="empty-state" />
</v-info>
</div>
<template v-else>
<header class="mintel-header">
<div class="header-left">
<h1 class="mintel-title">{{ itemTitle }}</h1>
<p class="mintel-subtitle">
<slot name="subtitle" />
</p>
</div>
<div class="header-right">
<slot name="actions" />
</div>
</header>
<v-divider />
<div class="mintel-content">
<slot />
</div>
</template>
</div>
</private-view>
</template>
<script setup lang="ts">
defineProps<{
title: string;
itemTitle?: string;
isEmpty?: boolean;
emptyTitle?: string;
emptyIcon?: string;
notice?: { type: string; message: string } | null;
}>();
defineEmits(['close-notice']);
</script>
<style scoped>
.mintel-manager-layout {
padding: 32px;
height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.mintel-header {
margin-bottom: 24px;
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.mintel-title {
font-size: 24px;
font-weight: 800;
margin-bottom: 4px;
color: var(--theme--foreground);
}
.mintel-subtitle {
color: var(--theme--foreground-subdued);
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.header-right {
display: flex;
gap: 12px;
}
.empty-state {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.mintel-content {
margin-top: 32px;
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<div class="mintel-select">
<v-select
:model-value="modelValue"
:items="items"
:placeholder="placeholder"
:searchable="searchable"
:show-deselect="showDeselect"
@update:model-value="$emit('update:modelValue', $event)"
/>
<v-button v-if="allowAdd" secondary rounded icon x-small class="add-button" @click="$emit('add')">
<v-icon name="add" />
</v-button>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
modelValue: {
type: [String, Number],
default: null
},
items: {
type: Array as () => Array<{ text: string; value: string | number }>,
required: true
},
placeholder: {
type: String,
default: 'Auswählen...'
},
searchable: {
type: Boolean,
default: true
},
showDeselect: {
type: Boolean,
default: true
},
allowAdd: {
type: Boolean,
default: false
}
});
defineEmits(['update:modelValue', 'add']);
</script>
<style scoped>
.mintel-select {
display: flex;
align-items: center;
gap: 8px;
}
.mintel-select :deep(.v-select) {
flex: 1;
}
.add-button {
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<div class="mintel-stat-card" @click="$emit('click')">
<div class="stat-icon">
<v-icon :name="icon" large />
</div>
<div class="stat-content">
<span class="stat-label">{{ label }}</span>
<span class="stat-value">{{ value }}</span>
</div>
<v-icon name="chevron_right" class="arrow" />
</div>
</template>
<script setup lang="ts">
defineProps<{
label: string;
value: string | number;
icon: string;
}>();
defineEmits(['click']);
</script>
<style scoped>
.mintel-stat-card {
background: var(--theme--background-normal);
border: 1px solid var(--theme--border);
padding: 24px;
border-radius: 12px;
display: flex;
align-items: center;
gap: 20px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.mintel-stat-card:hover {
border-color: var(--theme--primary);
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
}
.stat-icon {
width: 56px;
height: 56px;
background: var(--theme--background-subdued);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: var(--theme--primary);
}
.stat-content {
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
color: var(--theme--foreground-subdued);
letter-spacing: 0.5px;
}
.stat-value {
font-size: 28px;
font-weight: 800;
color: var(--theme--foreground);
}
.arrow {
position: absolute;
right: 24px;
opacity: 0.2;
}
.mintel-stat-card:hover .arrow {
opacity: 1;
color: var(--theme--primary);
}
</style>

View File

@@ -0,0 +1,3 @@
export { default as MintelSelect } from './MintelSelect.vue';
export { default as MintelManagerLayout } from './MintelManagerLayout.vue';
export { default as MintelStatCard } from './MintelStatCard.vue';

View File

@@ -0,0 +1,24 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: resolve('src/index.ts'),
name: 'MintelDirectusToolkit',
fileName: 'index',
formats: ['es']
},
rollupOptions: {
external: ['vue', '@directus/extensions-sdk'],
output: {
globals: {
vue: 'Vue',
'@directus/extensions-sdk': 'DirectusExtensionsSDK'
}
}
}
}
});

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/eslint-config", "name": "@mintel/eslint-config",
"version": "1.0.1", "version": "1.8.2",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://npm.infra.mintel.me" "registry": "https://npm.infra.mintel.me"
@@ -20,8 +20,8 @@
"dependencies": { "dependencies": {
"@eslint/eslintrc": "^3.0.0", "@eslint/eslintrc": "^3.0.0",
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@next/eslint-plugin-next": "15.1.6", "@next/eslint-plugin-next": "16.1.6",
"eslint-config-next": "15.1.6", "eslint-config-next": "16.1.6",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"typescript-eslint": "^8.54.0" "typescript-eslint": "^8.54.0"

File diff suppressed because one or more lines are too long

View File

@@ -1,29 +1,27 @@
{ {
"name": "@mintel/extension-feedback-commander", "name": "feedback-commander",
"description": "Custom High-Fidelity Feedback Management Extension for Directus", "description": "Custom High-Fidelity Feedback Management Extension for Directus",
"icon": "view_kanban", "icon": "view_kanban",
"version": "1.0.0", "version": "1.8.2",
"keywords": [ "type": "module",
"directus", "keywords": [
"directus-extension", "directus",
"directus-extension-module" "directus-extension",
], "directus-extension-module"
"files": [ ],
"dist" "directus:extension": {
], "type": "module",
"directus:extension": { "path": "index.js",
"type": "module", "source": "src/index.ts",
"path": "dist/index.js", "host": "*",
"source": "src/index.ts", "name": "Feedback Commander"
"host": "*", },
"name": "Feedback Commander" "scripts": {
}, "build": "directus-extension build && cp -f dist/index.js index.js 2>/dev/null || true",
"scripts": { "dev": "directus-extension build -w"
"build": "directus-extension build", },
"dev": "directus-extension build -w" "devDependencies": {
}, "@directus/extensions-sdk": "11.0.2",
"devDependencies": { "vue": "^3.4.0"
"@directus/extensions-sdk": "11.0.2", }
"vue": "^3.4.0" }
}
}

View File

@@ -78,7 +78,7 @@
<div class="card-text">{{ item.text }}</div> <div class="card-text">{{ item.text }}</div>
<footer class="card-footer"> <footer class="card-footer">
<div class="meta-tags"> <div class="meta-tags">
<v-chip x-small outline>{{ item.project }}</v-chip> <v-chip x-small outline>{{ item.company?.name || item.project }}</v-chip>
<v-icon :name="item.type === 'bug' ? 'bug_report' : 'lightbulb'" :color="item.type === 'bug' ? '#E91E63' : '#FFC107'" small /> <v-icon :name="item.type === 'bug' ? 'bug_report' : 'lightbulb'" :color="item.type === 'bug' ? '#E91E63' : '#FFC107'" small />
</div> </div>
<v-icon v-if="selectedItem?.id === item.id" name="chevron_right" small /> <v-icon v-if="selectedItem?.id === item.id" name="chevron_right" small />
@@ -142,7 +142,8 @@
<TransitionGroup name="thread-list"> <TransitionGroup name="thread-list">
<div v-for="reply in comments" :key="reply.id" class="reply-bubble"> <div v-for="reply in comments" :key="reply.id" class="reply-bubble">
<header class="reply-header"> <header class="reply-header">
<span class="reply-user">{{ reply.user_name }}</span> <span class="reply-user">{{ reply.user_name || 'System' }}</span>
<span v-if="reply.person" class="reply-person">({{ reply.person.first_name }} {{ reply.person.last_name }})</span>
<span class="reply-date">{{ formatDate(reply.date_created || reply.id) }}</span> <span class="reply-date">{{ formatDate(reply.date_created || reply.id) }}</span>
</header> </header>
<div class="reply-text">{{ reply.text }}</div> <div class="reply-text">{{ reply.text }}</div>
@@ -168,8 +169,12 @@
<v-card-title>Context</v-card-title> <v-card-title>Context</v-card-title>
<v-card-text class="meta-list"> <v-card-text class="meta-list">
<div class="meta-item"> <div class="meta-item">
<label><v-icon name="public" x-small /> Website</label> <label><v-icon name="business" x-small /> Organisation / Firma</label>
<strong>{{ selectedItem.project }}</strong> <strong>{{ selectedItem.company?.name || selectedItem.project }}</strong>
</div>
<div v-if="selectedItem.person" class="meta-item">
<label><v-icon name="person" x-small /> Zentrale Person</label>
<strong>{{ selectedItem.person.first_name }} {{ selectedItem.person.last_name }}</strong>
</div> </div>
<div class="meta-item"> <div class="meta-item">
<label><v-icon name="link" x-small /> Source Path</label> <label><v-icon name="link" x-small /> Source Path</label>
@@ -238,13 +243,14 @@ const statusOptions = [
]; ];
const projects = computed(() => { const projects = computed(() => {
const projSet = new Set(items.value.map(i => i.project).filter(Boolean)); const projSet = new Set(items.value.map(i => i.company?.name || i.project).filter(Boolean));
return Array.from(projSet).sort(); return Array.from(projSet).sort();
}); });
const filteredItems = computed(() => { const filteredItems = computed(() => {
return items.value.filter(item => { return items.value.filter(item => {
const matchProject = currentProject.value === 'all' || item.project === currentProject.value; const projectName = item.company?.name || item.project;
const matchProject = currentProject.value === 'all' || projectName === currentProject.value;
const status = item.status || 'open'; const status = item.status || 'open';
const matchStatus = currentStatusFilter.value === 'all' || status === currentStatusFilter.value; const matchStatus = currentStatusFilter.value === 'all' || status === currentStatusFilter.value;
return matchProject && matchStatus; return matchProject && matchStatus;
@@ -258,7 +264,8 @@ async function fetchData() {
const response = await api.get('/items/visual_feedback', { const response = await api.get('/items/visual_feedback', {
params: { params: {
sort: '-date_created,-id', sort: '-date_created,-id',
limit: 300 limit: 300,
fields: ['*', 'company.*', 'person.*']
} }
}); });
items.value = response.data.data; items.value = response.data.data;
@@ -278,7 +285,8 @@ async function selectItem(item) {
const response = await api.get('/items/visual_feedback_comments', { const response = await api.get('/items/visual_feedback_comments', {
params: { params: {
filter: { feedback_id: { _eq: item.id } }, filter: { feedback_id: { _eq: item.id } },
sort: '-date_created,-id' sort: '-date_created,-id',
fields: ['*', 'person.*']
} }
}); });
comments.value = response.data.data; comments.value = response.data.data;

View File

@@ -1,20 +1,20 @@
{ {
"name": "@mintel/gatekeeper", "name": "@mintel/gatekeeper",
"version": "1.0.0", "version": "1.8.2",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "eslint src/",
"test": "vitest run" "test": "vitest run"
}, },
"dependencies": { "dependencies": {
"@mintel/next-utils": "workspace:*", "@mintel/next-utils": "workspace:*",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.474.0", "lucide-react": "^0.474.0",
"next": "15.1.7", "next": "16.1.6",
"next-intl": "^4.8.2", "next-intl": "^4.8.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",

View File

@@ -11,6 +11,8 @@ export async function GET(req: NextRequest) {
// 1. URL Parameter Bypass (for automated tests/staging) // 1. URL Parameter Bypass (for automated tests/staging)
const originalUrl = req.headers.get("x-forwarded-uri") || "/"; const originalUrl = req.headers.get("x-forwarded-uri") || "/";
console.log(`[Verify] Check: ${originalUrl} | Cookie: ${session ? "Found" : "Missing"}`);
const host = const host =
req.headers.get("x-forwarded-host") || req.headers.get("host") || ""; req.headers.get("x-forwarded-host") || req.headers.get("host") || "";
const proto = req.headers.get("x-forwarded-proto") || "https"; const proto = req.headers.get("x-forwarded-proto") || "https";
@@ -44,7 +46,7 @@ export async function GET(req: NextRequest) {
return response; return response;
} }
} catch (e) { } catch (_e) {
// URL parsing failed, proceed with normal logic // URL parsing failed, proceed with normal logic
} }
@@ -54,15 +56,17 @@ export async function GET(req: NextRequest) {
if (session?.value) { if (session?.value) {
if (session.value === password) { if (session.value === password) {
isAuthenticated = true; isAuthenticated = true;
console.log(`[Verify] Legacy password match`);
} else { } else {
try { try {
const payload = JSON.parse(session.value); const payload = JSON.parse(session.value);
if (payload.identity) { if (payload.identity) {
isAuthenticated = true; isAuthenticated = true;
identity = payload.identity; identity = payload.identity;
console.log(`[Verify] Identity verified: ${identity}`);
} }
} catch (e) { } catch (_e) {
// Fallback or old format console.log(`[Verify] JSON Parse failed for cookie: ${session.value.substring(0, 10)}...`);
} }
} }
} }

View File

@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
export async function GET(req: NextRequest) { export async function GET(_req: NextRequest) {
const cookieStore = await cookies(); const cookieStore = await cookies();
const authCookieName = const authCookieName =
process.env.AUTH_COOKIE_NAME || "mintel_gatekeeper_session"; process.env.AUTH_COOKIE_NAME || "mintel_gatekeeper_session";
@@ -17,7 +17,7 @@ export async function GET(req: NextRequest) {
const payload = JSON.parse(session.value); const payload = JSON.parse(session.value);
identity = payload.identity || "Guest"; identity = payload.identity || "Guest";
company = payload.company || null; company = payload.company || null;
} catch (e) { } catch (_e) {
// Old format probably just the password // Old format probably just the password
} }

View File

@@ -17,8 +17,8 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
async function login(formData: FormData) { async function login(formData: FormData) {
"use server"; "use server";
const email = formData.get("email") as string; const email = (formData.get("email") as string || "").trim();
const password = formData.get("password") as string; const password = (formData.get("password") as string || "").trim();
const expectedCode = process.env.GATEKEEPER_PASSWORD || "mintel"; const expectedCode = process.env.GATEKEEPER_PASSWORD || "mintel";
const adminEmail = process.env.DIRECTUS_ADMIN_EMAIL; const adminEmail = process.env.DIRECTUS_ADMIN_EMAIL;
@@ -31,19 +31,19 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
let userIdentity = ""; let userIdentity = "";
let userCompany: any = null; let userCompany: any = null;
// 1. Check Global Admin (from ENV) // 1. Check Generic Code (Guest) - High Priority to prevent autofill traps
if ( if (password === expectedCode) {
userIdentity = "Guest";
}
// 2. Check Global Admin (from ENV)
else if (
adminEmail && adminEmail &&
adminPassword && adminPassword &&
email === adminEmail && email === adminEmail.trim() &&
password === adminPassword password === adminPassword.trim()
) { ) {
userIdentity = "Admin"; userIdentity = "Admin";
} }
// 2. Check Generic Code (Guest)
else if (!email && password === expectedCode) {
userIdentity = "Guest";
}
// 3. Check Lightweight Client Users (dedicated collection) // 3. Check Lightweight Client Users (dedicated collection)
if (email && password && process.env.INFRA_DIRECTUS_URL) { if (email && password && process.env.INFRA_DIRECTUS_URL) {
try { try {
@@ -116,6 +116,7 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
} }
if (userIdentity) { if (userIdentity) {
console.log(`[Login] Success: ${userIdentity} | Redirect: ${targetRedirect}`);
const cookieStore = await cookies(); const cookieStore = await cookies();
// Store identity in the cookie (simplified for now, ideally signed) // Store identity in the cookie (simplified for now, ideally signed)
const sessionValue = JSON.stringify({ const sessionValue = JSON.stringify({
@@ -126,6 +127,8 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
const isDev = process.env.NODE_ENV === "development"; const isDev = process.env.NODE_ENV === "development";
console.log(`[Login] Setting Cookie: ${authCookieName} | Domain: ${cookieDomain || "Default"}`);
cookieStore.set(authCookieName, sessionValue, { cookieStore.set(authCookieName, sessionValue, {
httpOnly: true, httpOnly: true,
secure: !isDev, secure: !isDev,
@@ -136,6 +139,7 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
}); });
redirect(targetRedirect); redirect(targetRedirect);
} else { } else {
console.log(`[Login] Failed for inputs. Redirecting back with error.`);
redirect(`/login?error=1&redirect=${encodeURIComponent(targetRedirect)}`); redirect(`/login?error=1&redirect=${encodeURIComponent(targetRedirect)}`);
} }
} }

View File

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

View File

@@ -19,7 +19,6 @@ COPY packages/cli/package.json ./packages/cli/package.json
COPY packages/observability/package.json ./packages/observability/package.json COPY packages/observability/package.json ./packages/observability/package.json
COPY packages/next-observability/package.json ./packages/next-observability/package.json COPY packages/next-observability/package.json ./packages/next-observability/package.json
COPY packages/husky-config/package.json ./packages/husky-config/package.json COPY packages/husky-config/package.json ./packages/husky-config/package.json
COPY packages/ui/package.json ./packages/ui/package.json
# Use a secret for NPM_TOKEN and a cache mount for the pnpm store # Use a secret for NPM_TOKEN and a cache mount for the pnpm store
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \

View File

@@ -1,19 +1,13 @@
# Step 1: Builder image # Step 1: Base image for Next.js builds
FROM node:20-alpine AS builder FROM node:20-alpine
RUN apk add --no-cache libc6-compat curl RUN apk add --no-cache libc6-compat curl
# Enable pnpm
RUN corepack enable pnpm && \
corepack prepare pnpm@10.2.0 --activate
WORKDIR /app WORKDIR /app
RUN corepack enable pnpm
# Step 2: Install dependencies # Final environment
# We copy everything first because we have a .dockerignore ENV NODE_ENV=production
# and we need the workspace structure for pnpm to work correctly ENV NEXT_TELEMETRY_DISABLED=1
COPY . .
# Use a secret for NPM_TOKEN to authenticate with private registry
RUN --mount=type=cache,target=/root/.local/share/pnpm/store/v3 \
--mount=type=secret,id=NPM_TOKEN \
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) && \
pnpm i --frozen-lockfile
# Step 3: Build shared packages
RUN pnpm --filter "./packages/*" -r build

View File

@@ -1,19 +1,22 @@
FROM node:20-alpine FROM node:20-alpine AS runner
RUN apk add --no-cache libc6-compat curl
# Install essential production utilities WORKDIR /app
RUN apk add --no-cache curl libc6-compat
# Set standard production environment
ENV NODE_ENV=production ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000 ENV PORT=3000
ENV HOSTNAME="0.0.0.0" ENV HOSTNAME="0.0.0.0"
WORKDIR /app
# Create non-root user for security # Create non-root user for security
RUN addgroup --system --gid 1001 nodejs && \ RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs adduser --system --uid 1001 nextjs
# Expose the default Next.js port # Set correct permissions
RUN chown -R nextjs:nodejs /app
USER nextjs
EXPOSE 3000 EXPOSE 3000
CMD ["node", "server.js"]

View File

@@ -53,7 +53,7 @@ services:
- "traefik.http.services.${PROJECT_NAME}-gatekeeper.loadbalancer.server.port=3000" - "traefik.http.services.${PROJECT_NAME}-gatekeeper.loadbalancer.server.port=3000"
directus: directus:
image: registry.infra.mintel.me/mintel/directus:latest image: registry.infra.mintel.me/mintel/directus:${IMAGE_TAG:-latest}
restart: always restart: always
networks: networks:
- infra - infra

View File

@@ -275,6 +275,10 @@ jobs:
docker system prune -f --filter "until=24h" docker system prune -f --filter "until=24h"
EOF EOF
- name: 🧹 Post-Deploy Cleanup (Runner)
if: always()
run: docker builder prune -f --filter "until=1h"
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# JOB 5: Notifications # JOB 5: Notifications
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────

View File

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

View File

@@ -2,7 +2,7 @@
set -e set -e
# Configuration # Configuration
REGISTRY_DATA="/opt/infra/registry/data/docker/registry/v2" REGISTRY_DATA="/mnt/HC_Volume_104575103/registry-data/docker/registry/v2"
KEEP_TAGS=3 KEEP_TAGS=3
echo "🏥 Starting Aggressive Registry & Docker Maintenance..." echo "🏥 Starting Aggressive Registry & Docker Maintenance..."
@@ -15,31 +15,26 @@ for repo_dir in "$REGISTRY_DATA/repositories/mintel/"*; do
if [ -d "$tags_dir" ]; then if [ -d "$tags_dir" ]; then
echo "🔍 Processing repository: mintel/$repo_name" echo "🔍 Processing repository: mintel/$repo_name"
# Prune main-* tags # Prune various tag patterns
echo " 📦 Pruning main tags..." PATTERNS=("main-*" "testing-*" "branch-*" "v*" "rc*" "[0-9a-f]*")
main_tags=$(ls -dt "$tags_dir"/main-* 2>/dev/null || true)
count=0 for pattern in "${PATTERNS[@]}"; do
for tag_path in $main_tags; do echo " 📦 Pruning $pattern tags..."
((++count)) tags=$(ls -dt "$tags_dir"/${pattern} 2>/dev/null || true)
if [ $count -gt $KEEP_TAGS ]; then count=0
echo " 🗑️ Deleting old main tag: $(basename "$tag_path")" for tag_path in $tags; do
rm -rf "$tag_path" tag_name=$(basename "$tag_path")
fi if [[ "$tag_name" == "latest" ]]; then continue; fi
((++count))
if [ $count -gt $KEEP_TAGS ]; then
echo " 🗑️ Deleting old tag: $tag_name"
rm -rf "$tag_path"
fi
done
done done
# Prune version tags (v* and rc*) # Always prune buildcache
echo " 🏷️ Pruning version tags..."
version_tags=$(ls -dt "$tags_dir"/v1* 2>/dev/null || true)
count=0
for tag_path in $version_tags; do
((++count))
if [ $count -gt $KEEP_TAGS ]; then
echo " 🗑️ Deleting old version tag: $(basename "$tag_path")"
rm -rf "$tag_path"
fi
done
# Always prune buildcache (as it rebuilds quickly)
if [ -d "$tags_dir/buildcache" ]; then if [ -d "$tags_dir/buildcache" ]; then
echo " 🧹 Deleting buildcache tag" echo " 🧹 Deleting buildcache tag"
rm -rf "$tags_dir/buildcache" rm -rf "$tags_dir/buildcache"
@@ -48,8 +43,15 @@ for repo_dir in "$REGISTRY_DATA/repositories/mintel/"*; do
done done
# 2. Run Garbage Collection # 2. Run Garbage Collection
echo "♻️ Running Registry Garbage Collection..." echo "♻️ Detecting Registry Container..."
docker exec registry-registry-1 bin/registry garbage-collect /etc/docker/registry/config.yml REGISTRY_CONTAINER=$(docker ps --format "{{.Names}}" | grep registry | head -1 || true)
if [ -n "$REGISTRY_CONTAINER" ]; then
echo "♻️ Running Registry Garbage Collection on $REGISTRY_CONTAINER..."
docker exec "$REGISTRY_CONTAINER" bin/registry garbage-collect /etc/docker/registry/config.yml --delete-untagged
else
echo "⚠️ Registry container not found. Skipping GC."
fi
# 3. Prune Host Docker resources (Shorter window: 24h) # 3. Prune Host Docker resources (Shorter window: 24h)
echo "🧹 Pruning Host Docker resources..." echo "🧹 Pruning Host Docker resources..."

View File

@@ -0,0 +1,93 @@
#!/bin/bash
set -e
# wait-for-upstream.sh
# Usage: ./wait-for-upstream.sh <org/repo> <version_tag> [poll_interval_sec]
REPO=$1
TAG=$2
INTERVAL=${3:-30}
MAX_RETRIES=40 # ~20 minutes default
if [[ -z "$REPO" || -z "$TAG" ]]; then
echo "❌ Error: REPO and TAG are required."
echo "Usage: $0 <org/repo> <version_tag>"
exit 1
fi
if [[ -z "$GITEA_TOKEN" ]]; then
echo "❌ Error: GITEA_TOKEN is not set."
exit 1
fi
GITEA_API="https://git.infra.mintel.me/api/v1"
echo "🔎 Searching for upstream release $TAG in $REPO..."
# 1. Get the SHA of the tag to be more precise
TAG_INFO=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$GITEA_API/repos/$REPO/tags/$TAG")
TARGET_SHA=$(echo "$TAG_INFO" | jq -r '.commit.sha // empty')
if [[ -z "$TARGET_SHA" || "$TARGET_SHA" == "null" ]]; then
echo "⚠️ Warning: Tag $TAG not found yet. Upstream might be lagging."
echo " Waiting 15s for tag to appear..."
sleep 15
TAG_INFO=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$GITEA_API/repos/$REPO/tags/$TAG")
TARGET_SHA=$(echo "$TAG_INFO" | jq -r '.commit.sha // empty')
if [[ -z "$TARGET_SHA" || "$TARGET_SHA" == "null" ]]; then
echo "❌ Error: Tag $TAG does not exist in $REPO."
exit 1
fi
fi
echo "✅ Target SHA for $TAG is $TARGET_SHA"
# 2. Find the run for the specific SHA
# We list recent runs and filter by head_sha
RUN_QUERY=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$GITEA_API/repos/$REPO/actions/runs?limit=30")
RUN_ID=$(echo "$RUN_QUERY" | jq -r ".workflow_runs[] | select(.head_sha == \"$TARGET_SHA\") | .id" | head -n 1)
if [[ -z "$RUN_ID" || "$RUN_ID" == "null" ]]; then
echo " No recent action run found for SHA $TARGET_SHA yet."
echo " Checking if we should wait or if it was already successful..."
# Fallback: wait a bit more for new tags
echo "⏳ waiting for run to appear..."
sleep 20
RUN_QUERY=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$GITEA_API/repos/$REPO/actions/runs?limit=30")
RUN_ID=$(echo "$RUN_QUERY" | jq -r ".workflow_runs[] | select(.head_sha == \"$TARGET_SHA\") | .id" | head -n 1)
if [[ -z "$RUN_ID" || "$RUN_ID" == "null" ]]; then
echo "✅ No run found but Tag exists. Assuming manual release or already completed. Proceeding."
exit 0
fi
fi
echo "⏳ Waiting for upstream run $RUN_ID status..."
RETRY_COUNT=0
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
STATUS_QUERY=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$GITEA_API/repos/$REPO/actions/runs/$RUN_ID")
STATUS=$(echo "$STATUS_QUERY" | jq -r '.status')
CONCLUSION=$(echo "$STATUS_QUERY" | jq -r '.conclusion')
echo " - Current Status: $STATUS (Conclusion: $CONCLUSION)"
if [[ "$STATUS" == "success" || "$CONCLUSION" == "success" ]]; then
echo "✅ Upstream release $TAG is READY."
exit 0
fi
if [[ "$STATUS" == "failure" || "$CONCLUSION" == "failure" || "$CONCLUSION" == "cancelled" ]]; then
echo "❌ Error: Upstream release $TAG FAILED or was CANCELLED."
exit 1
fi
echo " - Still working... waiting $INTERVAL seconds (Attempt $((RETRY_COUNT+1))/$MAX_RETRIES)"
sleep $INTERVAL
RETRY_COUNT=$((RETRY_COUNT+1))
done
echo "❌ Error: Timeout waiting for upstream release $TAG."
exit 1

View File

@@ -0,0 +1,7 @@
# @mintel/mail
## 1.7.0
### Minor Changes
- 96ec2c7: Initial release of the branded email system package.

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/mail", "name": "@mintel/mail",
"version": "1.2.0", "version": "1.8.2",
"private": false, "private": false,
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
@@ -38,6 +38,7 @@
"@mintel/tsconfig": "workspace:*", "@mintel/tsconfig": "workspace:*",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0", "@types/react-dom": "^19.0.0",
"prettier": "^3.8.1",
"tsup": "^8.3.5", "tsup": "^8.3.5",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"vitest": "^3.0.4" "vitest": "^3.0.4"

View File

@@ -22,3 +22,6 @@ export * from "./layouts/ClientLayout";
// Export Templates // Export Templates
export * from "./templates/ContactFormNotification"; export * from "./templates/ContactFormNotification";
export * from "./templates/ConfirmationMessage"; export * from "./templates/ConfirmationMessage";
export * from "./templates/FollowUpTemplate";
export * from "./templates/ProjectEstimateTemplate";
export * from "./templates/SiteAuditTemplate";

View File

@@ -0,0 +1,88 @@
import * as React from "react";
import { Heading, Text, Button } from "@react-email/components";
import { MintelLayout } from "../layouts/MintelLayout";
export interface FollowUpTemplateProps {
companyName: string;
}
export const FollowUpTemplate = ({
companyName,
}: FollowUpTemplateProps) => {
const preview = `Kurzes Follow-up: ${companyName}`;
return (
<MintelLayout preview={preview}>
<Heading style={h1}>Kurzes Follow-up</Heading>
<Text style={intro}>
Hallo noch einmal,<br /><br />
ich wollte mich nur kurz erkundigen, ob Sie bereits Zeit hatten,
einen Blick auf das Audit Ihrer Website zu werfen, das ich Ihnen
vor ein paar Tagen gesendet habe.
</Text>
<Text style={bodyText}>
Vielleicht passt es ja diese Woche für ein kurzes, unverbindliches
Telefonat, um die Punkte gemeinsam durchzugehen?
</Text>
<Button
style={button}
href="https://calendly.com/mintel-me/intro"
>
Treffen vereinbaren
</Button>
<Text style={footerText}>
Beste Grüße,<br />
<strong>Marc Mintel</strong>
</Text>
</MintelLayout>
);
};
export default FollowUpTemplate;
const h1 = {
fontSize: "28px",
fontWeight: "900",
margin: "0 0 24px",
color: "#ffffff",
letterSpacing: "-0.04em",
};
const intro = {
fontSize: "16px",
lineHeight: "24px",
color: "#cccccc",
margin: "0 0 24px",
};
const bodyText = {
fontSize: "16px",
lineHeight: "24px",
color: "#888888",
margin: "0 0 32px",
};
const button = {
backgroundColor: "#333333",
borderRadius: "0",
color: "#ffffff",
fontSize: "13px",
fontWeight: "bold",
textDecoration: "none",
textAlign: "center" as const,
display: "inline-block",
padding: "12px 24px",
textTransform: "uppercase" as const,
letterSpacing: "0.1em",
border: "1px solid #444444",
};
const footerText = {
fontSize: "14px",
color: "#666666",
lineHeight: "20px",
marginTop: "48px",
};

View File

@@ -0,0 +1,86 @@
import * as React from "react";
import { Heading, Text, Section } from "@react-email/components";
import { MintelLayout } from "../layouts/MintelLayout";
export interface ProjectEstimateTemplateProps {
companyName: string;
}
export const ProjectEstimateTemplate = ({
companyName,
}: ProjectEstimateTemplateProps) => {
const preview = `Ihre personalisierte Projekt-Schätzung: ${companyName}`;
return (
<MintelLayout preview={preview}>
<Heading style={h1}>Ihre Projekt-Schätzung</Heading>
<Text style={intro}>
Hallo {companyName},<br /><br />
vielen Dank für unser Gespräch. Wie versprochen sende ich Ihnen hiermit
die detaillierte Schätzung für Ihre neue digitale Webpräsenz.
</Text>
<Section style={infoBox}>
<Text style={infoText}>
Im Anhang finden Sie das PDF-Dokument mit allen Positionen,
Umfängen und dem strategischen Ausblick.
</Text>
</Section>
<Text style={bodyText}>
Ich freue mich auf Ihr Feedback und stehe für Rückfragen jederzeit
zur Verfügung.
</Text>
<Text style={footerText}>
Beste Grüße,<br />
<strong>Marc Mintel</strong>
</Text>
</MintelLayout>
);
};
export default ProjectEstimateTemplate;
const h1 = {
fontSize: "28px",
fontWeight: "900",
margin: "0 0 24px",
color: "#ffffff",
letterSpacing: "-0.04em",
};
const intro = {
fontSize: "16px",
lineHeight: "24px",
color: "#cccccc",
margin: "0 0 24px",
};
const infoBox = {
backgroundColor: "#0f172a",
padding: "24px",
borderLeft: "4px solid #ffffff",
marginBottom: "32px",
};
const infoText = {
fontSize: "15px",
color: "#ffffff",
margin: "0",
lineHeight: "1.5",
};
const bodyText = {
fontSize: "16px",
lineHeight: "24px",
color: "#888888",
margin: "0 0 32px",
};
const footerText = {
fontSize: "14px",
color: "#666666",
lineHeight: "20px",
marginTop: "48px",
};

View File

@@ -0,0 +1,146 @@
import * as React from "react";
import { Heading, Section, Text, Button, Link } from "@react-email/components";
import { MintelLayout } from "../layouts/MintelLayout";
export interface SiteAuditTemplateProps {
companyName: string;
auditHighlights: string[];
websiteUrl: string;
}
export const SiteAuditTemplate = ({
companyName,
auditHighlights,
websiteUrl,
}: SiteAuditTemplateProps) => {
const preview = `Analyse Ihrer Webpräsenz: ${companyName}`;
return (
<MintelLayout preview={preview}>
<Heading style={h1}>Analyse Ihrer Webpräsenz</Heading>
<Text style={intro}>
Hallo {companyName},<br /><br />
ich habe mir Ihre aktuelle Website ({websiteUrl}) im Detail angeschaut und
einige technische sowie strategische Potenziale identifiziert.
</Text>
<Section style={auditContainer}>
<Heading as="h2" style={h2}>Audit Highlights</Heading>
{auditHighlights.map((highlight, i) => (
<div key={i} style={highlightRow}>
<div style={bullet} />
<Text style={highlightText}>{highlight}</Text>
</div>
))}
</Section>
<Text style={bodyText}>
In der heutigen digitalen Landschaft ist eine performante und strategisch
ausgerichtete Website kein Luxus mehr, sondern das Fundament für
nachhaltiges Wachstum.
</Text>
<Section style={ctaSection}>
<Button
style={button}
href={`mailto:marc@mintel.me?subject=Feedback zum Audit: ${companyName}`}
>
Audit gemeinsam besprechen
</Button>
</Section>
<Text style={footerText}>
Beste Grüße,<br />
<strong>Marc Mintel</strong><br />
Digitaler Architekt
</Text>
</MintelLayout>
);
};
export default SiteAuditTemplate;
const h1 = {
fontSize: "28px",
fontWeight: "900",
margin: "0 0 24px",
color: "#ffffff",
letterSpacing: "-0.04em",
};
const h2 = {
fontSize: "14px",
fontWeight: "900",
textTransform: "uppercase" as const,
color: "#444444",
margin: "0 0 16px",
letterSpacing: "0.1em",
};
const intro = {
fontSize: "16px",
lineHeight: "24px",
color: "#cccccc",
margin: "0 0 32px",
};
const auditContainer = {
backgroundColor: "#151515",
padding: "32px",
borderRadius: "8px",
marginBottom: "32px",
border: "1px solid #222222",
};
const highlightRow = {
display: "flex" as const,
alignItems: "flex-start" as const,
marginBottom: "12px",
};
const bullet = {
width: "6px",
height: "6px",
backgroundColor: "#4CAF50",
marginTop: "8px",
marginRight: "12px",
flexShrink: 0,
};
const highlightText = {
fontSize: "15px",
color: "#ffffff",
margin: "0",
};
const bodyText = {
fontSize: "16px",
lineHeight: "24px",
color: "#888888",
margin: "0 0 32px",
};
const ctaSection = {
textAlign: "center" as const,
marginBottom: "48px",
};
const button = {
backgroundColor: "#ffffff",
borderRadius: "0",
color: "#000000",
fontSize: "14px",
fontWeight: "bold",
textDecoration: "none",
textAlign: "center" as const,
display: "inline-block",
padding: "16px 32px",
textTransform: "uppercase" as const,
letterSpacing: "0.1em",
};
const footerText = {
fontSize: "14px",
color: "#666666",
lineHeight: "20px",
};

View File

@@ -0,0 +1,23 @@
import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
test: {
environment: "node",
alias: {
"prettier/plugins/html": path.resolve(
process.cwd(),
"../../node_modules/prettier/plugins/html.js",
),
"prettier/parser-html": path.resolve(
process.cwd(),
"../../node_modules/prettier/plugins/html.js",
),
},
server: {
deps: {
inline: [/@react-email/],
},
},
},
});

View File

@@ -1,5 +1,17 @@
# @mintel/next-config # @mintel/next-config
## 1.6.1
### Patch Changes
- Add `turbopack: {}` to support Next.js 16 default Turbopack behavior when a webpack config is present.
## 1.6.1
### Patch Changes
- Add `turbopack: {}` to support Next.js 16 default Turbopack behavior when a webpack config is present.
## 1.0.1 ## 1.0.1
### Patch Changes ### Patch Changes

View File

@@ -7,6 +7,7 @@ import path from "node:path";
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
export const baseNextConfig = { export const baseNextConfig = {
output: "standalone", output: "standalone",
turbopack: {},
images: { images: {
dangerouslyAllowSVG: true, dangerouslyAllowSVG: true,
contentDispositionType: "attachment", contentDispositionType: "attachment",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/next-config", "name": "@mintel/next-config",
"version": "1.0.1", "version": "1.8.2",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://npm.infra.mintel.me" "registry": "https://npm.infra.mintel.me"
@@ -16,6 +16,7 @@
}, },
"dependencies": { "dependencies": {
"next-intl": "^4.8.2", "next-intl": "^4.8.2",
"@sentry/nextjs": "^8.0.0" "@sentry/nextjs": "^10.38.0",
"next": "16.1.6"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/next-feedback", "name": "@mintel/next-feedback",
"version": "1.0.0", "version": "1.8.2",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://npm.infra.mintel.me" "registry": "https://npm.infra.mintel.me"
@@ -23,16 +23,16 @@
"scripts": { "scripts": {
"build": "tsup", "build": "tsup",
"dev": "tsup --watch", "dev": "tsup --watch",
"lint": "eslint src/", "lint": "eslint src/"
"test": "vitest run"
}, },
"dependencies": { "dependencies": {
"@directus/sdk": "^21.0.0", "@directus/sdk": "^21.0.0",
"@medv/finder": "^4.0.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^11.5.4", "framer-motion": "^11.5.4",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"lucide-react": "^0.441.0", "lucide-react": "^0.441.0",
"next": "15.1.7", "next": "16.1.6",
"tailwind-merge": "^2.5.2" "tailwind-merge": "^2.5.2"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -6,6 +6,7 @@ import { MessageSquare, X, Check, Plus, List, Send, User } from "lucide-react";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import html2canvas from "html2canvas"; import html2canvas from "html2canvas";
import { finder } from "@medv/finder";
function cn(...inputs: any[]) { function cn(...inputs: any[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@@ -107,15 +108,16 @@ export function FeedbackOverlay() {
}, []); }, []);
const getSelector = (el: HTMLElement): string => { const getSelector = (el: HTMLElement): string => {
if (el.id) return `#${el.id}`; return finder(el, {
const path = []; root: document.body,
let curr: HTMLElement | null = el; className: (name) =>
while (curr && curr.parentElement) { !name.startsWith('record-mode-') &&
const index = Array.from(curr.parentElement.children).indexOf(curr) + 1; !name.startsWith('feedback-') &&
path.unshift(`${curr.tagName.toLowerCase()}:nth-child(${index})`); !name.includes('[') &&
curr = curr.parentElement; !name.includes('/') &&
} !name.match(/^[a-z]-[0-9]/),
return path.join(" > "); idName: (name) => !name.startsWith('__next') && !name.includes(':'),
});
}; };
useEffect(() => { useEffect(() => {

View File

@@ -6,7 +6,4 @@ export default defineConfig({
dts: true, dts: true,
clean: true, clean: true,
sourcemap: true, sourcemap: true,
banner: {
js: "'use client';",
},
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/next-observability", "name": "@mintel/next-observability",
"version": "1.0.0", "version": "1.8.2",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://npm.infra.mintel.me" "registry": "https://npm.infra.mintel.me"
@@ -28,8 +28,8 @@
}, },
"dependencies": { "dependencies": {
"@mintel/observability": "workspace:*", "@mintel/observability": "workspace:*",
"@sentry/nextjs": "^8.55.0", "@sentry/nextjs": "^10.38.0",
"next": "15.1.7" "next": "16.1.6"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0",

View File

@@ -1,22 +1,22 @@
{ {
"name": "@mintel/next-utils", "name": "@mintel/next-utils",
"version": "1.0.1", "version": "1.8.2",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://npm.infra.mintel.me" "registry": "https://npm.infra.mintel.me"
}, },
"type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"scripts": { "scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts", "build": "tsup src/index.ts --format esm --dts --clean",
"dev": "tsup src/index.ts --format cjs,esm --watch --dts", "dev": "tsup src/index.ts --format esm --watch --dts",
"lint": "eslint src/", "lint": "eslint src/",
"test": "vitest run" "test": "vitest run"
}, },
"dependencies": { "dependencies": {
"@directus/sdk": "^21.0.0", "@directus/sdk": "^21.0.0",
"next": "15.1.7", "next": "16.1.6",
"next-intl": "^4.8.2", "next-intl": "^4.8.2",
"zod": "^3.0.0" "zod": "^3.0.0"
}, },

View File

@@ -7,18 +7,43 @@ import {
AuthenticationClient, AuthenticationClient,
} from "@directus/sdk"; } from "@directus/sdk";
export type MintelDirectusClient = DirectusClient<any> & export type MintelDirectusClient<Schema extends object = any> =
RestClient<any> & DirectusClient<Schema> & RestClient<Schema> & AuthenticationClient<Schema>;
AuthenticationClient<any>;
/** /**
* Creates a Directus client configured with Mintel standards * Creates a Directus client configured with Mintel standards.
* Automatically handles internal vs. external URLs based on environment.
*/ */
export function createMintelDirectusClient(url?: string): MintelDirectusClient { export function createMintelDirectusClient<Schema extends object = any>(
const directusUrl = url?: string,
url || process.env.DIRECTUS_URL || "http://localhost:8055"; ): MintelDirectusClient<Schema> {
const isServer = typeof window === "undefined";
return createDirectus(directusUrl).with(rest()).with(authentication()); // 1. If an explicit URL is provided, use it.
if (url) {
return createDirectus<Schema>(url).with(rest()).with(authentication());
}
// 2. On server: Prioritize INTERNAL_DIRECTUS_URL, fallback to DIRECTUS_URL
if (isServer) {
const directusUrl =
process.env.INTERNAL_DIRECTUS_URL ||
process.env.DIRECTUS_URL ||
"http://localhost:8055";
return createDirectus<Schema>(directusUrl)
.with(rest())
.with(authentication());
}
// 3. In browser: Use a proxy path if we are on a different origin,
// or use the current origin if no DIRECTUS_URL is set.
const proxyPath = "/api/directus"; // Standard Mintel proxy path
const browserUrl =
typeof window !== "undefined"
? `${window.location.origin}${proxyPath}`
: proxyPath;
return createDirectus<Schema>(browserUrl).with(rest()).with(authentication());
} }
/** /**

View File

@@ -4,10 +4,17 @@ export const mintelEnvSchema = {
NODE_ENV: z NODE_ENV: z
.enum(["development", "production", "test"]) .enum(["development", "production", "test"])
.default("development"), .default("development"),
NEXT_PUBLIC_BASE_URL: z.string().url(), NEXT_PUBLIC_BASE_URL: z.string().url().optional(),
NEXT_PUBLIC_TARGET: z
.enum(["development", "testing", "staging", "production"])
.optional(),
TARGET: z
.enum(["development", "testing", "staging", "production"])
.optional(),
// Analytics (Proxy Pattern) // Analytics (Proxy Pattern)
UMAMI_WEBSITE_ID: z.string().optional(), UMAMI_WEBSITE_ID: z.string().optional(),
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string().optional(),
UMAMI_API_ENDPOINT: z UMAMI_API_ENDPOINT: z
.string() .string()
.url() .url()
@@ -23,6 +30,8 @@ export const mintelEnvSchema = {
LOG_LEVEL: z LOG_LEVEL: z
.enum(["trace", "debug", "info", "warn", "error", "fatal"]) .enum(["trace", "debug", "info", "warn", "error", "fatal"])
.default("info"), .default("info"),
// Mail
MAIL_HOST: z.string().optional(), MAIL_HOST: z.string().optional(),
MAIL_PORT: z.coerce.number().default(587), MAIL_PORT: z.coerce.number().default(587),
MAIL_USERNAME: z.string().optional(), MAIL_USERNAME: z.string().optional(),
@@ -32,17 +41,60 @@ export const mintelEnvSchema = {
(val) => (typeof val === "string" ? val.split(",").filter(Boolean) : val), (val) => (typeof val === "string" ? val.split(",").filter(Boolean) : val),
z.array(z.string()).default([]), z.array(z.string()).default([]),
), ),
// Directus
DIRECTUS_URL: z.string().url().default("http://localhost:8055"),
DIRECTUS_ADMIN_EMAIL: z.string().optional(),
DIRECTUS_ADMIN_PASSWORD: z.string().optional(),
DIRECTUS_API_TOKEN: z.string().optional(),
INTERNAL_DIRECTUS_URL: z.string().url().optional(),
}; };
export function validateMintelEnv(schemaExtension = {}) { /**
const fullSchema = z.object({ * Standard Mintel refinements for environment variables.
...mintelEnvSchema, * Enforces mandatory requirements for non-development environments.
...schemaExtension, */
export const withMintelRefinements = <T extends z.ZodTypeAny>(schema: T) => {
return schema.superRefine((data: any, ctx) => {
const skipValidation =
process.env.SKIP_ENV_VALIDATION === "true" ||
process.env.SKIP_RUNTIME_ENV_VALIDATION === "true";
if (skipValidation) return;
const target = data.TARGET || data.NEXT_PUBLIC_TARGET || "development";
// Strict validation for non-development environments
if (target !== "development") {
if (!data.MAIL_HOST) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "MAIL_HOST is required in non-development environments",
path: ["MAIL_HOST"],
});
}
}
}); });
};
export type MintelEnv<T extends z.ZodRawShape = Record<string, never>> =
z.infer<
ReturnType<
typeof withMintelRefinements<z.ZodObject<typeof mintelEnvSchema & T>>
>
>;
export function validateMintelEnv<
T extends z.ZodRawShape = Record<string, never>,
>(schemaExtension: T = {} as T): MintelEnv<T> {
const fullSchema = withMintelRefinements(
z.object(mintelEnvSchema).extend(schemaExtension),
);
const isBuildTime = const isBuildTime =
process.env.NEXT_PHASE === "phase-production-build" || process.env.NEXT_PHASE === "phase-production-build" ||
process.env.SKIP_ENV_VALIDATION === "true"; process.env.SKIP_ENV_VALIDATION === "true" ||
process.env.SKIP_RUNTIME_ENV_VALIDATION === "true";
const result = fullSchema.safeParse(process.env); const result = fullSchema.safeParse(process.env);
@@ -51,7 +103,7 @@ export function validateMintelEnv(schemaExtension = {}) {
console.warn( console.warn(
"⚠️ Some environment variables are missing during build, but skipping strict validation.", "⚠️ Some environment variables are missing during build, but skipping strict validation.",
); );
// Return partial data to allow build to continue // Return process.env casted to the full schema type to unblock builds
return process.env as unknown as z.infer<typeof fullSchema>; return process.env as unknown as z.infer<typeof fullSchema>;
} }
@@ -62,5 +114,5 @@ export function validateMintelEnv(schemaExtension = {}) {
throw new Error("Invalid environment variables"); throw new Error("Invalid environment variables");
} }
return result.data; return result.data as MintelEnv<T>;
} }

View File

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

View File

@@ -0,0 +1,57 @@
import { build } from 'esbuild';
import { resolve, dirname } from 'path';
import { mkdirSync } from 'fs';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const entryPoints = [
resolve(__dirname, 'src/index.ts'),
resolve(__dirname, 'src/server.ts')
];
try {
mkdirSync(resolve(__dirname, 'dist'), { recursive: true });
} catch (e) { }
console.log(`Building entry points...`);
build({
entryPoints: entryPoints,
bundle: true,
platform: 'node',
target: 'node18',
outdir: resolve(__dirname, 'dist'),
format: 'esm',
jsx: 'automatic',
loader: {
'.tsx': 'tsx',
'.ts': 'ts',
'.js': 'js',
},
external: ["@react-pdf/renderer", "react", "react-dom", "jsdom", "jsdom/*", "jquery", "jquery/*", "canvas", "fs", "path", "os", "http", "https", "zlib", "stream", "util", "url", "net", "tls", "crypto"],
plugins: [{
name: 'mock-canvas',
setup(build) {
build.onResolve({ filter: /^canvas/ }, args => ({ path: args.path, namespace: 'mock-canvas' }));
build.onLoad({ filter: /.*/, namespace: 'mock-canvas' }, () => ({ contents: 'export default {};', loader: 'js' }));
}
}, {
name: 'mock-jsdom',
setup(build) {
build.onResolve({ filter: /^jsdom/ }, args => ({ path: args.path, namespace: 'mock-jsdom' }));
build.onLoad({ filter: /.*/, namespace: 'mock-jsdom' }, () => ({ contents: 'export default {};', loader: 'js' }));
}
}]
}).then(() => {
console.log("Build succeeded!");
}).catch((e) => {
if (e.errors) {
console.error("Build failed with errors:");
e.errors.forEach(err => console.error(` ${err.text} at ${err.location?.file}:${err.location?.line}`));
} else {
console.error("Build failed:", e);
}
process.exit(1);
});

View File

@@ -0,0 +1,38 @@
{
"name": "@mintel/pdf",
"version": "1.8.2",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./server": {
"types": "./dist/server.d.ts",
"import": "./dist/server.js",
"default": "./dist/server.js"
}
},
"scripts": {
"build": "node build.mjs",
"dev": "node build.mjs --watch"
},
"devDependencies": {
"@directus/extensions-sdk": "11.0.2",
"esbuild": "^0.25.0",
"typescript": "^5.6.3"
},
"dependencies": {
"@crawlee/cheerio": "^3.16.0",
"@mintel/mail": "workspace:*",
"@react-pdf/renderer": "^4.3.0",
"axios": "^1.7.9",
"cheerio": "^1.0.0",
"react": "^19.2.4",
"react-dom": "^19.2.4"
}
}

View File

@@ -0,0 +1,241 @@
"use client";
import * as React from "react";
import {
Page as PDFPage,
Text as PDFText,
View as PDFView,
StyleSheet as PDFStyleSheet,
} from "@react-pdf/renderer";
import {
pdfStyles,
Header,
Footer,
FoldingMarks,
DocumentTitle,
} from "./pdf/SharedUI.js";
import { SimpleLayout } from "./pdf/SimpleLayout.js";
const localStyles = PDFStyleSheet.create({
sectionContainer: {
marginTop: 0,
},
agbSection: {
marginBottom: 20,
},
labelRow: {
flexDirection: "row",
alignItems: "baseline",
marginBottom: 6,
},
monoNumber: {
fontSize: 7,
fontWeight: "bold",
color: "#94a3b8",
letterSpacing: 2,
width: 25,
},
sectionTitle: {
fontSize: 9,
fontWeight: "bold",
color: "#000000",
textTransform: "uppercase",
letterSpacing: 0.5,
},
officialText: {
fontSize: 8,
lineHeight: 1.5,
color: "#334155",
textAlign: "justify",
paddingLeft: 25,
},
});
const AGBSection = ({
index,
title,
children,
}: {
index: string;
title: string;
children: React.ReactNode;
}) => (
<PDFView style={localStyles.agbSection} wrap={false}>
<PDFView style={localStyles.labelRow}>
<PDFText style={localStyles.monoNumber}>{index}</PDFText>
<PDFText style={localStyles.sectionTitle}>{title}</PDFText>
</PDFView>
<PDFText style={localStyles.officialText}>{children}</PDFText>
</PDFView>
);
interface AgbsPDFProps {
headerIcon?: string;
footerLogo?: string;
mode?: "estimation" | "full";
}
export const AgbsPDF = ({
headerIcon,
footerLogo,
mode = "full",
}: AgbsPDFProps) => {
const date = new Date().toLocaleDateString("de-DE", {
year: "numeric",
month: "long",
day: "numeric",
});
const companyData = {
name: "Marc Mintel",
address1: "Georg-Meistermann-Straße 7",
address2: "54586 Schüller",
ustId: "DE367588065",
};
const bankData = {
name: "N26",
bic: "NTSBDEB1XXX",
iban: "DE50 1001 1001 2620 4328 65",
};
const content = (
<>
<DocumentTitle
title="Allgemeine Geschäftsbedingungen"
subLines={[`Stand: ${date}`]}
/>
<PDFView style={localStyles.sectionContainer}>
<AGBSection index="01" title="Geltungsbereich">
Diese Allgemeinen Geschäftsbedingungen gelten für alle Verträge
zwischen Marc Mintel (nachfolgend Auftragnehmer) und dem jeweiligen
Kunden (nachfolgend Auftraggeber). Abweichende oder ergänzende
Bedingungen des Auftraggebers werden nicht Vertragsbestandteil, auch
wenn ihrer Geltung nicht ausdrücklich widersprochen wird.
</AGBSection>
<AGBSection index="02" title="Vertragsgegenstand">
Der Auftragnehmer erbringt Dienstleistungen im Bereich:
Webentwicklung, technische Umsetzung digitaler Systeme, Funktionen,
Schnittstellen und Automatisierungen sowie Hosting, Betrieb und
Wartung, sofern ausdrücklich vereinbard. Der Auftragnehmer schuldet
ausschließlich die vereinbarte technische Leistung, nicht jedoch einen
wirtschaftlichen Erfolg, bestimmte Umsätze, Conversions, Reichweiten,
Suchmaschinen-Rankings oder rechtliche Ergebnisse.
</AGBSection>
<AGBSection index="03" title="Mitwirkungspflichten des Auftraggebers">
Der Auftraggeber verpflichtet sich, alle zur Leistungserbringung
erforderlichen Inhalte, Informationen, Zugänge und Entscheidungen
rechtzeitig, vollständig und korrekt bereitzustellen. Hierzu zählen
insbesondere Texte, Bilder, Videos, Produktdaten, Freigaben, Feedback,
Zugangsdaten sowie rechtlich erforderliche Inhalte (z. B. Impressum,
DSGVO). Verzögerungen oder Unterlassungen führen zu Verschiebungen
aller Termine ohne Schadensersatzanspruch.
</AGBSection>
<AGBSection index="04" title="Ausführungs- und Bearbeitungszeiten">
Angegebene Bearbeitungszeiten sind unverbindliche Schätzungen, keine
garantierten Fristen. Fixe Termine oder Deadlines gelten nur, wenn sie
ausdrücklich schriftlich als verbindlich vereinbart wurden.
</AGBSection>
<AGBSection index="05" title="Abnahme">
Die Leistung gilt als abgenommen, wenn der Auftraggeber sie produktiv
nutzt oder innerhalb von 7 Tagen nach Bereitstellung keine
wesentlichen Mängel angezeigt werden. Optische Abweichungen,
Geschmacksfragen oder subjektive Einschätzungen stellen keine Mängel
dar.
</AGBSection>
<AGBSection index="06" title="Haftung">
Der Auftragnehmer haftet nur für Schäden, die auf vorsätzlicher oder
grob fahrlässiger Pflichtverletzung beruhen. Eine Haftung für
entgangenen Gewinn, Umsatzausfälle, Datenverlust,
Betriebsunterbrechungen, mittelbare oder Folgeschäden ist
ausgeschlossen, soweit gesetzlich zulässig.
</AGBSection>
<AGBSection index="07" title="Verfügbarkeit & Betrieb">
Bei vereinbartem Hosting oder Betrieb schuldet der Auftragnehmer keine
permanente Verfügbarkeit. Wartungsarbeiten, Updates,
Sicherheitsmaßnahmen oder externe Störungen können zu zeitweisen
Einschränkungen führen und begründen keine Haftungsansprüche.
</AGBSection>
<AGBSection index="07a" title="Betriebs- und Pflegeleistung">
Die Betriebs- und Pflegeleistung umfasst ausschließlich die
Sicherstellung des technischen Betriebs, Wartung, Updates,
Fehlerbehebung der bestehenden Systeme sowie Pflege bestehender
Datensätze ohne Strukturänderung. Nicht Bestandteil sind die
Erstellung neuer Inhalte (Blogartikel, News, Produkte), redaktionelle
Tätigkeiten, strategische Planung oder der Aufbau neuer
Features/Datenmodelle. Leistungen darüber hinaus gelten als
Neuentwicklung.
</AGBSection>
<AGBSection index="08" title="Drittanbieter & externe Systeme">
Der Auftragnehmer übernimmt keine Verantwortung für Leistungen,
Ausfälle oder Änderungen externer Dienste, APIs, Schnittstellen oder
Plattformen Dritter. Eine Funktionsfähigkeit kann nur im Rahmen der
jeweils aktuellen externen Schnittstellen gewährleistet werden.
</AGBSection>
<AGBSection index="09" title="Inhalte & Rechtliches">
Der Auftraggeber ist allein verantwortlich für Inhalte, rechtliche
Konformität (DSGVO, Urheberrecht etc.) sowie bereitgestellte Daten.
Der Auftragnehmer übernimmt keine rechtliche Prüfung.
</AGBSection>
<AGBSection index="10" title="Vergütung & Zahlungsverzug">
Alle Preise netto zzgl. MwSt. Rechnungen sind innerhalb von 7 Tagen
fällig. Bei Zahlungsverzug ist der Auftragnehmer berechtigt,
Leistungen auszusetzen, Systeme offline zu nehmen oder laufende
Arbeiten zu stoppen.
</AGBSection>
<AGBSection index="11" title="Kündigung laufender Leistungen">
Laufende Leistungen (z. B. Hosting & Betrieb) können mit einer Frist
von 4 Wochen zum Monatsende gekündigt werden, sofern nichts anderes
vereinbart ist.
</AGBSection>
<AGBSection index="12" title="Schlussbestimmungen">
Es gilt das Recht der Bundesrepublik Deutschland. Gerichtsstand ist
der Sitz des Auftragnehmers. Sollte eine Bestimmung unwirksam sein,
bleibt die Wirksamkeit der übrigen Regelungen unberührt.
</AGBSection>
</PDFView>
</>
);
if (mode === "full") {
return (
<SimpleLayout
companyData={companyData}
bankData={bankData}
footerLogo={footerLogo}
icon={headerIcon}
pageNumber="10"
showPageNumber={false}
>
{content}
</SimpleLayout>
);
}
return (
<PDFPage size="A4" style={pdfStyles.page}>
<FoldingMarks />
<Header icon={headerIcon} showAddress={false} />
{content}
<Footer
logo={footerLogo}
companyData={companyData}
bankData={bankData}
showDetails={false}
showPageNumber={false}
/>
</PDFPage>
);
};

View File

@@ -0,0 +1,79 @@
"use client";
import * as React from "react";
import { Document as PDFDocument } from "@react-pdf/renderer";
import { EstimationPDF } from "./EstimationPDF.js";
import { AgbsPDF } from "./AgbsPDF.js";
import { SimpleLayout } from "./pdf/SimpleLayout.js";
import { ClosingModule } from "./pdf/modules/CommonModules.js";
interface CombinedProps {
estimationProps: any;
showAgbs?: boolean;
techDetails?: any[];
principles?: any[];
maintenanceDetails?: any[];
standardsDetails?: any[];
}
export const CombinedQuotePDF = ({
estimationProps,
showAgbs = true,
techDetails,
principles,
maintenanceDetails,
standardsDetails,
mode = "full",
}: CombinedProps & { mode?: "estimation" | "full" }) => {
const date = new Date().toLocaleDateString("de-DE", {
year: "numeric",
month: "long",
day: "numeric",
});
const companyData = {
name: "Marc Mintel",
address1: "Georg-Meistermann-Straße 7",
address2: "54586 Schüller",
ustId: "DE367588065",
};
const bankData = {
name: "N26",
bic: "NTSBDEB1XXX",
iban: "DE50 1001 1001 2620 4328 65",
};
const layoutProps = {
date,
icon: estimationProps.headerIcon,
footerLogo: estimationProps.footerLogo,
companyData,
bankData,
};
return (
<PDFDocument
title={`Mintel - ${estimationProps.state.companyName || estimationProps.state.name}`}
>
<EstimationPDF
{...estimationProps}
mode={mode}
techDetails={techDetails}
principles={principles}
maintenanceDetails={maintenanceDetails}
standardsDetails={standardsDetails}
/>
{showAgbs && (
<AgbsPDF
mode={mode}
headerIcon={estimationProps.headerIcon}
footerLogo={estimationProps.footerLogo}
/>
)}
<SimpleLayout {...layoutProps} pageNumber="END" showPageNumber={false}>
<ClosingModule />
</SimpleLayout>
</PDFDocument>
);
};

View File

@@ -0,0 +1,95 @@
"use client";
import * as React from "react";
import { Page as PDFPage, Document as PDFDocument } from "@react-pdf/renderer";
import { pdfStyles } from "./pdf/SharedUI.js";
import { SimpleLayout } from "./pdf/SimpleLayout.js";
// Modules
import { FrontPageModule } from "./pdf/modules/FrontPageModule.js";
import { BriefingModule } from "./pdf/modules/BriefingModule.js";
import { SitemapModule } from "./pdf/modules/SitemapModule.js";
import { EstimationModule } from "./pdf/modules/EstimationModule.js";
import { TransparenzModule } from "./pdf/modules/TransparenzModule.js";
import { ClosingModule } from "./pdf/modules/CommonModules.js";
import { calculatePositions } from "../logic/pricing/calculator.js";
interface PDFProps {
state: any;
totalPrice: number;
monthlyPrice?: number;
totalPagesCount?: number;
pricing: any;
headerIcon?: string;
footerLogo?: string;
}
export const EstimationPDF = ({
state,
totalPrice,
pricing,
headerIcon,
footerLogo,
}: PDFProps) => {
const date = new Date().toLocaleDateString("de-DE", {
year: "numeric",
month: "long",
day: "numeric",
});
const positions = calculatePositions(state, pricing);
const companyData = {
name: "Marc Mintel",
address1: "Georg-Meistermann-Straße 7",
address2: "54586 Schüller",
ustId: "DE367588065",
};
const commonProps = {
state,
date,
icon: headerIcon,
footerLogo,
companyData,
};
let pageCounter = 1;
const getPageNum = () => (pageCounter++).toString().padStart(2, "0");
return (
<PDFDocument title={`Angebot - ${state.companyName || "Projekt"}`}>
<PDFPage size="A4" style={pdfStyles.titlePage}>
<FrontPageModule state={state} headerIcon={headerIcon} date={date} />
</PDFPage>
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<BriefingModule state={state} />
</SimpleLayout>
{state.sitemap && state.sitemap.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<SitemapModule state={state} />
</SimpleLayout>
)}
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<EstimationModule
state={state}
positions={positions}
totalPrice={totalPrice}
date={date}
/>
</SimpleLayout>
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<TransparenzModule pricing={pricing} />
</SimpleLayout>
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<ClosingModule />
</SimpleLayout>
</PDFDocument>
);
};

Some files were not shown because too many files have changed in this diff Show More