Compare commits

..

102 Commits

Author SHA1 Message Date
87445975cb fix(analytics-mailer): strictly enforce monthly reports and remove weekly logic completely
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m24s
Monorepo Pipeline / 🧪 Test (push) Successful in 42s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m10s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 5s
2026-08-03 14:20:38 +02:00
24625c20f2 feat: enhance analytics mailer with umami entry/exit pages and beginner-friendly UI
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m23s
Monorepo Pipeline / 🧪 Test (push) Successful in 41s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m20s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 6s
Analytics Mailer / 📊 Send Analytics Reports (push) Successful in 46s
2026-07-27 10:43:44 +02:00
9a92874011 fix(analytics-mailer): send emails individually to avoid exposing other recipients in CC/To field
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m25s
Monorepo Pipeline / 🧪 Test (push) Successful in 43s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m8s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 7s
Analytics Mailer / 📊 Send Analytics Reports (push) Successful in 47s
2026-07-21 12:01:20 +02:00
a54c3a784b fix(analytics-mailer): correct greeting for MB Grid Solutions
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧪 Test (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Monorepo Pipeline / 🚀 Release (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-07-21 12:01:07 +02:00
7b975ed88f fix(analytics-mailer): correct greetings for E-TIB and KLZ Cables
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧪 Test (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Monorepo Pipeline / 🚀 Release (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-07-21 11:59:44 +02:00
1a5a60304f style(mail): reduce margin of evaluation badges
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧪 Test (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Monorepo Pipeline / 🚀 Release (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-07-21 11:59:14 +02:00
48374528ad chore(analytics-mailer): ignore dry-run html previews
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m23s
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Monorepo Pipeline / 🚀 Release (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-07-21 11:57:25 +02:00
541da2db20 feat(mail): add client-safe context evaluations to analytics template
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m19s
Monorepo Pipeline / 🧪 Test (push) Successful in 40s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m7s
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-07-21 11:46:11 +02:00
39bb862fd6 feat(analytics-mailer): add gotify error logging
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m21s
Monorepo Pipeline / 🧪 Test (push) Successful in 40s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m6s
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-07-21 11:38:19 +02:00
09a85000bb chore(analytics-mailer): update client email recipients
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m25s
Monorepo Pipeline / 🧪 Test (push) Successful in 40s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m7s
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-07-21 11:32:23 +02:00
b6e8628970 feat(analytics-mailer): replace mailgun with stalwart smtp
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m23s
Monorepo Pipeline / 🧪 Test (push) Successful in 40s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m7s
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-07-21 10:46:51 +02:00
53ad9f23d8 feat(analytics-mailer): Redesign email to Umami format with 3 periods, German translation, and inline glossary
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m24s
Monorepo Pipeline / 🧪 Test (push) Successful in 41s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m4s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 7s
2026-07-20 14:17:07 +02:00
7a9dd06fc6 feat: setup analytics-mailer with gitea cron pipeline for e-tib and klz
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m42s
Monorepo Pipeline / 🧪 Test (push) Successful in 56s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m35s
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
Monthly Analytics Mailer / 📊 Send Monthly Reports (push) Failing after 59s
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 7s
2026-06-21 20:52:31 +02:00
985e532c61 feat(vikunja-mcp): add task attachments and extra metadata parameters support
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m38s
Monorepo Pipeline / 🧪 Test (push) Successful in 54s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m38s
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-06-21 12:26:44 +02:00
04462f21e1 feat: add outline MCP server
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m56s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m24s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m11s
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-06-04 21:40:00 +02:00
11babf3347 feat(mcp): expand Vikunja server with project, delete, and comment tools
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m20s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m3s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m13s
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-06-03 17:41:12 +02:00
3de92accc4 chore: configure Vikunja API token and base URL for MCP server
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 49s
Monorepo Pipeline / 🧹 Lint (push) Failing after 31s
Monorepo Pipeline / 🧪 Test (push) Failing after 23s
Monorepo Pipeline / 🏗️ Build (push) Failing after 22s
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-06-03 17:34:00 +02:00
c8961411ec feat: add vikunja mcp server
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Has been cancelled
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 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-06-03 17:26:04 +02:00
42ea30c11f fix: disable automatic vikunja icon changes (pride logo)
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1m2s
Monorepo Pipeline / 🧪 Test (push) Successful in 2m15s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m19s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m17s
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-06-03 17:22:10 +02:00
7b2cc3723a feat: add vikunja deployment configuration
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 5s
Monorepo Pipeline / 🧹 Lint (push) Successful in 5m15s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m31s
Monorepo Pipeline / 🏗️ Build (push) Successful in 5m35s
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-06-03 17:16:55 +02:00
0fc4ddbc28 fix: clean buildx caches and dangling builder volumes in mintel-optimizer
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 7m37s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m27s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m33s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m10s
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-06-03 16:46:42 +02:00
9f8eab2ad2 fix(mcp): resolve port clash on 3001 and force pnpm@10 for Node 20 builds
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m32s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m19s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m11s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 6s
2026-05-26 11:58:50 +02:00
69c483ac46 fix(infra): use dynamic path for registry volume to prevent prune failure
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m23s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m16s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m33s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 43s
2026-05-06 12:24:19 +02:00
ac178ccbb0 fix(qa): downgrade to v3 artifacts and align cookies for Gitea runners
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m6s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m14s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m24s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 13s
2026-04-25 19:13:22 +02:00
3f16828bdf ci: restore secrets and upload-artifact
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 9s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m26s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m29s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m51s
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-04-25 18:38:51 +02:00
0f7cd94e14 ci: comment out secrets in workflow_call
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m3s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m1s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m33s
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-04-25 18:34:03 +02:00
2cc6512afa ci: restore prepare job with diagnostics and verbose install
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 7s
Monorepo Pipeline / 🧪 Test (push) Successful in 55s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m4s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m43s
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-04-25 18:27:54 +02:00
151c98c884 ci: minimize prepare job for debugging
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 6s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m7s
Monorepo Pipeline / 🏗️ Build (push) Has started running
Monorepo Pipeline / 🚀 Release (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-04-25 18:24:29 +02:00
ba17d7940e ci: add more diagnostics to prepare job
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🚀 Release (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
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Monorepo Pipeline / 🧪 Test (push) Has been cancelled
2026-04-25 18:23:39 +02:00
46120dbd56 ci: remove docker cleanup from prepare job
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 5s
Monorepo Pipeline / 🧪 Test (push) Successful in 58s
Monorepo Pipeline / 🚀 Release (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
2026-04-25 18:22:11 +02:00
571c4797fa ci: add diagnostics to prepare job
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 58s
Monorepo Pipeline / 🚀 Release (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
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
2026-04-25 18:18:21 +02:00
c7d107a578 fix: use standard npm registry host
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m8s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m27s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m43s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-04-24 12:16:08 +02:00
5f50e98a0a fix: reorder steps and add workspace wipe
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m12s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m9s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m31s
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-04-24 12:08:39 +02:00
45f831ca1c fix: improve registry auth robustness
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 40s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m48s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m40s
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-04-24 11:59:01 +02:00
73356cca30 fix: harden docker prune steps
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m0s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m35s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m50s
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-04-24 11:53:14 +02:00
b7e630c1d8 chore: harden runner hygiene with aggressive cleanup
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 42s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m14s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m53s
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-04-24 11:05:56 +02:00
13c8bc29ae chore: add blender-mv utility
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 4s
Monorepo Pipeline / 🧪 Test (push) Successful in 54s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m43s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m14s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 8s
2026-04-21 18:47:53 +02:00
51a892fd76 fix(next-config): harden rewrite precedence
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m11s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m15s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 17s
2026-04-14 15:38:30 +02:00
9cc8e573ec fix(next-config): merge custom rewrites properly instead of overriding
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m7s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m13s
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-04-14 14:48:02 +02:00
a80464abc0 chore: repair lockfile mismatch to unblock CI
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 56s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m18s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m28s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 39s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 43s
Monorepo Pipeline / 🚀 Release (push) Successful in 2m23s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 2m40s
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 7s
2026-04-13 15:44:09 +02:00
0b929d00a1 fix(next-config): move serverActions to experimental for Next.js 15+ compatibility
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 18s
Monorepo Pipeline / 🧪 Test (push) Failing after 10s
Monorepo Pipeline / 🏗️ Build (push) Failing after 10s
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-04-13 12:30:31 +02:00
8ec2ecb454 fix(gatekeeper): bypass image optimizer for branding assets to fix missing assets in subpath deployments
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m13s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m13s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m30s
Monorepo Pipeline / 🚀 Release (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-04-13 12:26:45 +02:00
2a9c5780c3 fix: gitea mcp issues
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 4s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m41s
Monorepo Pipeline / 🧹 Lint (push) Successful in 5m17s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m16s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 11s
2026-03-12 13:33:10 +01:00
fbfc9feab0 fix(next-config): add serverActions.allowedOrigins to support branch deployments
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 25s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m28s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m22s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m3s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-03-10 23:29:12 +01:00
8486261555 fix(mcp): refactor all mcp servers to use multi-session sse transport
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m1s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m44s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m55s
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-03-10 13:32:16 +01:00
5e1f2669e6 feat(kabelfachmann-mcp): add local Ollama support for KABELFACHMANN_LLM_PROVIDER
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m4s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m53s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m2s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-03-08 14:01:37 +01:00
541f1c17b7 feat(mcps): add kabelfachmann MCP with Kabelhandbuch integration and remove legacy PM2 orchestration
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m6s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m52s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m1s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-03-08 01:01:43 +01:00
048fafa3db fix(payload-ai): remove phantom SCSS import and disable dynamic provider injection
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m3s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m34s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m56s
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
- Remove non-existent ChatWindow.scss import causing Webpack resolution errors
- Disable dynamic ChatWindowProvider injection (now statically declared in host app)
- Revert build script to tsc (no SCSS copy needed)
2026-03-07 11:46:41 +01:00
77e2c4f9b6 ci: remove unused zod dependencies across mcp servers to satisfy depcheck validation
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m21s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m48s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m59s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 5s
2026-03-06 17:56:14 +01:00
4eb1aaf640 ci: fix strict TS overloaded parameter matching inside useChat and payload tool bindings
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m14s
Monorepo Pipeline / 🧹 Lint (push) Failing after 2m4s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m39s
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-03-06 16:10:50 +01:00
61f2f83e0c ci: fix unhandled typescript exceptions and strict eslint errors caught by the pipeline
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 9s
Monorepo Pipeline / 🏗️ Build (push) Failing after 9s
Monorepo Pipeline / 🧪 Test (push) Failing after 9s
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-03-06 15:49:45 +01:00
2dc61e4937 chore: finalize containerized MCP setup and fix memory-mcp native module issues
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 9s
Monorepo Pipeline / 🧪 Test (push) Failing after 9s
Monorepo Pipeline / 🏗️ Build (push) Failing after 8s
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-03-06 15:20:52 +01:00
a6ca876823 chore: release v1.9.17
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 11s
Monorepo Pipeline / 🧪 Test (push) Failing after 9s
Monorepo Pipeline / 🏗️ Build (push) Failing after 9s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-03-06 01:32:12 +01:00
f615565323 fix(glitchtip-mcp): remove unused zod dependency blocking CI release 2026-03-06 01:32:11 +01:00
fcbf388ef8 chore: release v1.9.16
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 5s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m10s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m43s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m42s
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-03-06 00:55:54 +01:00
cbed10052b feat(payload-ai): implement context-aware Payload CMS Agent
- ChatWindow now gathers page context (URL, collectionSlug, document ID)
- chatEndpoint fetches real AIChatPermissions from database
- Agent uses OpenRouter Gemini 3 Flash with maxSteps: 10 for autonomous multi-step tool execution
- Fallback default collections when no permissions configured
2026-03-06 00:55:39 +01:00
560213680c fix(infra): add missing Gitea cleanup commands to maintenance script
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 6s
Monorepo Pipeline / 🧪 Test (push) Successful in 2m34s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m46s
Monorepo Pipeline / 🏗️ Build (push) Successful in 5m58s
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-03-05 23:23:09 +01:00
7e2542bf1f fix(infra): update volume ID for registry pruning
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Failing after 2s
Monorepo Pipeline / 🧹 Lint (push) Has been skipped
Monorepo Pipeline / 🧪 Test (push) Has been skipped
Monorepo Pipeline / 🏗️ Build (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-03-05 23:08:29 +01:00
df6bef7345 feat(klz-payload-mcp): revert to production URL for CMS operations 2026-03-05 21:51:46 +01:00
aa57e8c48b feat(klz-payload-mcp): implement JWT authentication for robust CMS updates 2026-03-05 17:55:59 +01:00
822e8a9d0f feat(mcps): add full CRUD capabilities to klz-payload-mcp 2026-03-05 12:53:47 +01:00
f0d1fb6647 feat(mcps): add mutation tools for pages and posts to klz-payload-mcp 2026-03-05 12:50:54 +01:00
751ffd59a0 feat(mcps): add pages and posts functions to klz-payload-mcp 2026-03-05 12:47:24 +01:00
d0a17a8a31 feat(mcps): add klz-payload-mcp on port 3006 for customer data 2026-03-05 12:42:20 +01:00
daa2750f89 feat(mcps): unify SSE/Stdio transport and fix handshake timeouts 2026-03-05 12:04:19 +01:00
29423123b3 feat(mcps): add glitchtip-mcp on port 3005 2026-03-05 11:16:23 +01:00
5c10eb0009 feat(mcps): add wiki/packages/releases/projects to gitea + new umami & serpbear MCPs 2026-03-05 10:52:05 +01:00
dca35a9900 chore: update pnpm lockfile for gitea-mcp new dependencies
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m19s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m2s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m40s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 18s
2026-03-04 15:34:45 +01:00
4430d473cb feat(mcps): enhance Gitea MCP with new tools and fix Memory MCP stdio execution
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 8s
Monorepo Pipeline / 🧹 Lint (push) Failing after 12s
Monorepo Pipeline / 🧪 Test (push) Failing after 13s
Monorepo Pipeline / 🏗️ Build (push) Failing after 13s
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-03-04 15:20:15 +01:00
0c27e3b5d8 fix(ci): implement robust gitea registry auth token discovery to replace docker/login-action
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 10s
Monorepo Pipeline / 🧪 Test (push) Failing after 10s
Monorepo Pipeline / 🏗️ Build (push) Failing after 10s
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-03-04 11:07:01 +01:00
616d8a039b feat(gitea): add branch and event filters to pipeline discovery 2026-03-04 10:07:41 +01:00
ee3d7714c2 feat(mcps): migrate gitea and memory MCPs to SSE transport on pm2 2026-03-04 10:05:08 +01:00
ddf896e3f9 fix(gitea): prevent mcp server crash if token is missing 2026-03-03 20:53:47 +01:00
b9d0199115 fix(mcps): natively load .env for production start scripts 2026-03-03 19:40:50 +01:00
1670b8e5ef chore: bump payload-ai 1.9.15
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 57s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m21s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m22s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 37s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 43s
Monorepo Pipeline / 🚀 Release (push) Successful in 2m34s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 3m9s
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-03-03 15:10:07 +01:00
1c43d12e4d fix(payload-ai): convert server actions to api endpoints, drop @payload-config dependency
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m20s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m22s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m33s
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-03-03 14:58:35 +01:00
5cf9922822 feat: add local Qdrant-based memory MCP and dev setup 2026-03-03 13:40:13 +01:00
9a4a95feea fix(packages): remove private flag from all engine packages
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 59s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m18s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m18s
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-03-03 13:39:38 +01:00
d3902c4c77 fix(ci): use NPM_TOKEN instead of REGISTRY_PASS for Gitea docker registry login 2026-03-03 13:35:12 +01:00
21ec8a33ae fix(ci): use explicit registry token instead of GITHUB_TOKEN for docker login 2026-03-03 12:54:13 +01:00
79d221de5e chore: sync lockfile and payload-ai extensions for release v1.9.10
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m20s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m27s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m35s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Failing after 17s
Monorepo Pipeline / 🐳 Build Build-Base (push) Failing after 17s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Failing after 17s
Monorepo Pipeline / 🚀 Release (push) Successful in 1m33s
2026-03-03 12:40:41 +01:00
24fde20030 chore: release v1.9.10
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 11s
Monorepo Pipeline / 🧪 Test (push) Failing after 10s
Monorepo Pipeline / 🏗️ Build (push) Failing after 9s
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-03-03 12:40:02 +01:00
4a4409ca85 chore: remove accidentally tracked wip packages breaking lockfile 2026-03-03 12:39:59 +01:00
d96d6a4b13 chore: release v1.9.9
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 10s
Monorepo Pipeline / 🧪 Test (push) Failing after 9s
Monorepo Pipeline / 🏗️ Build (push) Failing after 9s
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-03-03 12:24:39 +01:00
8f6b12d827 fix(packages): remove private flag from all feature/engine packages to allow npm publish 2026-03-03 12:24:38 +01:00
a11714d07d chore(ci): migrate docker registry publishers to git.infra.mintel.me 2026-03-03 12:13:39 +01:00
52f7e68f25 chore: release v1.9.8
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m15s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m17s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m15s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 37s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 41s
Monorepo Pipeline / 🚀 Release (push) Successful in 1m44s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 2m31s
2026-03-03 11:52:29 +01:00
217ac33675 chore: release v1.9.8
Some checks are pending
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m11s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m7s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m19s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 37s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 41s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 2m32s
Monorepo Pipeline / 🚀 Release (push) Has started running
2026-03-03 11:44:54 +01:00
f2b8b136af chore: release v1.9.7
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m15s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m6s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m19s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 38s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 43s
Monorepo Pipeline / 🚀 Release (push) Successful in 1m54s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 2m33s
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-03-02 21:16:51 +01:00
2e07b213d1 chore: remove unused 3d dependencies in gatekeeper to fix lint 2026-03-02 21:16:49 +01:00
a2c1eaefba chore: release v1.9.6
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m18s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m32s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m3s
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-03-02 21:08:34 +01:00
80ff266f9c fix: allow vitest to pass with no tests in seo-engine
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 54s
Monorepo Pipeline / 🧹 Lint (push) Failing after 2m7s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m13s
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-03-02 21:04:14 +01:00
6b1c5b7e30 chore: release @mintel/payload-ai
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Failing after 47s
Monorepo Pipeline / 🧹 Lint (push) Failing after 2m3s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m10s
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-03-02 21:00:51 +01:00
80eefad5ea feat: extract reusable @mintel/payload-ai package 2026-03-02 21:00:09 +01:00
72556af24c fix(mcp): handle gitea api envelope responses and add safety checks
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Failing after 54s
Monorepo Pipeline / 🧹 Lint (push) Failing after 2m36s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m27s
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-03-02 12:53:11 +01:00
2a5466c6c0 feat(gitea-mcp): add custom Gitea MCP server for Antigravity compatibility
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧪 Test (push) Failing after 50s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m6s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m13s
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-03-02 12:36:57 +01:00
2d36a4ec71 ci(qa): refactor QA suite into granular jobs and fix NPM_TOKEN auth
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Failing after 47s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m3s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m41s
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-03-02 11:51:12 +01:00
ded9da7d32 feat(seo-engine): implement competitor scraper, MDX draft editor, and strategy report generator
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Failing after 51s
Monorepo Pipeline / 🧹 Lint (push) Failing after 2m25s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m28s
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-03-02 10:16:11 +01:00
36ed26ad79 feat(gatekeeper): major UI upgrade - high-fidelity light theme, iridescent mouse-reactive form, and enhanced background animation
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m10s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m15s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m53s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-02-28 21:48:03 +01:00
4e72a0baac fix(pipeline): remove image-processor build job and cms-infra from gatekeeper dockerfile
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 46s
Monorepo Pipeline / 🧹 Lint (push) Successful in 1m48s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m48s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-02-27 23:08:39 +01:00
8ca7eb3f49 chore: release v1.9.5
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 59s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m33s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m4s
Monorepo Pipeline / 🐳 Build Image Processor (push) Failing after 24s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Failing after 37s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Failing after 14s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 41s
Monorepo Pipeline / 🚀 Release (push) Successful in 1m41s
2026-02-27 22:27:30 +01:00
32d3ff010a feat(release): introduce dedicated release script to replace flawed git push hook
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 7s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m4s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m50s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m21s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (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-27 21:03:54 +01:00
154 changed files with 15166 additions and 331 deletions

View File

@@ -0,0 +1,5 @@
---
"@mintel/next-config": patch
---
fix: add serverActions.allowedOrigins to support branch deployments

View File

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

23
.env
View File

@@ -3,6 +3,7 @@ IMAGE_TAG=v1.8.19
PROJECT_NAME=at-mintel PROJECT_NAME=at-mintel
PROJECT_COLOR=#82ed20 PROJECT_COLOR=#82ed20
GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582 GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582
GITEA_HOST=https://git.infra.mintel.me
OPENROUTER_API_KEY=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 OPENROUTER_API_KEY=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
ZYTE_API_KEY=1f0f74726f044f55aaafc7ead32cd489 ZYTE_API_KEY=1f0f74726f044f55aaafc7ead32cd489
REPLICATE_API_KEY=r8_W3grtpXMRfi0u3AM9VdkKbuWdZMmhwU2Tn0yt REPLICATE_API_KEY=r8_W3grtpXMRfi0u3AM9VdkKbuWdZMmhwU2Tn0yt
@@ -11,6 +12,11 @@ DATA_FOR_SEO_API_KEY=bWFyY0BtaW50ZWwubWU6MjQ0YjBjZmIzOGY3NTIzZA==
DATA_FOR_SEO_LOGIN=marc@mintel.me DATA_FOR_SEO_LOGIN=marc@mintel.me
DATA_FOR_SEO_PASSWORD=244b0cfb38f7523d DATA_FOR_SEO_PASSWORD=244b0cfb38f7523d
# Kabelfachmann LLM Configuration
KABELFACHMANN_LLM_PROVIDER=openrouter
KABELFACHMANN_OLLAMA_MODEL=qwen3.5
KABELFACHMANN_OLLAMA_HOST=http://host.docker.internal:11434
# Authentication # Authentication
GATEKEEPER_PASSWORD=mintel GATEKEEPER_PASSWORD=mintel
AUTH_COOKIE_NAME=mintel_gatekeeper_session AUTH_COOKIE_NAME=mintel_gatekeeper_session
@@ -41,3 +47,20 @@ SENTRY_DSN=
# Analytics (Umami) # Analytics (Umami)
NEXT_PUBLIC_UMAMI_WEBSITE_ID= NEXT_PUBLIC_UMAMI_WEBSITE_ID=
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
UMAMI_USERNAME=mmintel
UMAMI_PASSWORD=Tim300493.
# MCP Services
QDRANT_URL=http://qdrant:6333
SERPBEAR_API_KEY=Tim300493.
GLITCHTIP_API_KEY=3d37db66fa1fc3a1cdcd1357728b056225aca950f0163b50c5e922344807ede9
# Corrected MCP Base URLs
UMAMI_BASE_URL=https://analytics.infra.mintel.me
GLITCHTIP_BASE_URL=https://errors.infra.mintel.me
SERPBEAR_BASE_URL=https://seo.infra.mintel.me
# Vikunja MCP
VIKUNJA_API_TOKEN=tk_468a6f7a59b329aa42796378e55075b79eb4d3d4
VIKUNJA_BASE_URL=https://tasks.infra.mintel.me

View File

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

View File

@@ -0,0 +1,49 @@
name: Analytics Mailer (Monthly)
on:
schedule:
# Run monthly on the 1st day of the month at 08:30 UTC
- cron: '30 8 1 * *'
workflow_dispatch:
jobs:
send-reports:
name: 📊 Send Analytics Reports (Monthly)
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
UMAMI_BASE_URL: ${{ secrets.UMAMI_BASE_URL }}
UMAMI_USERNAME: ${{ secrets.UMAMI_USERNAME }}
UMAMI_PASSWORD: ${{ secrets.UMAMI_PASSWORD }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_PORT: ${{ secrets.SMTP_PORT }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASS: ${{ secrets.SMTP_PASS }}
SMTP_SENDER: ${{ secrets.SMTP_SENDER }}
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
GOTIFY_TOKEN: ${{ secrets.GOTIFY_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 mail templates
run: pnpm --filter @mintel/mail build
- name: Build analytics mailer
run: pnpm --filter @mintel/analytics-mailer build
- name: Run Analytics Mailer
run: pnpm --filter @mintel/analytics-mailer run start

View File

@@ -192,9 +192,6 @@ jobs:
file: packages/infra/docker/Dockerfile.gatekeeper file: packages/infra/docker/Dockerfile.gatekeeper
name: Gatekeeper (Product) name: Gatekeeper (Product)
- image: image-processor
file: apps/image-service/Dockerfile
name: Image Processor
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -202,12 +199,31 @@ jobs:
- name: 🐳 Set up Docker Buildx - name: 🐳 Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: 🔐 Registry Login - name: 🔐 Discover Valid Registry Token
uses: docker/login-action@v3 id: discover_token
with: run: |
registry: registry.infra.mintel.me echo "Testing available secrets against git.infra.mintel.me Docker registry..."
username: ${{ secrets.REGISTRY_USER }} TOKENS="${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}"
password: ${{ secrets.REGISTRY_PASS }} USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel"
for TOKEN in $TOKENS; do
if [ -n "$TOKEN" ]; then
for U in $USERS; do
if [ -n "$U" ]; then
echo "Attempting docker login for a token with user $U..."
if echo "$TOKEN" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then
echo "✅ Successfully authenticated with a token."
echo "::add-mask::$TOKEN"
echo "token=$TOKEN" >> $GITHUB_OUTPUT
echo "user=$U" >> $GITHUB_OUTPUT
exit 0
fi
fi
done
fi
done
echo "❌ All available tokens failed to authenticate!"
exit 1
- name: 🏗️ Build & Push ${{ matrix.name }} - name: 🏗️ Build & Push ${{ matrix.name }}
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
@@ -219,8 +235,8 @@ jobs:
provenance: false provenance: false
push: true push: true
secrets: | secrets: |
NPM_TOKEN=${{ secrets.NPM_TOKEN }} NPM_TOKEN=${{ steps.discover_token.outputs.token }}
tags: | tags: |
registry.infra.mintel.me/mintel/${{ matrix.image }}:${{ github.ref_name }} git.infra.mintel.me/mmintel/${{ matrix.image }}:${{ github.ref_name }}
registry.infra.mintel.me/mintel/${{ matrix.image }}:latest git.infra.mintel.me/mmintel/${{ matrix.image }}:latest

View File

@@ -18,10 +18,55 @@ on:
required: true required: true
GATEKEEPER_PASSWORD: GATEKEEPER_PASSWORD:
required: true required: true
NPM_TOKEN:
required: false
MINTEL_PRIVATE_TOKEN:
required: false
GITEA_PAT:
required: false
jobs: jobs:
qa_suite: prepare:
name: 🛡 Nightly QA Suite name: 🏗 Prepare & Install
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- name: 🔐 Registry Auth
run: |
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
echo "@mintel:registry=https://$REGISTRY" > .npmrc
echo "//$REGISTRY/:_authToken=${{ secrets.NPM_TOKEN || secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}" >> .npmrc
- name: 🔍 Diagnostics
run: |
echo "👤 User: $(id)"
echo "📂 Directory: $(pwd)"
echo "📂 Disk Space:"
df -h
- name: Install dependencies
run: |
rm -rf .next .turbo node_modules || true
pnpm install --no-frozen-lockfile --reporter=append-only
- name: 📦 Archive dependencies
uses: actions/upload-artifact@v3
with:
name: node_modules
path: node_modules
retention-days: 1
static:
name: 🔍 Static Analysis
needs: prepare
runs-on: docker runs-on: docker
container: container:
image: catthehacker/ubuntu:act-latest image: catthehacker/ubuntu:act-latest
@@ -36,98 +81,126 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- name: 🔐 Registry Auth - name: 📥 Restore dependencies
run: | uses: actions/download-artifact@v3
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.MINTEL_PRIVATE_TOKEN || secrets.GITEA_PAT }}" >> .npmrc
- name: Install dependencies
id: deps
run: |
pnpm store prune
pnpm install --no-frozen-lockfile
- name: 📦 Cache APT Packages
uses: actions/cache@v4
with: with:
path: /var/cache/apt/archives name: node_modules
key: apt-cache-${{ runner.os }}-${{ runner.arch }}-chromium - name: 🌐 HTML Validation
- name: 💾 Cache Chromium
id: cache-chromium
uses: actions/cache@v4
with:
path: /usr/bin/chromium
key: ${{ runner.os }}-chromium-native-${{ hashFiles('package.json') }}
- name: 🔍 Install Chromium (Native & ARM64)
if: steps.cache-chromium.outputs.cache-hit != 'true' && steps.deps.outcome == 'success'
run: |
rm -f /etc/apt/apt.conf.d/docker-clean
apt-get update
apt-get install -y gnupg wget ca-certificates
OS_ID=$(. /etc/os-release && echo $ID)
CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
if [ "$OS_ID" = "debian" ]; then
apt-get install -y chromium
else
mkdir -p /etc/apt/keyrings
KEY_ID="82BB6851C64F6880"
wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_ID" | gpg --dearmor > /etc/apt/keyrings/xtradeb.gpg
echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" > /etc/apt/sources.list.d/xtradeb-ppa.list
printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" > /etc/apt/preferences.d/xtradeb
apt-get update
apt-get install -y --allow-downgrades chromium
fi
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/google-chrome
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/chromium-browser
# ── Quality Gates ─────────────────────────────────────────────────────────
- name: 🌐 Full Sitemap HTML Validation
if: always() && steps.deps.outcome == 'success'
env: env:
NEXT_PUBLIC_BASE_URL: ${{ inputs.TARGET_URL }} NEXT_PUBLIC_BASE_URL: ${{ inputs.TARGET_URL }}
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
run: pnpm run check:html run: pnpm run check:html
- name: 🖼️ Asset Scan
- name: 🌐 Dynamic Asset Presence & Error Scan
if: always() && steps.deps.outcome == 'success'
env: env:
NEXT_PUBLIC_BASE_URL: ${{ inputs.TARGET_URL }} NEXT_PUBLIC_BASE_URL: ${{ inputs.TARGET_URL }}
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
run: pnpm run check:assets run: pnpm run check:assets
- name: ♿ Accessibility Scan (WCAG) accessibility:
if: always() && steps.deps.outcome == 'success' name: ♿ Accessibility
needs: prepare
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: 📥 Restore dependencies
uses: actions/download-artifact@v3
with:
name: node_modules
- name: 🔍 Install Chromium
run: |
apt-get update && apt-get install -y gnupg wget ca-certificates
CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
mkdir -p /etc/apt/keyrings
wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x82BB6851C64F6880" | gpg --dearmor > /etc/apt/keyrings/xtradeb.gpg
echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" > /etc/apt/sources.list.d/xtradeb-ppa.list
printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" > /etc/apt/preferences.d/xtradeb
apt-get update && apt-get install -y --allow-downgrades chromium
ln -sf /usr/bin/chromium /usr/bin/google-chrome
- name: ♿ WCAG Scan
continue-on-error: true continue-on-error: true
env: env:
NEXT_PUBLIC_BASE_URL: ${{ inputs.TARGET_URL }} NEXT_PUBLIC_BASE_URL: ${{ inputs.TARGET_URL }}
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
run: pnpm run check:wcag run: pnpm run check:wcag
- name: 📦 Unused Dependencies Scan (depcheck) analysis:
if: always() && steps.deps.outcome == 'success' name: 🧪 Maintenance & Links
needs: prepare
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: 📥 Restore dependencies
uses: actions/download-artifact@v3
with:
name: node_modules
- name: 📦 Depcheck
continue-on-error: true continue-on-error: true
run: pnpm dlx depcheck --ignores="*eslint*,*typescript*,*tailwindcss*,*postcss*,*prettier*,*@types/*,*husky*,*lint-staged*,*@next/*,*@lhci/*,*commitlint*,*cspell*,*rimraf*,*@payloadcms/*,*start-server-and-test*,*html-validate*,*critters*,*dotenv*,*turbo*" run: pnpm dlx depcheck --ignores="*eslint*,*typescript*,*tailwindcss*,*postcss*,*prettier*,*@types/*,*husky*,*lint-staged*,*@next/*,*@lhci/*,*commitlint*,*cspell*,*rimraf*,*@payloadcms/*,*start-server-and-test*,*html-validate*,*critters*,*dotenv*,*turbo*"
- name: 🔗 Lychee Link Check
- name: 🔗 Markdown & HTML Link Check (Lychee)
if: always() && steps.deps.outcome == 'success'
uses: lycheeverse/lychee-action@v2 uses: lycheeverse/lychee-action@v2
with: with:
args: --accept 200,204,429 --timeout 15 content/ app/ public/ args: --accept 200,204,429 --timeout 15 content/ app/ public/
fail: true fail: true
- name: 🎭 LHCI Desktop Audit performance:
id: lhci_desktop name: 🎭 Lighthouse
if: always() && steps.deps.outcome == 'success' needs: prepare
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- name: 📥 Restore dependencies
uses: actions/download-artifact@v3
with:
name: node_modules
- name: 🔍 Install Chromium
run: |
apt-get update && apt-get install -y gnupg wget ca-certificates
CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
mkdir -p /etc/apt/keyrings
wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x82BB6851C64F6880" | gpg --dearmor > /etc/apt/keyrings/xtradeb.gpg
echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" > /etc/apt/sources.list.d/xtradeb-ppa.list
printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" > /etc/apt/preferences.d/xtradeb
apt-get update && apt-get install -y --allow-downgrades chromium
ln -sf /usr/bin/chromium /usr/bin/google-chrome
- name: 🎭 LHCI Desktop
env: env:
LHCI_URL: ${{ inputs.TARGET_URL }} LHCI_URL: ${{ inputs.TARGET_URL }}
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
run: pnpm run pagespeed:test -- --collect.settings.preset=desktop run: pnpm run pagespeed:test -- --collect.settings.preset=desktop
- name: 📱 LHCI Mobile
- name: 📱 LHCI Mobile Audit
id: lhci_mobile
if: always() && steps.deps.outcome == 'success'
env: env:
LHCI_URL: ${{ inputs.TARGET_URL }} LHCI_URL: ${{ inputs.TARGET_URL }}
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
@@ -135,7 +208,7 @@ jobs:
notifications: notifications:
name: 🔔 Notify name: 🔔 Notify
needs: [qa_suite] needs: [prepare, static, accessibility, analysis, performance]
if: always() if: always()
runs-on: docker runs-on: docker
container: container:
@@ -144,22 +217,30 @@ jobs:
- name: 🔔 Gotify - name: 🔔 Gotify
shell: bash shell: bash
run: | run: |
SUITE="${{ needs.qa_suite.result }}" PREPARE="${{ needs.prepare.result }}"
STATIC="${{ needs.static.result }}"
A11Y="${{ needs.accessibility.result }}"
ANALYSIS="${{ needs.analysis.result }}"
PERF="${{ needs.performance.result }}"
PROJECT="${{ inputs.PROJECT_NAME }}" PROJECT="${{ inputs.PROJECT_NAME }}"
URL="${{ inputs.TARGET_URL }}" URL="${{ inputs.TARGET_URL }}"
if [[ "$SUITE" != "success" ]]; then if [[ "$PREPARE" != "success" || "$STATIC" != "success" || "$PERF" != "success" ]]; then
PRIORITY=8 PRIORITY=8
EMOJI="⚠️" EMOJI="🚨"
STATUS_LINE="Nightly QA Failed! Action required." STATUS_LINE="Nightly QA Failed! Action required."
else else
PRIORITY=2 PRIORITY=2
EMOJI="✅" EMOJI="✅"
STATUS_LINE="Nightly QA Passed perfectly." STATUS_LINE="Nightly QA Passed."
fi fi
TITLE="$EMOJI $PROJECT Nightly QA" TITLE="$EMOJI $PROJECT Nightly QA"
MESSAGE="$STATUS_LINE\n$URL\nPlease check Pipeline output for details." MESSAGE="$STATUS_LINE
Prepare: $PREPARE | Static: $STATIC | A11y: $A11Y
Analysis: $ANALYSIS | Perf: $PERF
$URL"
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=$TITLE" \ -F "title=$TITLE" \

8
.gitignore vendored
View File

@@ -47,3 +47,11 @@ directus/uploads/directus-health-file
data/crawls/ data/crawls/
packages/estimation-engine/out/ packages/estimation-engine/out/
apps/web/out/estimations/ apps/web/out/estimations/
# Memory MCP
data/qdrant/
packages/memory-mcp/models/
# Kabelfachmann MCP
packages/kabelfachmann-mcp/data/
packages/kabelfachmann-mcp/models/

View File

@@ -5,49 +5,4 @@ if [ -f "$SCRIPT_DIR/scripts/validate-sdk-imports.sh" ]; then
"$SCRIPT_DIR/scripts/validate-sdk-imports.sh" || exit 1 "$SCRIPT_DIR/scripts/validate-sdk-imports.sh" || exit 1
fi fi
# Check if we are pushing a tag
while read local_ref local_sha remote_ref remote_sha
do
if [[ "$remote_ref" == refs/tags/* ]]; 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."
# Push branch AND tag directly inside the hook because Git native push has already recorded the old SHA
CURRENT_BRANCH=$(git branch --show-current)
if [ -n "$CURRENT_BRANCH" ]; then
git push origin "$CURRENT_BRANCH" --no-verify
fi
git push origin "$TAG" --force --no-verify
echo ""
echo "✨ VERSIONS SYNCED & PUSHED SUCCESSFULLY ✨"
echo "The correct commit has been tagged on origin."
echo "(Info: You will see a 'pre-push hook failed' and 'failed to push' error below. Please completely ignore it. We MUST abort the native git push, otherwise Git would push the pre-sync commit to the tag and break the version history.)"
echo ""
# We MUST exit 1, otherwise native Git will push the wrong commit to the tag
exit 1
else
echo "✨ Versions already in sync for $TAG."
exit 0
fi
fi
done

View File

@@ -1,5 +1,5 @@
# Stage 1: Builder # Stage 1: Builder
FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder FROM git.infra.mintel.me/mmintel/nextjs:latest AS builder
WORKDIR /app WORKDIR /app
# Clean the workspace in case the base image is dirty # Clean the workspace in case the base image is dirty
@@ -37,7 +37,7 @@ COPY . .
RUN pnpm build RUN pnpm build
# Stage 2: Runner # Stage 2: Runner
FROM registry.infra.mintel.me/mintel/runtime:latest AS runner FROM git.infra.mintel.me/mmintel/runtime:latest AS runner
WORKDIR /app WORKDIR /app
ENV HOSTNAME="0.0.0.0" ENV HOSTNAME="0.0.0.0"

1
apps/analytics-mailer/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dry-run-*.html

View File

@@ -0,0 +1,30 @@
[
{
"websiteId": "59a7db94-0100-4c7e-98ef-99f45b17f9c3",
"domain": "klz-cables.com",
"clientEmail": "klaus.mintel@klz-cables.com, michael.bodemer@klz-cables.com, marc@mintel.me",
"clientName": "KLZ Cables",
"greeting": "Hallo Klaus, hallo Michael,"
},
{
"websiteId": "b9035279-72f1-49bc-909d-b073d184c388",
"domain": "mintel.me",
"clientEmail": "marc@mintel.me",
"clientName": "Mintel.me",
"greeting": "Hallo Team Mintel,"
},
{
"websiteId": "caa402bc-b2d2-4349-a254-84c0dd2a4460",
"domain": "mb-grid-solutions.com",
"clientEmail": "marc@mintel.me",
"clientName": "MB Grid Solutions",
"greeting": "Hallo Marc,"
},
{
"websiteId": "d773ea10-a3b3-4ccf-9024-987e14c4d669",
"domain": "e-tib.com",
"clientEmail": "d.joseph@e-tib.com, marc@mintel.me",
"clientName": "E-TIB GmbH",
"greeting": "Hallo Danny,"
}
]

View File

@@ -0,0 +1,27 @@
{
"name": "@mintel/analytics-mailer",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsup src/index.ts --format esm --clean",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
},
"dependencies": {
"@mintel/mail": "workspace:*",
"axios": "^1.7.9",
"dotenv": "^16.4.7",
"nodemailer": "^9.0.3"
},
"devDependencies": {
"@mintel/eslint-config": "workspace:*",
"@mintel/tsconfig": "workspace:*",
"@types/node": "^22.19.10",
"@types/nodemailer": "^8.0.1",
"tsup": "^8.3.5",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
"vitest": "^4.1.10"
}
}

View File

@@ -0,0 +1,28 @@
import { describe, it, expect, vi } from "vitest";
import { fetchUmamiData } from "./index";
describe("fetchUmamiData", () => {
it("fetches entry and exit pages correctly", async () => {
const apiMock = {
get: vi.fn().mockImplementation((url, config) => {
if (url.includes("/stats")) return Promise.resolve({ data: { visitors: 10, visits: 10, pageviews: 10, bounces: 5, totaltime: 100 } });
if (url.includes("/pageviews")) return Promise.resolve({ data: { pageviews: [] } });
if (url.includes("/metrics")) {
if (config.params.type === "path") return Promise.resolve({ data: [{ x: "/path", y: 5 }] });
if (config.params.type === "referrer") return Promise.resolve({ data: [{ x: "google", y: 3 }] });
if (config.params.type === "entry") return Promise.resolve({ data: [{ x: "/entry", y: 4 }] });
if (config.params.type === "exit") return Promise.resolve({ data: [{ x: "/exit", y: 2 }] });
}
return Promise.resolve({ data: {} });
})
};
const result = await fetchUmamiData(apiMock, "test-id", 0, 1000, "Test");
// We expect the new entry and exit pages to be parsed and returned correctly
expect(result.topEntryPages).toBeDefined();
expect(result.topEntryPages[0]).toEqual({ url: "/entry", visitors: 4 });
expect(result.topExitPages).toBeDefined();
expect(result.topExitPages[0]).toEqual({ url: "/exit", visitors: 2 });
});
});

View File

@@ -0,0 +1,202 @@
import fs from "fs/promises";
import path from "path";
import axios from "axios";
import { render } from "@mintel/mail";
import { AnalyticsTemplate } from "@mintel/mail";
import { getThisMonth, getThisYear } from "./utils/time-calculator";
import nodemailer from "nodemailer";
import * as dotenv from "dotenv";
dotenv.config();
const UMAMI_BASE_URL = process.env.UMAMI_BASE_URL || "https://umami.infra.mintel.me";
const UMAMI_USERNAME = process.env.UMAMI_USERNAME;
const UMAMI_PASSWORD = process.env.UMAMI_PASSWORD;
const UMAMI_API_KEY = process.env.UMAMI_API_KEY;
const SMTP_HOST = process.env.SMTP_HOST || "mail.infra.mintel.me";
const SMTP_PORT = parseInt(process.env.SMTP_PORT || "587", 10);
const SMTP_USER = process.env.SMTP_USER;
const SMTP_PASS = process.env.SMTP_PASS;
const SMTP_SENDER = process.env.SMTP_SENDER || "reports@mintel.me";
const GOTIFY_URL = process.env.GOTIFY_URL;
const GOTIFY_TOKEN = process.env.GOTIFY_TOKEN;
interface ConfigClient {
websiteId: string;
domain: string;
clientEmail: string;
clientName: string;
greeting?: string;
}
async function getUmamiToken(): Promise<string> {
if (UMAMI_API_KEY) {
return UMAMI_API_KEY;
}
if (!UMAMI_USERNAME || !UMAMI_PASSWORD) {
throw new Error("Missing UMAMI_USERNAME and UMAMI_PASSWORD or UMAMI_API_KEY");
}
const res = await axios.post(`${UMAMI_BASE_URL}/api/auth/login`, {
username: UMAMI_USERNAME,
password: UMAMI_PASSWORD,
});
return res.data.token;
}
import https from "https";
export async function fetchUmamiData(api: any, websiteId: string, startAt: number, endAt: number, label: string) {
let unit = "day";
if (label === "Dieses Jahr") unit = "month";
const [statsRes, pagesRes, refRes, viewsRes, entryRes, exitRes] = await Promise.all([
api.get(`/websites/${websiteId}/stats`, { params: { startAt, endAt } }),
api.get(`/websites/${websiteId}/metrics`, { params: { type: "path", limit: 5, startAt, endAt } }),
api.get(`/websites/${websiteId}/metrics`, { params: { type: "referrer", limit: 5, startAt, endAt } }),
api.get(`/websites/${websiteId}/pageviews`, { params: { startAt, endAt, unit, timezone: "Europe/Berlin" } }).catch(() => ({ data: { pageviews: [] } })),
api.get(`/websites/${websiteId}/metrics`, { params: { type: "entry", limit: 5, startAt, endAt } }),
api.get(`/websites/${websiteId}/metrics`, { params: { type: "exit", limit: 5, startAt, endAt } }),
]);
const stats = statsRes.data;
const topPages = (pagesRes.data || []).map((p: any) => ({ url: p.x, views: p.y || 0 }));
const topReferrers = (refRes.data || []).map((r: any) => ({ url: r.x, visitors: r.y || 0 }));
const topEntryPages = (entryRes.data || []).map((p: any) => ({ url: p.x, visitors: p.y || 0 }));
const topExitPages = (exitRes.data || []).map((p: any) => ({ url: p.x, visitors: p.y || 0 }));
const visitors = typeof stats.visitors === "object" ? stats.visitors?.value || 0 : stats.visitors || 0;
const visits = typeof stats.visits === "object" ? stats.visits?.value || 0 : stats.visits || 0;
const views = typeof stats.pageviews === "object" ? stats.pageviews?.value || 0 : stats.pageviews || 0;
const bounces = typeof stats.bounces === "object" ? stats.bounces?.value || 0 : stats.bounces || 0;
const totaltime = typeof stats.totaltime === "object" ? stats.totaltime?.value || 0 : stats.totaltime || 0;
const bounceRate = visits > 0 ? Math.round((bounces / visits) * 100) : 0;
let visitDuration = "0s";
if (visits > 0) {
const avgSeconds = Math.round(totaltime / visits);
if (avgSeconds < 60) {
visitDuration = `${avgSeconds}s`;
} else {
visitDuration = `${Math.floor(avgSeconds / 60)}m ${avgSeconds % 60}s`;
}
}
const chartData = (viewsRes.data?.pageviews || []).map((pv: any) => ({
label: pv.x,
value: pv.y || 0
}));
return {
label,
visitors,
visits,
views,
bounceRate,
visitDuration,
topPages,
topReferrers,
topEntryPages,
topExitPages,
chartData,
};
}
async function main() {
const isDryRun = process.argv.includes("--dry-run");
// Load config
const configPath = path.resolve(process.cwd(), "config.json");
const configRaw = await fs.readFile(configPath, "utf-8");
const clients: ConfigClient[] = JSON.parse(configRaw);
// Setup Umami API
const token = await getUmamiToken();
const headers = UMAMI_API_KEY ? { "x-umami-api-key": token } : { Authorization: `Bearer ${token}` };
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
const api = axios.create({ baseURL: `${UMAMI_BASE_URL}/api`, headers, httpsAgent });
// Setup SMTP Transporter (Nodemailer)
const transporter = nodemailer.createTransport({
host: SMTP_HOST,
port: SMTP_PORT,
secure: SMTP_PORT === 465,
auth: SMTP_USER && SMTP_PASS ? {
user: SMTP_USER,
pass: SMTP_PASS,
} : undefined,
});
const timeRanges = [getThisMonth(), getThisYear()];
console.log(`Generating reports...`);
for (const client of clients) {
if (client.websiteId === "replace-with-umami-website-id") continue;
console.log(`Processing ${client.domain} (${client.clientName})...`);
try {
const periods = await Promise.all(
timeRanges.map((tr) => fetchUmamiData(api, client.websiteId, tr.startAt, tr.endAt, tr.label))
);
const html = await render(AnalyticsTemplate({
greeting: client.greeting || `Hallo ${client.clientName},`,
domain: client.domain,
periods,
}));
const subject = `Performance-Report - ${client.domain}`;
const recipients = client.clientEmail.split(',').map(e => e.trim()).filter(e => e.length > 0);
if (isDryRun) {
for (const recipient of recipients) {
console.log(`[DRY-RUN] Would send individual email to: ${recipient} (for ${client.domain})`);
}
const debugPath = path.resolve(process.cwd(), `dry-run-${client.domain}.html`);
await fs.writeFile(debugPath, html);
console.log(`[DRY-RUN] Saved HTML preview to ${debugPath}`);
} else {
if (!SMTP_HOST) {
throw new Error("SMTP_HOST is missing for live run.");
}
for (const recipient of recipients) {
await transporter.sendMail({
from: SMTP_SENDER,
to: recipient,
subject,
html,
});
console.log(`Successfully sent to ${recipient}`);
}
}
} catch (e: any) {
const errorMsg = e.response?.data || e.message;
console.error(`Failed to process client ${client.domain}:`, errorMsg);
if (GOTIFY_URL && GOTIFY_TOKEN) {
try {
await axios.post(`${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`, {
title: `❌ Analytics Mailer Error (${client.domain})`,
message: `Failed to generate or send report for ${client.domain}.\n\nError:\n${errorMsg}`,
priority: 8
});
} catch (gotifyErr: any) {
console.error("Failed to push to Gotify:", gotifyErr.message);
}
}
}
}
console.log("Done.");
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});

View File

@@ -0,0 +1,19 @@
export function getLast7Days() {
const endAt = Date.now();
const startAt = endAt - 7 * 24 * 60 * 60 * 1000;
return { label: "Letzte 7 Tage", startAt, endAt };
}
export function getThisMonth() {
const now = new Date();
const startAt = new Date(now.getFullYear(), now.getMonth(), 1).getTime();
const endAt = Date.now();
return { label: "Dieser Monat", startAt, endAt };
}
export function getThisYear() {
const now = new Date();
const startAt = new Date(now.getFullYear(), 0, 1).getTime();
const endAt = Date.now();
return { label: "Dieses Jahr", startAt, endAt };
}

View File

@@ -0,0 +1,10 @@
{
"extends": "@mintel/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "NodeNext",
"moduleResolution": "NodeNext"
},
"include": ["src/**/*"]
}

View File

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

177
blender-mv.sh Executable file
View File

@@ -0,0 +1,177 @@
#!/usr/bin/env bash
# blender-mv: A smart file/directory mover that automatically updates Blender paths
set -e
print_usage() {
echo "Usage: blender-mv [OPTIONS] SOURCE DEST"
echo ""
echo "Options:"
echo " --search-project <dir> Scan this specific project directory for .blend files"
echo " to update (useful if you move an external asset folder,"
echo " and old .blend files referencing it are elsewhere)."
echo ""
echo "Example: blender-mv ./old_folder ./new_folder"
}
SEARCH_PROJECT=""
# Parse arguments
while [[ "$#" -gt 0 ]]; do
case $1 in
--search-project) SEARCH_PROJECT="$2"; shift 2 ;;
-h|--help) print_usage; exit 0 ;;
*)
if [[ -z "$SOURCE" ]]; then
SOURCE="$1"
elif [[ -z "$DEST" ]]; then
DEST="$1"
else
echo "Unknown parameter passed: $1"
print_usage
exit 1
fi
shift
;;
esac
done
if [[ -z "$SOURCE" || -z "$DEST" ]]; then
echo "Error: Both SOURCE and DEST must be provided."
print_usage
exit 1
fi
if [[ ! -e "$SOURCE" ]]; then
echo "Error: Source path '$SOURCE' does not exist."
exit 1
fi
# Function to get absolute normalized path (handles missing destination gracefully)
get_abs_path() {
python3 -c 'import os,sys; print(os.path.normpath(os.path.abspath(sys.argv[1])))' "$1" 2>/dev/null
}
# Resolve source before moving
ABS_SOURCE=$(get_abs_path "$SOURCE")
# Determine resulting target path
if [[ -d "$DEST" ]]; then
# If DEST is an existing dir, standard mv puts SOURCE inside it
BASENAME_SOURCE=$(basename "$SOURCE")
ABS_DEST=$(get_abs_path "$DEST/$BASENAME_SOURCE")
else
# Placed exactly at DEST (rename or target dir doesn't exist yet)
ABS_DEST=$(get_abs_path "$DEST")
fi
echo "Moving: $SOURCE -> $DEST"
mv "$SOURCE" "$DEST"
echo "Replacing paths in Blender's recent-files lists..."
# Find all recent-files.txt across Blender versions and replace the string
find "$HOME/Library/Application Support/Blender" -type f -name "recent-files.txt" 2>/dev/null | while read -r RECENT_FILE; do
# Using sed with alternative delimiter to avoid path interference
sed -i '' "s|${ABS_SOURCE}|${ABS_DEST}|g" "$RECENT_FILE"
done
# Prepare the embedded Python payload to update blend paths
TMP_PY=$(mktemp /tmp/blender_update_paths.XXXXXX.py)
cat << 'EOF' > "$TMP_PY"
import bpy
import sys
def replace_in_prop(prop, old_path, new_path):
if prop and isinstance(prop, str) and old_path in prop:
return prop.replace(old_path, new_path)
return prop
def run():
# Args are passed after `--`
if '--' not in sys.argv:
return
idx = sys.argv.index('--')
if len(sys.argv) <= idx + 2:
return
old_path = sys.argv[idx + 1]
new_path = sys.argv[idx + 2]
changed = False
# 1. Libraries (Linked objects/collections)
for lib in bpy.data.libraries:
new_fp = replace_in_prop(lib.filepath, old_path, new_path)
if new_fp != lib.filepath:
lib.filepath = new_fp
changed = True
# 2. Images (Textures, Env Maps)
for img in bpy.data.images:
new_fp = replace_in_prop(img.filepath, old_path, new_path)
if new_fp != img.filepath:
img.filepath = new_fp
changed = True
# 3. Cache files (Alembic/VDB)
for cache in bpy.data.cache_files:
new_fp = replace_in_prop(cache.filepath, old_path, new_path)
if new_fp != cache.filepath:
cache.filepath = new_fp
changed = True
# 4. Fonts
for font in bpy.data.fonts:
new_fp = replace_in_prop(font.filepath, old_path, new_path)
if new_fp != font.filepath:
font.filepath = new_fp
changed = True
# 5. Video strips
for scene in bpy.data.scenes:
if scene.sequence_editor:
for strip in scene.sequence_editor.sequences_all:
if strip.type in ('MOVIE', 'IMAGE'):
new_fp = replace_in_prop(strip.filepath, old_path, new_path)
if new_fp != strip.filepath:
strip.filepath = new_fp
changed = True
if changed:
print(f"[*] Updated paths in {bpy.data.filepath}")
bpy.ops.wm.save_mainfile()
try:
run()
except Exception as e:
print(f"[!] Error updating blend file paths: {e}")
EOF
# Determine what to search for blend files
if [[ -n "$SEARCH_PROJECT" ]]; then
SEARCH_ROOT=$(get_abs_path "$SEARCH_PROJECT")
echo "Scanning project root '$SEARCH_ROOT' for .blend files..."
elif [[ -d "$ABS_DEST" ]]; then
SEARCH_ROOT="$ABS_DEST"
echo "Scanning moved directory for .blend files..."
elif [[ "$ABS_DEST" == *.blend ]]; then
SEARCH_ROOT="$ABS_DEST"
echo "Updating moved .blend file..."
else
echo "No project root defined and moved object is not a directory or .blend file."
echo "Finished."
rm -f "$TMP_PY"
exit 0
fi
# Execute headless blender on all found blend files
if [[ -d "$SEARCH_ROOT" ]]; then
find "$SEARCH_ROOT" -type f -name "*.blend" -print0 | while IFS= read -r -d '' BLEND_FILE; do
blender -b "$BLEND_FILE" -P "$TMP_PY" -- "$ABS_SOURCE" "$ABS_DEST" >/dev/null 2>&1 || echo "Warning: Failed to process $BLEND_FILE"
done
elif [[ -f "$SEARCH_ROOT" && "$SEARCH_ROOT" == *.blend ]]; then
blender -b "$SEARCH_ROOT" -P "$TMP_PY" -- "$ABS_SOURCE" "$ABS_DEST" >/dev/null 2>&1 || echo "Warning: Failed to process $SEARCH_ROOT"
fi
rm -f "$TMP_PY"
echo "Success: Move complete and paths updated."

View File

@@ -0,0 +1,39 @@
services:
gatekeeper-proxy:
image: alpine:latest
command: sleep infinity
restart: unless-stopped
networks:
- infra
labels:
- "caddy=http://gatekeeper.localhost"
- "caddy.route=/*"
- "caddy.route.0_redir=/ /gatekeeper/login 302"
- "caddy.route.1_reverse_proxy=gatekeeper-app:3000"
gatekeeper-app:
image: node:20-alpine
working_dir: /app
volumes:
- .:/app
- gatekeeper_root_node_modules:/app/node_modules
- gatekeeper_pkg_node_modules:/app/packages/gatekeeper/node_modules
- gatekeeper_next_cache:/app/packages/gatekeeper/.next
- gatekeeper_pnpm_store:/pnpm
environment:
- NODE_ENV=development
- NPM_TOKEN=${NPM_TOKEN:-}
networks:
- infra
command: >
sh -c "corepack enable && pnpm config set store-dir /pnpm && pnpm install --no-frozen-lockfile && pnpm --filter @mintel/gatekeeper run dev --hostname 0.0.0.0 --port 3000"
networks:
infra:
external: true
volumes:
gatekeeper_root_node_modules:
gatekeeper_pkg_node_modules:
gatekeeper_next_cache:
gatekeeper_pnpm_store:

114
docker-compose.mcps.yml Normal file
View File

@@ -0,0 +1,114 @@
services:
qdrant:
image: qdrant/qdrant:latest
container_name: qdrant-mcp
ports:
- "6335:6333"
- "6336:6334"
volumes:
- ./data/qdrant:/qdrant/storage
restart: unless-stopped
networks:
- mcp-network
gitea-mcp:
build:
context: ./packages/gitea-mcp
container_name: gitea-mcp
env_file:
- .env
ports:
- "3011:3001"
restart: unless-stopped
networks:
- mcp-network
memory-mcp:
build:
context: ./packages/memory-mcp
container_name: memory-mcp
env_file:
- .env
ports:
- "3002:3002"
depends_on:
- qdrant
restart: unless-stopped
networks:
- mcp-network
umami-mcp:
build:
context: ./packages/umami-mcp
container_name: umami-mcp
env_file:
- .env
ports:
- "3003:3003"
restart: unless-stopped
networks:
- mcp-network
serpbear-mcp:
build:
context: ./packages/serpbear-mcp
container_name: serpbear-mcp
env_file:
- .env
ports:
- "3004:3004"
restart: unless-stopped
networks:
- mcp-network
glitchtip-mcp:
build:
context: ./packages/glitchtip-mcp
container_name: glitchtip-mcp
env_file:
- .env
ports:
- "3005:3005"
restart: unless-stopped
networks:
- mcp-network
klz-payload-mcp:
build:
context: ./packages/klz-payload-mcp
container_name: klz-payload-mcp
env_file:
- .env
ports:
- "3006:3006"
restart: unless-stopped
networks:
- mcp-network
vikunja-mcp:
build:
context: ./packages/vikunja-mcp
container_name: vikunja-mcp
env_file:
- .env
ports:
- "3007:3007"
restart: unless-stopped
networks:
- mcp-network
outline-mcp:
build:
context: ./packages/outline-mcp
container_name: outline-mcp
env_file:
- .env
ports:
- "3008:3008"
restart: unless-stopped
networks:
- mcp-network
networks:
mcp-network:
driver: bridge

69
eslint-errors-2.txt Normal file
View File

@@ -0,0 +1,69 @@

/Users/marcmintel/Projects/at-mintel/packages/gitea-mcp/src/index.ts
 11:0 error Parsing error: Identifier expected

/Users/marcmintel/Projects/at-mintel/packages/glitchtip-mcp/src/index.ts
 124:19 warning 'res' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/klz-payload-mcp/src/index.ts
 39:18 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/memory-mcp/src/qdrant.test.ts
 7:52 warning 'text' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/page-audit/src/report.ts
 7:47 warning 'PageAuditData' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 7:62 warning 'AuditIssue' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/AiFieldButton.tsx
 11:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateSlugButton.tsx
 20:21 warning 'replaceState' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 21:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateThumbnailButton.tsx
 21:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/OptimizeButton.tsx
 5:10 warning 'Button' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/mcpAdapter.ts
 44:15 warning 'toolSchema' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/memoryDb.ts
 89:31 warning 'query' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/payloadLocal.ts
 3:40 warning 'User' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/types.ts
 1:15 warning 'Plugin' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/ConceptPDF.tsx
 4:18 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:10 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/EstimationPDF.tsx
 4:18 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:10 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 54:11 warning 'getPageNum' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/InfoPDF.tsx
 5:13 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 12:5 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SharedUI.tsx
 528:5 warning 'bankData' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SimpleLayout.tsx
 4:52 warning 'PDFText' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:26 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/seo-engine/src/report.ts
 5:3 warning 'TopicCluster' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 6:3 warning 'ContentGap' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 7:3 warning 'CompetitorRanking' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

✖ 28 problems (1 error, 27 warnings)


97
eslint-errors.txt Normal file
View File

@@ -0,0 +1,97 @@

/Users/marcmintel/Projects/at-mintel/packages/gitea-mcp/src/index.ts
 12:5 warning 'Resource' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 14:10 warning 'z' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 427:30 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
 745:50 error Unnecessary escape character: \/ no-useless-escape
 745:60 error Unnecessary escape character: \/ no-useless-escape
 799:54 error Unnecessary escape character: \/ no-useless-escape
 799:64 error Unnecessary escape character: \/ no-useless-escape

/Users/marcmintel/Projects/at-mintel/packages/glitchtip-mcp/src/index.ts
 124:19 warning 'res' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/klz-payload-mcp/src/index.ts
 39:18 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/memory-mcp/src/qdrant.test.ts
 7:52 warning 'text' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/page-audit/src/report.ts
 7:47 warning 'PageAuditData' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 7:62 warning 'AuditIssue' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/chatPlugin.ts
 1:15 warning 'Config' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 10:17 error 'config' is never reassigned. Use 'const' instead prefer-const
 48:37 warning 'req' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/ChatWindow/index.tsx
 43:5 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 44:63 warning 'setMessages' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/AiFieldButton.tsx
 11:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateSlugButton.tsx
 20:21 warning 'replaceState' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 21:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateThumbnailButton.tsx
 21:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/OptimizeButton.tsx
 5:10 warning 'Button' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/endpoints/chatEndpoint.ts
 96:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 100:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/mcpAdapter.ts
 44:15 warning 'toolSchema' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 53:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/memoryDb.ts
 50:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 88:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 89:31 warning 'query' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/payloadLocal.ts
 3:40 warning 'User' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 25:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 45:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 61:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 78:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 95:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/types.ts
 1:15 warning 'Plugin' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/ConceptPDF.tsx
 4:18 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:10 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/EstimationPDF.tsx
 4:18 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:10 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 54:11 warning 'getPageNum' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/InfoPDF.tsx
 5:13 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 12:5 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SharedUI.tsx
 528:5 warning 'bankData' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SimpleLayout.tsx
 4:52 warning 'PDFText' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:26 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/seo-engine/src/report.ts
 5:3 warning 'TopicCluster' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 6:3 warning 'ContentGap' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 7:3 warning 'CompetitorRanking' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

✖ 49 problems (16 errors, 33 warnings)
 1 error and 0 warnings potentially fixable with the `--fix` option.


12
fix-private.mjs Normal file
View File

@@ -0,0 +1,12 @@
import fs from 'fs';
import glob from 'glob';
const files = glob.sync('/Users/marcmintel/Projects/at-mintel/packages/*/package.json');
files.forEach(f => {
const content = fs.readFileSync(f, 'utf8');
if (content.includes('"private": true,')) {
console.log(`Fixing ${f}`);
const newContent = content.replace(/\s*"private": true,?\n/g, '\n');
fs.writeFileSync(f, newContent);
}
});

View File

@@ -5,11 +5,19 @@
"scripts": { "scripts": {
"build": "pnpm -r build", "build": "pnpm -r build",
"dev": "pnpm -r dev", "dev": "pnpm -r dev",
"dev:gatekeeper": "bash -c 'trap \"COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml down && COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml up --build --remove-orphans'",
"dev:mcps:up": "docker-compose -f docker-compose.mcps.yml up -d --build --remove-orphans",
"dev:mcps:down": "docker-compose -f docker-compose.mcps.yml down",
"dev:mcps:watch": "pnpm -r --filter=\"./packages/*-mcp\" exec tsc -w",
"dev:mcps": "npm run dev:mcps:up && npm run dev:mcps:watch",
"start:mcps": "npm run dev:mcps:up",
"start:mcps:force": "docker-compose -f docker-compose.mcps.yml up -d --build --force-recreate --remove-orphans",
"lint": "pnpm -r --filter='./packages/**' --filter='./apps/**' 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 --",
"release:version": "bash scripts/release.sh",
"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"
@@ -23,7 +31,7 @@
"@next/eslint-plugin-next": "16.1.6", "@next/eslint-plugin-next": "16.1.6",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@types/node": "^20.17.16", "@types/node": "^20.19.33",
"@types/react": "^19.2.10", "@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.2",
@@ -34,6 +42,7 @@
"husky": "^9.1.7", "husky": "^9.1.7",
"jsdom": "^27.4.0", "jsdom": "^27.4.0",
"lint-staged": "^16.2.7", "lint-staged": "^16.2.7",
"pm2": "^6.0.14",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "^5.0.0", "typescript": "^5.0.0",
@@ -47,7 +56,7 @@
"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.9.0", "version": "1.9.17",
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"@parcel/watcher", "@parcel/watcher",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/cli", "name": "@mintel/cli",
"version": "1.9.0", "version": "1.9.17",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm" "registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"

View File

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

View File

@@ -1,7 +1,6 @@
{ {
"name": "@mintel/concept-engine", "name": "@mintel/concept-engine",
"version": "1.9.0", "version": "1.9.17",
"private": true,
"description": "AI-powered web project concept generation and analysis", "description": "AI-powered web project concept generation and analysis",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/content-engine", "name": "@mintel/content-engine",
"version": "1.9.0", "version": "1.9.17",
"private": false, "private": false,
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/eslint-config", "name": "@mintel/eslint-config",
"version": "1.9.0", "version": "1.9.17",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm" "registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"

View File

@@ -1,7 +1,6 @@
{ {
"name": "@mintel/estimation-engine", "name": "@mintel/estimation-engine",
"version": "1.9.0", "version": "1.9.17",
"private": true,
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",

View File

@@ -1,7 +1,6 @@
{ {
"name": "@mintel/gatekeeper", "name": "@mintel/gatekeeper",
"version": "1.9.0", "version": "1.9.17",
"private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
@@ -12,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@mintel/next-utils": "workspace:*", "@mintel/next-utils": "workspace:*",
"framer-motion": "^11.18.2",
"lucide-react": "^0.474.0", "lucide-react": "^0.474.0",
"next": "16.1.6", "next": "16.1.6",
"react": "^19.0.0", "react": "^19.0.0",

View File

@@ -8,7 +8,7 @@
} }
body { body {
@apply bg-white text-slate-800 font-serif antialiased selection:bg-slate-900 selection:text-white; @apply bg-[#f5f5f7] text-black/80 font-serif antialiased selection:bg-black/10 selection:text-black;
line-height: 1.6; line-height: 1.6;
} }
@@ -18,15 +18,15 @@
h4, h4,
h5, h5,
h6 { h6 {
@apply font-sans font-bold text-slate-900 tracking-tighter; @apply font-sans font-bold text-black tracking-tighter;
} }
p { p {
@apply mb-4 text-base leading-relaxed text-slate-700; @apply mb-4 text-base leading-relaxed text-black/50;
} }
a { a {
@apply text-slate-900 hover:text-slate-700 transition-colors no-underline; @apply text-black/50 hover:text-black transition-colors no-underline;
} }
} }
@@ -36,34 +36,58 @@
} }
.btn { .btn {
@apply inline-flex items-center justify-center px-6 py-3 border border-slate-200 bg-white text-slate-600 font-sans font-bold text-sm uppercase tracking-widest rounded-full transition-all duration-500 ease-industrial hover:border-slate-400 hover:text-slate-900 hover:bg-slate-50 hover:-translate-y-0.5 hover:shadow-xl hover:shadow-slate-100 active:translate-y-0 active:shadow-sm; @apply inline-flex items-center justify-center px-6 py-3 border border-black/10 bg-white text-black/60 font-sans font-bold text-sm uppercase tracking-widest rounded-full transition-all duration-500 ease-industrial hover:border-black/20 hover:text-black hover:bg-white hover:-translate-y-0.5 hover:shadow-xl hover:shadow-black/5 active:translate-y-0 active:shadow-sm;
} }
.btn-primary { .btn-primary {
@apply border-slate-900 text-slate-900 hover:bg-slate-900 hover:text-white; @apply border-black bg-black text-white hover:bg-black/85 hover:text-white;
} }
} }
/* Custom scrollbar */ /* Custom scrollbar - light theme */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 6px;
height: 8px; height: 6px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: #f1f5f9; background: #f5f5f7;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #cbd5e1; background: #d1d1d6;
border-radius: 4px; border-radius: 4px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #94a3b8; background: #b0b0b8;
} }
/* Animations */ /* Animations */
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shake { @keyframes shake {
0%, 0%,
100% { 100% {
@@ -79,6 +103,15 @@
} }
} }
.animate-fade-in {
animation: fade-in 0.8s ease-out forwards;
}
.animate-slide-up {
animation: slide-up 0.8s ease-out 0.2s forwards;
opacity: 0;
}
.animate-shake { .animate-shake {
animation: shake 0.2s ease-in-out 0s 2; animation: shake 0.2s ease-in-out 0s 2;
} }

View File

@@ -13,6 +13,17 @@ const newsreader = Newsreader({
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Gatekeeper | Access Control", title: "Gatekeeper | Access Control",
description: "Mintel Infrastructure Protection", description: "Mintel Infrastructure Protection",
openGraph: {
title: "Gatekeeper | Access Control",
description: "Mintel Infrastructure Protection",
siteName: "Mintel Gatekeeper",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Gatekeeper | Access Control",
description: "Mintel Infrastructure Protection",
},
}; };
export default function RootLayout({ export default function RootLayout({

View File

@@ -1,7 +1,8 @@
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { ArrowRight, ShieldCheck } from "lucide-react"; import { ShieldCheck } from "lucide-react";
import Image from "next/image"; import { GateScene } from "../../components/gate-scene";
import { AnimatedLoginForm } from "../../components/animated-login-form";
interface LoginPageProps { interface LoginPageProps {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
@@ -17,8 +18,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 || "").trim(); const email = ((formData.get("email") as string) || "").trim();
const password = (formData.get("password") as string || "").trim(); 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;
@@ -116,7 +117,9 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
} }
if (userIdentity) { if (userIdentity) {
console.log(`[Login] Success: ${userIdentity} | Redirect: ${targetRedirect}`); 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({
@@ -127,7 +130,9 @@ 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"}`); console.log(
`[Login] Setting Cookie: ${authCookieName} | Domain: ${cookieDomain || "Default"}`,
);
cookieStore.set(authCookieName, sessionValue, { cookieStore.set(authCookieName, sessionValue, {
httpOnly: true, httpOnly: true,
@@ -145,101 +150,79 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
} }
return ( return (
<div className="min-h-screen flex items-center justify-center relative bg-white font-serif antialiased overflow-hidden"> <div className="min-h-screen flex items-center justify-center relative bg-[#f5f5f7] font-serif antialiased overflow-hidden selection:bg-black/10 selection:text-black">
{/* Background Decor - Signature mintel.me style */} {/* 3D Digital Gate Background */}
<div <GateScene />
className="absolute inset-0 pointer-events-none opacity-[0.03] scale-[1.01]"
style={{
backgroundImage: `linear-gradient(to right, #000 1px, transparent 1px), linear-gradient(to bottom, #000 1px, transparent 1px)`,
backgroundSize: "clamp(30px, 8vw, 40px) clamp(30px, 8vw, 40px)",
}}
/>
<main className="relative z-10 w-full max-w-sm px-8 sm:px-6"> <main className="relative z-10 w-full max-w-[380px] px-6 sm:px-4 pb-24 sm:pb-32 pointer-events-auto">
<div className="space-y-12 sm:space-y-16 animate-fade-in"> <div className="space-y-10 animate-fade-in">
{/* Top Icon Box - Signature mintel.me Black Square */} {/* Top Icon Box */}
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-16 h-16 bg-black rounded-xl flex items-center justify-center shadow-xl shadow-slate-100 hover:scale-105 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] rotate-2 hover:rotate-0"> <a
<Image href="https://mintel.me"
target="_blank"
rel="noopener noreferrer"
className="w-14 h-14 bg-white rounded-2xl flex items-center justify-center shadow-lg shadow-black/[0.06] hover:scale-105 hover:shadow-black/10 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] border border-black/[0.06] hover:border-black/10"
>
<img
src="/gatekeeper/icon-white.svg" src="/gatekeeper/icon-white.svg"
alt="Mintel" alt="Mintel"
width={32} style={{ filter: "invert(1)" }}
height={32} width={28}
className="w-8 h-8" height={28}
unoptimized className="w-7 h-7 opacity-80"
/> />
</div> </a>
</div> </div>
<div className="space-y-12 animate-slide-up"> <div className="space-y-8 animate-slide-up">
<div className="text-center space-y-4"> <div className="text-center space-y-3">
<h1 className="text-xs font-sans font-bold uppercase tracking-[0.4em] text-slate-900 border-b border-slate-50 pb-4 inline-block mx-auto min-w-[200px]"> <h1 className="text-[11px] font-sans font-bold uppercase tracking-[0.5em] text-black/80 pb-3 inline-block mx-auto min-w-[220px]">
{projectName} <span className="text-slate-300">Gatekeeper</span> {projectName} <span className="text-black/30">Gatekeeper</span>
</h1> </h1>
<p className="text-[10px] text-slate-400 font-sans uppercase tracking-widest italic flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-4">
<span className="w-1 h-1 bg-slate-200 rounded-full" /> <div className="h-px w-8 bg-gradient-to-r from-transparent to-black/10" />
<p className="text-[8px] text-black/30 font-sans uppercase tracking-[0.35em] font-semibold">
Infrastructure Protection Infrastructure Protection
<span className="w-1 h-1 bg-slate-200 rounded-full" />
</p> </p>
<div className="h-px w-8 bg-gradient-to-l from-transparent to-black/10" />
</div>
</div> </div>
{error && ( {error && (
<div className="bg-red-50 text-red-600 px-5 py-3 rounded-2xl text-[9px] font-sans font-bold uppercase tracking-widest flex items-center gap-3 border border-red-100 animate-shake"> <div className="bg-red-50 backdrop-blur-md text-red-600 px-5 py-4 rounded-2xl text-[9px] font-sans font-bold uppercase tracking-widest flex items-center gap-3 border border-red-200 animate-shake">
<ShieldCheck className="w-4 h-4" /> <ShieldCheck className="w-4 h-4 text-red-500/70" />
<span>Access Denied. Try Again.</span> <span>Access Denied. Try Again.</span>
</div> </div>
)} )}
<form action={login} className="space-y-4"> {/* The Animated Framer Motion Form */}
<input type="hidden" name="redirect" value={redirectUrl} /> <AnimatedLoginForm
redirectUrl={redirectUrl}
<div className="space-y-2"> loginAction={login}
<div className="relative group"> projectName={projectName}
<input
type="email"
name="email"
placeholder="EMAIL (OPTIONAL)"
className="w-full bg-slate-50/50 border border-slate-200 rounded-2xl px-6 py-4 focus:outline-none focus:border-slate-900 focus:bg-white transition-all text-[10px] font-sans font-bold tracking-[0.2em] uppercase placeholder:text-slate-300 shadow-sm shadow-slate-50"
/> />
</div>
<div className="relative group"> {/* Bottom Section */}
<input <div className="pt-4 sm:pt-6 flex flex-col items-center gap-5">
type="password" <div className="h-px w-16 bg-gradient-to-r from-transparent via-black/10 to-transparent" />
name="password" <a
required href="https://mintel.me"
autoFocus target="_blank"
autoComplete="current-password" rel="noopener noreferrer"
placeholder="ACCESS CODE" className="opacity-30 transition-opacity hover:opacity-60"
className="w-full bg-slate-50/50 border border-slate-200 rounded-2xl px-6 py-4 focus:outline-none focus:border-slate-900 focus:bg-white transition-all text-sm font-sans font-bold tracking-[0.3em] uppercase placeholder:text-slate-300 placeholder:tracking-widest shadow-sm shadow-slate-50"
/>
</div>
</div>
<button
type="submit"
className="btn btn-primary w-full py-5 rounded-2xl text-[10px] shadow-lg shadow-slate-100 flex items-center justify-center"
> >
Unlock Access <img
<ArrowRight className="ml-3 w-3 h-3 group-hover:translate-x-1 transition-transform" /> src="/gatekeeper/logo-white.svg"
</button>
</form>
{/* Bottom Section - Full Branding Parity */}
<div className="pt-12 sm:pt-20 flex flex-col items-center gap-6 sm:gap-8">
<div className="h-px w-8 bg-slate-100" />
<div className="opacity-80 transition-opacity hover:opacity-100">
<Image
src="/gatekeeper/logo-black.svg"
alt={projectName} alt={projectName}
width={140} width={120}
height={40} height={36}
className="h-7 sm:h-auto grayscale contrast-125 w-auto" className="h-5 sm:h-6 w-auto"
unoptimized style={{ filter: "invert(1)" }}
/> />
</div> </a>
<p className="text-[8px] font-sans font-bold text-slate-300 uppercase tracking-[0.4em] sm:tracking-[0.5em] text-center"> <p className="text-[7px] font-sans font-semibold text-black/25 uppercase tracking-[0.5em] text-center">
&copy; 2026 MINTEL &copy; {new Date().getFullYear()} MINTEL
</p> </p>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,126 @@
import { ImageResponse } from "next/og";
export const runtime = "edge";
// Image metadata
export const alt = "Gatekeeper Infrastructure Protection";
export const size = {
width: 1200,
height: 630,
};
export const contentType = "image/png";
export default async function Image() {
const projectName = process.env.PROJECT_NAME || "MINTEL";
return new ImageResponse(
<div
style={{
background: "linear-gradient(to bottom, #020617, #0f172a)",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
position: "relative",
fontFamily: "Inter, sans-serif",
}}
>
{/* Subtle Background Pattern matching the industrial look */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundImage:
"radial-gradient(circle at 50% 50%, #334155 1px, transparent 1px)",
backgroundSize: "40px 40px",
opacity: 0.1,
}}
/>
{/* Central Card Element */}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "rgba(15, 23, 42, 0.6)",
border: "1px solid rgba(51, 65, 85, 0.4)",
borderRadius: "32px",
padding: "80px",
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
}}
>
{/* Top Icon Box */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100px",
height: "100px",
background: "#000",
borderRadius: "24px",
border: "2px solid #334155",
boxShadow: "0 20px 40px rgba(0,0,0,0.8)",
marginBottom: "40px",
transform: "rotate(2deg)",
}}
>
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
</div>
{/* Project Name & Typography */}
<div
style={{
display: "flex",
fontSize: "64px",
fontWeight: 900,
letterSpacing: "0.2em",
color: "white",
textTransform: "uppercase",
marginBottom: "16px",
}}
>
{projectName}{" "}
<span style={{ color: "#64748b", marginLeft: "10px" }}>
GATEKEEPER
</span>
</div>
<div
style={{
display: "flex",
fontSize: "24px",
fontWeight: 600,
letterSpacing: "0.4em",
color: "#94a3b8",
textTransform: "uppercase",
}}
>
Infrastructure Protection
</div>
</div>
</div>,
{
...size,
},
);
}

View File

@@ -0,0 +1,283 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { motion } from "framer-motion";
import { ArrowRight, Lock, Shield, Fingerprint } from "lucide-react";
interface AnimatedLoginFormProps {
redirectUrl: string;
loginAction: (formData: FormData) => Promise<void>;
projectName: string;
}
export function AnimatedLoginForm({
redirectUrl,
loginAction,
}: AnimatedLoginFormProps) {
const [isFocused, setIsFocused] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [mounted, setMounted] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const beamRef = useRef<HTMLDivElement>(null);
// Mouse tracking refs (no re-render)
const mouse = useRef({ x: 0, y: 0 });
const angle = useRef(0);
const tilt = useRef({ x: 0, y: 0 });
useEffect(() => {
setMounted(true);
}, []);
// Single rAF loop: iridescent border + perspective tilt
useEffect(() => {
let animId: number;
const onMouseMove = (e: MouseEvent) => {
mouse.current = { x: e.clientX, y: e.clientY };
};
window.addEventListener("mousemove", onMouseMove);
const animate = () => {
if (!wrapperRef.current || !beamRef.current) {
animId = requestAnimationFrame(animate);
return;
}
const rect = wrapperRef.current.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const dx = mouse.current.x - cx;
const dy = mouse.current.y - cy;
// Angle from form center to mouse → positions the bright highlight
const targetAngle = (Math.atan2(dy, dx) * 180) / Math.PI;
// Lerp angle smoothly (shortest path)
let diff = targetAngle - angle.current;
while (diff > 180) diff -= 360;
while (diff < -180) diff += 360;
angle.current += diff * 0.06;
// Intensity: slightly stronger on focus
const intensity = isFocused ? 1 : 0.7;
// Mouse-aligned iridescent conic gradient
// The "hotspot" (brightest white) faces the mouse
beamRef.current.style.background = `conic-gradient(from ${angle.current}deg at 50% 50%,
rgba(255,255,255,${1.0 * intensity}) 0deg,
rgba(200,210,255,${0.8 * intensity}) 20deg,
rgba(255,200,230,${0.7 * intensity}) 45deg,
rgba(150,160,180,${0.6 * intensity}) 80deg,
rgba(40,40,50,${0.5 * intensity}) 160deg,
rgba(20,20,30,${0.4 * intensity}) 200deg,
rgba(140,150,170,${0.5 * intensity}) 280deg,
rgba(210,225,255,${0.7 * intensity}) 320deg,
rgba(255,255,255,${1.0 * intensity}) 360deg)`;
// Subtle perspective tilt — max ±4deg
const maxTilt = 4;
const normX = dx / (rect.width * 2);
const normY = dy / (rect.height * 2);
const targetTiltY = normX * maxTilt;
const targetTiltX = -normY * maxTilt;
tilt.current.x += (targetTiltX - tilt.current.x) * 0.08;
tilt.current.y += (targetTiltY - tilt.current.y) * 0.08;
wrapperRef.current.style.transform = `perspective(800px) rotateX(${tilt.current.x}deg) rotateY(${tilt.current.y}deg)`;
animId = requestAnimationFrame(animate);
};
animId = requestAnimationFrame(animate);
return () => {
window.removeEventListener("mousemove", onMouseMove);
cancelAnimationFrame(animId);
};
}, [isFocused]);
const handleSubmit = async (formData: FormData) => {
setIsSubmitting(true);
try {
await loginAction(formData);
} finally {
setIsSubmitting(false);
}
};
return (
<motion.div
initial={{ opacity: 0, y: 30, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ duration: 0.8, ease: [0.23, 1, 0.32, 1], delay: 0.3 }}
className="relative"
style={{ willChange: "transform" }}
>
{/* Outer wrapper for tilt (refs need non-motion div) */}
<div
ref={wrapperRef}
style={{ willChange: "transform", transformStyle: "preserve-3d" }}
className="relative"
>
{/* ── Always-on iridescent beam border ── */}
<div className="absolute -inset-[1.5px] rounded-[28px] z-0 overflow-hidden">
{/* Sharp edge layer */}
<div
ref={beamRef}
className="absolute inset-0 rounded-[28px]"
style={{ filter: "blur(0.4px)" }}
/>
{/* Soft glow bloom */}
<div
className="absolute inset-0 rounded-[28px]"
style={{
background: "inherit",
filter: "blur(15px)",
opacity: 0.5,
}}
/>
</div>
{/* ── Glassmorphism card — high-fidelity glossy light ── */}
<div
className="relative z-10 bg-white/70 backdrop-blur-3xl rounded-3xl p-7 sm:p-9"
style={{
boxShadow:
/* Razor-sharp inner border highlight */ "inset 0 0 0 1px rgba(255,255,255,0.7), " +
/* Top gloss edge */ "inset 0 1.5px 0.5px rgba(255,255,255,1), " +
/* Secondary soft top gloss */ "inset 0 4px 10px rgba(255,255,255,0.4), " +
/* Bottom inner shadow */ "inset 0 -1px 1px rgba(0,0,0,0.05), " +
/* Outer drop shadows for depth */ "0 25px 50px -12px rgba(0,0,0,0.08), 0 4px 8px rgba(0,0,0,0.02)",
}}
>
{/* Subtle surface "sheen" gradient */}
<div className="absolute inset-0 rounded-3xl bg-gradient-to-br from-white/40 via-transparent to-black/[0.02] pointer-events-none" />
{/* Shield icon header */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.5, duration: 0.6 }}
className="flex justify-center mb-6"
>
<div className="relative">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-black/[0.04] to-black/[0.08] border border-black/[0.06] flex items-center justify-center backdrop-blur-sm">
<Shield className="w-5 h-5 text-black/40" />
</div>
{mounted && (
<div className="absolute -inset-2 rounded-2xl border border-black/[0.04] animate-ping opacity-30" />
)}
</div>
</motion.div>
<form
action={handleSubmit}
className="space-y-5 relative z-10"
onFocus={() => setIsFocused(true)}
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setIsFocused(false);
}
}}
>
<input type="hidden" name="redirect" value={redirectUrl} />
<div className="space-y-3">
{/* Email Input */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.6, duration: 0.5 }}
className="relative group"
>
<Fingerprint className="absolute left-4 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-black/20 group-focus-within:text-black/50 transition-colors duration-300 pointer-events-none" />
<input
type="email"
name="email"
placeholder="Identity (optional)"
onFocus={() => setIsFocused(true)}
className="w-full bg-black/[0.02] border border-black/[0.06] rounded-2xl pl-11 pr-4 py-3.5 focus:outline-none focus:border-black/15 focus:bg-white/80 transition-all duration-300 text-[11px] font-sans font-medium tracking-[0.08em] placeholder:text-black/25 placeholder:normal-case placeholder:tracking-normal text-black/70"
/>
<div className="absolute bottom-0 left-4 right-4 h-px bg-gradient-to-r from-transparent via-black/0 to-transparent group-focus-within:via-black/10 transition-all duration-500" />
</motion.div>
{/* Password Input */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.7, duration: 0.5 }}
className="relative group"
>
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-black/20 group-focus-within:text-black/50 transition-colors duration-300 pointer-events-none" />
<input
type="password"
name="password"
required
autoFocus
autoComplete="current-password"
placeholder="Access code"
onFocus={() => setIsFocused(true)}
className="w-full bg-black/[0.02] border border-black/[0.06] rounded-2xl pl-11 pr-4 py-3.5 focus:outline-none focus:border-black/15 focus:bg-white/80 transition-all duration-300 text-[13px] font-sans font-medium tracking-[0.15em] placeholder:text-black/25 placeholder:tracking-normal placeholder:text-[11px] placeholder:font-normal text-black/80"
/>
<div className="absolute bottom-0 left-4 right-4 h-px bg-gradient-to-r from-transparent via-black/0 to-transparent group-focus-within:via-black/10 transition-all duration-500" />
</motion.div>
</div>
{/* Submit Button */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8, duration: 0.5 }}
>
<motion.button
type="submit"
disabled={isSubmitting}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.98 }}
className={`relative w-full py-4 rounded-2xl text-[10px] font-bold tracking-[0.3em] uppercase flex items-center justify-center overflow-hidden transition-all duration-300 ${
isSubmitting
? "bg-black/5 text-black/25 cursor-not-allowed"
: "bg-black text-white hover:bg-black/85 shadow-lg shadow-black/10"
}`}
>
{!isSubmitting && (
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full hover:translate-x-full transition-transform duration-700" />
)}
{isSubmitting ? (
<span className="flex items-center gap-3 relative z-10">
<motion.div
animate={{ rotate: 360 }}
transition={{
repeat: Infinity,
duration: 1,
ease: "linear",
}}
className="w-4 h-4 border-2 border-white/20 border-t-white/70 rounded-full"
/>
Authenticating...
</span>
) : (
<span className="flex items-center gap-3 relative z-10">
Unlock Access
<ArrowRight className="w-4 h-4" />
</span>
)}
</motion.button>
</motion.div>
</form>
{/* Security badge */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.0, duration: 0.5 }}
className="mt-5 flex items-center justify-center gap-2 text-[8px] font-sans font-semibold text-black/25 uppercase tracking-[0.3em]"
>
<div className="w-1 h-1 rounded-full bg-black/20 animate-pulse" />
Encrypted Connection
</motion.div>
</div>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,127 @@
"use client";
import { useEffect, useRef } from "react";
export function GateScene() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const mouse = useRef({ x: -1000, y: -1000 });
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const FONT_SIZE = 13;
const COL_GAP = 18;
const ROW_GAP = 20;
interface Cell {
char: string;
alpha: number;
targetAlpha: number;
speed: number;
nextChange: number;
}
let cells: Cell[][] = [];
let cols = 0;
let rows = 0;
let animId: number;
const init = () => {
cols = Math.ceil(canvas.width / COL_GAP);
rows = Math.ceil(canvas.height / ROW_GAP);
cells = Array.from({ length: cols }, () =>
Array.from({ length: rows }, () => ({
char: Math.random() > 0.5 ? "1" : "0",
alpha: Math.random() * 0.08,
targetAlpha: Math.random() * 0.15,
speed: 0.008 + Math.random() * 0.02,
nextChange: Math.floor(Math.random() * 80),
})),
);
};
const onMouseMove = (e: MouseEvent) => {
mouse.current = { x: e.clientX, y: e.clientY };
};
const resize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
init();
};
let frame = 0;
const draw = () => {
frame++;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = `${FONT_SIZE}px 'Courier New', monospace`;
for (let c = 0; c < cols; c++) {
for (let r = 0; r < rows; r++) {
const cell = cells[c][r];
const x = c * COL_GAP;
const y = r * ROW_GAP + FONT_SIZE;
// Mouse proximity influence
const dx = mouse.current.x - x;
const dy = mouse.current.y - y;
const distSq = dx * dx + dy * dy;
const proximity = Math.max(0, 1 - Math.sqrt(distSq) / 250);
// Nudge alpha toward target
cell.alpha += (cell.targetAlpha - cell.alpha) * cell.speed;
// More aggressive random behavior
if (frame >= cell.nextChange) {
cell.targetAlpha = Math.random() * 0.25; // Higher max alpha
cell.speed = 0.01 + Math.random() * 0.03; // Faster transitions
cell.nextChange = frame + 20 + Math.floor(Math.random() * 80); // More frequent changes
// Higher flip probability near mouse
const flipProb = 0.3 + proximity * 0.5;
if (Math.random() < flipProb) {
cell.char = cell.char === "0" ? "1" : "0";
}
}
const a = Math.min(0.4, cell.alpha + proximity * 0.35);
if (a < 0.01) continue;
// Dark chars on light background
ctx.fillStyle = `rgba(0, 0, 0, ${a})`;
ctx.fillText(cell.char, x, y);
}
}
animId = requestAnimationFrame(draw);
};
resize();
window.addEventListener("resize", resize);
window.addEventListener("mousemove", onMouseMove);
animId = requestAnimationFrame(draw);
return () => {
cancelAnimationFrame(animId);
window.removeEventListener("resize", resize);
window.removeEventListener("mousemove", onMouseMove);
};
}, []);
return (
<div className="absolute inset-0 pointer-events-none z-0 bg-[#f5f5f7]">
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
<div
className="absolute inset-0 pointer-events-none"
style={{
background:
"radial-gradient(ellipse 55% 65% at 50% 50%, rgba(245,245,247,0.7) 0%, transparent 100%)",
}}
/>
</div>
);
}

View File

@@ -0,0 +1,19 @@
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
RUN npm install -g pnpm@10.18.3 && pnpm install
COPY tsconfig.json ./
COPY src ./src
RUN pnpm build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
# Use node to run the compiled start.js
ENTRYPOINT ["node", "dist/start.js"]

View File

@@ -0,0 +1,22 @@
{
"name": "@mintel/gitea-mcp",
"version": "1.9.17",
"description": "Native Gitea MCP server for 100% Antigravity compatibility",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/start.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.5.0",
"axios": "^1.7.2",
"dotenv": "^17.3.1",
"express": "^4.19.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.14.10",
"typescript": "^5.5.3"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
import { config } from 'dotenv';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
// Try to load .env.local first (contains credentials usually)
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
// Fallback to .env (contains defaults)
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
// Now boot the compiled MCP index
import('./index.js').catch(err => {
console.error('Failed to start MCP Server:', err);
process.exit(1);
});

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*"
]
}

View File

@@ -0,0 +1,15 @@
FROM node:20-bookworm-slim AS builder
WORKDIR /app
COPY package.json ./
RUN npm install -g pnpm@10.18.3 && pnpm install --ignore-workspace
COPY tsconfig.json ./
COPY src ./src
RUN pnpm build
FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
ENTRYPOINT ["node", "dist/start.js"]

View File

@@ -0,0 +1,24 @@
{
"name": "@mintel/glitchtip-mcp",
"version": "1.9.17",
"description": "GlitchTip Error Tracking MCP server for Mintel infrastructure",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/start.js",
"dev": "tsx watch src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.5.0",
"axios": "^1.7.2",
"dotenv": "^17.3.1",
"express": "^4.19.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.14.10",
"tsx": "^4.19.2",
"typescript": "^5.5.3"
}
}

View File

@@ -0,0 +1,219 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express, { Request, Response } from 'express';
import crypto from 'crypto';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import https from "https";
const GLITCHTIP_BASE_URL = process.env.GLITCHTIP_BASE_URL || "https://glitchtip.infra.mintel.me";
const GLITCHTIP_API_KEY = process.env.GLITCHTIP_API_KEY;
if (!GLITCHTIP_API_KEY) {
console.error("Warning: GLITCHTIP_API_KEY is not set. API calls will fail.");
}
const httpsAgent = new https.Agent({
rejectUnauthorized: false, // For internal infra
});
const glitchtipClient = axios.create({
baseURL: `${GLITCHTIP_BASE_URL}/api/0`,
headers: { Authorization: `Bearer ${GLITCHTIP_API_KEY}` },
httpsAgent,
timeout: 10000 // Add timeout to prevent hangs
});
// Logging middleware for GlitchTip client
glitchtipClient.interceptors.request.use((config) => {
console.error(`GlitchTip Request: ${config.method?.toUpperCase()} ${config.url}`);
return config;
}, (error) => {
console.error(`GlitchTip Request Error: ${error.message}`);
return Promise.reject(error);
});
glitchtipClient.interceptors.response.use((response) => {
return response;
}, (error) => {
const msg = error.response?.data?.detail || error.response?.data?.message || error.message;
console.error(`GlitchTip API Error (${error.config?.url}): ${msg}`);
return Promise.reject(error);
});
const LIST_PROJECTS_TOOL: Tool = {
name: "glitchtip_list_projects",
description: "List all projects and organizations in GlitchTip",
inputSchema: { type: "object", properties: {} },
};
const LIST_ISSUES_TOOL: Tool = {
name: "glitchtip_list_issues",
description: "List issues (errors) for a specific project",
inputSchema: {
type: "object",
properties: {
organization_slug: { type: "string", description: "The organization slug" },
project_slug: { type: "string", description: "The project slug" },
query: { type: "string", description: "Optional query filter (e.g., 'is:unresolved')" },
limit: { type: "number", description: "Maximum number of issues to return (default: 20)" },
},
required: ["organization_slug", "project_slug"],
},
};
const GET_ISSUE_DETAILS_TOOL: Tool = {
name: "glitchtip_get_issue_details",
description: "Get detailed information about a specific issue, including stack trace",
inputSchema: {
type: "object",
properties: {
issue_id: { type: "string", description: "The ID of the issue" },
},
required: ["issue_id"],
},
};
const UPDATE_ISSUE_TOOL: Tool = {
name: "glitchtip_update_issue",
description: "Update the status of an issue (e.g., resolve it)",
inputSchema: {
type: "object",
properties: {
issue_id: { type: "string", description: "The ID of the issue" },
status: { type: "string", enum: ["resolved", "unresolved", "ignored"], description: "The new status" },
},
required: ["issue_id", "status"],
},
};
const server = new Server(
{ name: "glitchtip-mcp", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
LIST_PROJECTS_TOOL,
LIST_ISSUES_TOOL,
GET_ISSUE_DETAILS_TOOL,
UPDATE_ISSUE_TOOL,
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
if (request.params.name === "glitchtip_list_projects") {
const res = await glitchtipClient.get('/projects/');
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
}
if (request.params.name === "glitchtip_list_issues") {
const { organization_slug, project_slug, query, limit = 20 } = request.params.arguments as any;
const res = await glitchtipClient.get(`/projects/${organization_slug}/${project_slug}/issues/`, {
params: { query, limit }
});
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
}
if (request.params.name === "glitchtip_get_issue_details") {
const { issue_id } = request.params.arguments as any;
const res = await glitchtipClient.get(`/issues/${issue_id}/`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
}
if (request.params.name === "glitchtip_update_issue") {
const { issue_id, status } = request.params.arguments as any;
const res = await glitchtipClient.put(`/issues/${issue_id}/`, { status });
return { content: [{ type: "text", text: `Issue ${issue_id} status updated to ${status}.` }] };
}
throw new Error(`Unknown tool: ${request.params.name}`);
} catch (e: any) {
const msg = e.response?.data?.detail || e.response?.data?.message || e.message;
return { isError: true, content: [{ type: "text", text: `GlitchTip Service Error: ${msg}` }] };
}
});
async function run() {
const isStdio = process.argv.includes('--stdio');
if (isStdio) {
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('GlitchTip MCP server is running on stdio');
} else {
const app = express();
const transports = new Map<string, SSEServerTransport>();
// Manual CORS middleware
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-mcp-protocol-version');
if (req.method === 'OPTIONS') {
res.status(204).end();
return;
}
next();
});
app.use((req, _res, next) => {
console.error(`${req.method} ${req.url}`);
next();
});
app.get('/sse', async (req, res) => {
const transport = new SSEServerTransport('/message', res as any);
const sessionId = transport.sessionId;
console.error(`New SSE connection: ${sessionId}`);
transports.set(sessionId, transport);
const heartbeatInterval = setInterval(() => {
res.write(": heartbeat\n\n");
}, 15000);
req.on('close', () => {
console.error(`SSE connection closed: ${sessionId}`);
clearInterval(heartbeatInterval);
transports.delete(sessionId);
});
await server.connect(transport);
await new Promise((resolve) => {
req.on('close', resolve);
});
});
app.post('/message', express.json(), async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
console.error(`POST /message/${sessionId} - Active transports: ${transports.size}`);
if (!transport) {
console.error(`No transport found for session: ${sessionId}`);
res.status(400).send('No active SSE connection for this session');
return;
}
await transport.handlePostMessage(req, res);
});
const PORT = Number(process.env.GLITCHTIP_MCP_PORT) || 3005;
app.listen(PORT, '0.0.0.0', () => {
console.error(`GlitchTip MCP server running on http://0.0.0.0:${PORT}/sse`);
});
}
}
run().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});

View File

@@ -0,0 +1,13 @@
import { config } from 'dotenv';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
import('./index.js').catch(err => {
console.error('Failed to start GlitchTip MCP Server:', err);
process.exit(1);
});

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*"
]
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/husky-config", "name": "@mintel/husky-config",
"version": "1.9.0", "version": "1.9.17",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm" "registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"

View File

@@ -1,5 +1,5 @@
# Start from the pre-built Nextjs Base image # Start from the pre-built Nextjs Base image
FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder FROM git.infra.mintel.me/mmintel/nextjs:latest AS builder
WORKDIR /app WORKDIR /app
@@ -20,7 +20,7 @@ ENV DIRECTUS_URL=$DIRECTUS_URL
RUN pnpm --filter ${APP_NAME:-app} build RUN pnpm --filter ${APP_NAME:-app} build
# Production runner image # Production runner image
FROM registry.infra.mintel.me/mintel/runtime:latest AS runner FROM git.infra.mintel.me/mmintel/runtime:latest AS runner
WORKDIR /app WORKDIR /app
# Copy standalone output and static files # Copy standalone output and static files

View File

@@ -13,7 +13,7 @@ COPY packages/eslint-config/package.json ./packages/eslint-config/package.json
COPY packages/next-config/package.json ./packages/next-config/package.json COPY packages/next-config/package.json ./packages/next-config/package.json
COPY packages/tsconfig/package.json ./packages/tsconfig/package.json COPY packages/tsconfig/package.json ./packages/tsconfig/package.json
COPY packages/infra/package.json ./packages/infra/package.json COPY packages/infra/package.json ./packages/infra/package.json
COPY packages/cms-infra/package.json ./packages/cms-infra/package.json
COPY packages/mail/package.json ./packages/mail/package.json COPY packages/mail/package.json ./packages/mail/package.json
COPY packages/cli/package.json ./packages/cli/package.json 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

View File

@@ -0,0 +1,2 @@
service:
allowiconchanges: false

View File

@@ -38,7 +38,7 @@ services:
- "traefik.http.middlewares.${PROJECT_NAME:-app}-auth.forwardauth.authResponseHeaders=X-Auth-User" - "traefik.http.middlewares.${PROJECT_NAME:-app}-auth.forwardauth.authResponseHeaders=X-Auth-User"
gatekeeper: gatekeeper:
image: registry.infra.mintel.me/mintel/gatekeeper:${IMAGE_TAG:-latest} image: git.infra.mintel.me/mmintel/gatekeeper:${IMAGE_TAG:-latest}
restart: always restart: always
networks: networks:
- infra - infra
@@ -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:${IMAGE_TAG:-latest} image: git.infra.mintel.me/mmintel/directus:${IMAGE_TAG:-latest}
restart: always restart: always
networks: networks:
- infra - infra

View File

@@ -0,0 +1,42 @@
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: vikunja
POSTGRES_PASSWORD: mintel_vikunja_DB_2026!
POSTGRES_DB: vikunja
volumes:
- ./data/db:/var/lib/postgresql/data
restart: always
vikunja:
image: vikunja/vikunja:latest
environment:
VIKUNJA_DATABASE_HOST: db
VIKUNJA_DATABASE_PASSWORD: mintel_vikunja_DB_2026!
VIKUNJA_DATABASE_TYPE: postgres
VIKUNJA_DATABASE_USER: vikunja
VIKUNJA_DATABASE_DATABASE: vikunja
VIKUNJA_SERVICE_PUBLICURL: https://tasks.infra.mintel.me/
VIKUNJA_SERVICE_ENABLEREGISTRATION: true
VIKUNJA_SERVICE_ALLOWICONCHANGES: false
VIKUNJA_SERVICE_JWTSECRET: d2FzaHViZXJzZWN1cmVqd3RzZWNyZXQyMDI2IQ==
volumes:
- ./data/files:/app/vikunja/files
- ./config.yml:/etc/vikunja/config.yml:ro
depends_on:
- db
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.vikunja.rule=Host(`tasks.infra.mintel.me`)"
- "traefik.http.routers.vikunja.entrypoints=websecure"
- "traefik.http.routers.vikunja.tls.certresolver=le"
- "traefik.http.services.vikunja.loadbalancer.server.port=3456"
networks:
- default
- infra
networks:
infra:
external: true

View File

@@ -177,12 +177,31 @@ jobs:
- name: 🐳 Set up Docker Buildx - name: 🐳 Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: 🔐 Registry Login - name: 🔐 Discover Valid Registry Token
uses: docker/login-action@v3 id: discover_token
with: run: |
registry: registry.infra.mintel.me echo "Testing available secrets against git.infra.mintel.me Docker registry..."
username: ${{ secrets.REGISTRY_USER }} TOKENS="${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}"
password: ${{ secrets.REGISTRY_PASS }} USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel"
for TOKEN in $TOKENS; do
if [ -n "$TOKEN" ]; then
for U in $USERS; do
if [ -n "$U" ]; then
echo "Attempting docker login for a token with user $U..."
if echo "$TOKEN" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then
echo "✅ Successfully authenticated with a token."
echo "::add-mask::$TOKEN"
echo "token=$TOKEN" >> $GITHUB_OUTPUT
echo "user=$U" >> $GITHUB_OUTPUT
exit 0
fi
fi
done
fi
done
echo "❌ All available tokens failed to authenticate!"
exit 1
- name: 🏗️ Docker Build & Push - name: 🏗️ Docker Build & Push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
@@ -197,8 +216,8 @@ jobs:
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
push: true push: true
secrets: | secrets: |
NPM_TOKEN=${{ secrets.NPM_TOKEN }} NPM_TOKEN=${{ steps.discover_token.outputs.token }}
tags: registry.infra.mintel.me/mintel/${{ github.event.repository.name }}:${{ needs.prepare.outputs.image_tag }} tags: git.infra.mintel.me/mmintel/${{ github.event.repository.name }}:${{ needs.prepare.outputs.image_tag }}
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# JOB 4: Deploy # JOB 4: Deploy
@@ -262,7 +281,7 @@ jobs:
set -e set -e
cd "/home/deploy/sites/${{ github.event.repository.name }}" cd "/home/deploy/sites/${{ github.event.repository.name }}"
chmod 600 "$ENV_FILE" chmod 600 "$ENV_FILE"
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin echo "${{ steps.discover_token.outputs.token }}" | docker login git.infra.mintel.me -u "${{ steps.discover_token.outputs.user }}" --password-stdin
docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" pull docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" pull
docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" up -d --remove-orphans docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" up -d --remove-orphans
docker system prune -f --filter "until=24h" docker system prune -f --filter "until=24h"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/infra", "name": "@mintel/infra",
"version": "1.9.0", "version": "1.9.17",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm" "registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"

View File

@@ -2,12 +2,24 @@
set -e set -e
# Configuration # Configuration
REGISTRY_DATA="/mnt/HC_Volume_104575103/registry-data/docker/registry/v2" REGISTRY_DATA="/mnt/HC_Volume_104796416/registry-data/docker/registry/v2"
KEEP_TAGS=3 KEEP_TAGS=3
echo "🏥 Starting Aggressive Mintel Infrastructure Optimization..." echo "🏥 Starting Aggressive Mintel Infrastructure Optimization..."
# 1. Prune Registry Tags (Filesystem level) # 1. Gitea Maintenance
echo "🍵 Running Gitea Maintenance..."
GITEA_CONTAINER=$(docker ps --format "{{.Names}}" | grep gitea | head -1 || true)
if [ -n "$GITEA_CONTAINER" ]; then
# Run common Gitea cleanup tasks
docker exec -u git "$GITEA_CONTAINER" gitea admin cron run cleanup_old_repository_archives || true
docker exec -u git "$GITEA_CONTAINER" gitea admin cron run cleanup_upload_directory || true
docker exec -u git "$GITEA_CONTAINER" gitea admin cron run cleanup_packages || true
docker exec -u git "$GITEA_CONTAINER" gitea admin cron run garbage_collect_attachment || true
docker exec -u git "$GITEA_CONTAINER" gitea admin cron run garbage_collect_lfs || true
fi
# 2. Prune Registry Tags (Filesystem level)
if [ -d "$REGISTRY_DATA" ]; then if [ -d "$REGISTRY_DATA" ]; then
echo "🔍 Processing Registry tags..." echo "🔍 Processing Registry tags..."
for repo_dir in "$REGISTRY_DATA/repositories/mintel/"*; do for repo_dir in "$REGISTRY_DATA/repositories/mintel/"*; do
@@ -44,7 +56,17 @@ fi
# 3. Global Docker Pruning # 3. Global Docker Pruning
echo "🧹 Pruning Docker resources..." echo "🧹 Pruning Docker resources..."
docker system prune -af --filter "until=24h" docker system prune -af --filter "until=24h"
echo "🧹 Pruning BuildKit builder containers and cache..."
# Remove orphaned buildkit containers to release volume locks
docker rm -f $(docker ps -aq --filter name=buildx_buildkit_builder) 2>/dev/null || true
docker builder prune -af || true
docker buildx prune -af || true
echo "🧹 Pruning Docker volumes..."
docker volume prune -f docker volume prune -f
# Force remove dangling buildx state volumes that might bypass standard prune
docker volume ls -q | grep buildx | xargs -r docker volume rm 2>/dev/null || true
echo "✅ Optimization complete!" echo "✅ Optimization complete!"
df -h /mnt/HC_Volume_104575103 df -h /mnt/HC_Volume_104796416

View File

@@ -1,8 +1,14 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Configuration # Find registry data dynamically (volume ID might change)
REGISTRY_DATA="/mnt/HC_Volume_104575103/registry-data/docker/registry/v2" REGISTRY_DATA=$(find /mnt -maxdepth 5 -type d -path '*/registry-data/docker/registry/v2' | head -n 1)
if [ -z "$REGISTRY_DATA" ]; then
echo "❌ Registry data directory not found!"
exit 1
fi
KEEP_TAGS=3 KEEP_TAGS=3
echo "🏥 Starting Aggressive Registry & Docker Maintenance..." echo "🏥 Starting Aggressive Registry & Docker Maintenance..."

View File

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

View File

@@ -0,0 +1,15 @@
FROM node:20-bookworm-slim AS builder
WORKDIR /app
COPY package.json ./
RUN npm install -g pnpm@10.18.3 && pnpm install --ignore-workspace
COPY tsconfig.json ./
COPY src ./src
RUN pnpm build
FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
ENTRYPOINT ["node", "dist/start.js"]

View File

@@ -0,0 +1,24 @@
{
"name": "@mintel/klz-payload-mcp",
"version": "1.9.17",
"description": "KLZ PayloadCMS MCP server for technical product data and leads",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/start.js",
"dev": "tsx watch src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.5.0",
"axios": "^1.7.2",
"dotenv": "^17.3.1",
"express": "^4.19.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.14.10",
"tsx": "^4.19.2",
"typescript": "^5.5.3"
}
}

View File

@@ -0,0 +1,653 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express, { Request, Response } from 'express';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import crypto from "crypto";
import https from "https";
const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://klz-cables.com";
const PAYLOAD_API_KEY = process.env.PAYLOAD_API_KEY;
const PAYLOAD_EMAIL = process.env.PAYLOAD_EMAIL || "agent@mintel.me";
const PAYLOAD_PASSWORD = process.env.PAYLOAD_PASSWORD || "agentpassword123";
const httpsAgent = new https.Agent({
rejectUnauthorized: false, // For internal infra
});
let jwtToken: string | null = null;
const payloadClient = axios.create({
baseURL: `${PAYLOAD_URL}/api`,
headers: PAYLOAD_API_KEY ? { Authorization: `users API-Key ${PAYLOAD_API_KEY}` } : {},
httpsAgent
});
payloadClient.interceptors.request.use(async (config) => {
if (!PAYLOAD_API_KEY && !jwtToken && PAYLOAD_EMAIL && PAYLOAD_PASSWORD) {
try {
const loginRes = await axios.post(`${PAYLOAD_URL}/api/users/login`, {
email: PAYLOAD_EMAIL,
password: PAYLOAD_PASSWORD
}, { httpsAgent });
if (loginRes.data && loginRes.data.token) {
jwtToken = loginRes.data.token;
}
} catch (e) {
console.error("Failed to authenticate with Payload CMS using email/password.");
}
}
if (jwtToken && !PAYLOAD_API_KEY) {
config.headers.Authorization = `JWT ${jwtToken}`;
}
return config;
});
payloadClient.interceptors.response.use(res => res, async (error) => {
const originalRequest = error.config;
// If token expired, clear it and retry
if (error.response?.status === 401 && !originalRequest._retry && !PAYLOAD_API_KEY) {
originalRequest._retry = true;
jwtToken = null; // Forces re-authentication on next interceptor run
return payloadClient(originalRequest);
}
return Promise.reject(error);
});
const SEARCH_PRODUCTS_TOOL: Tool = {
name: "payload_search_products",
description: "Search for technical product specifications (cables, cross-sections) in KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query or part number" },
limit: { type: "number", description: "Maximum number of results" },
},
},
};
const GET_PRODUCT_TOOL: Tool = {
name: "payload_get_product",
description: "Get a specific product by its slug or ID",
inputSchema: {
type: "object",
properties: {
slug: { type: "string", description: "Product slug" },
id: { type: "string", description: "Product ID (if slug is not used)" }
},
},
};
const CREATE_PRODUCT_TOOL: Tool = {
name: "payload_create_product",
description: "Create a new product in KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Product title" },
slug: { type: "string", description: "Product slug" },
data: { type: "object", description: "Additional product data (JSON)", additionalProperties: true }
},
required: ["title"]
},
};
const UPDATE_PRODUCT_TOOL: Tool = {
name: "payload_update_product",
description: "Update an existing product in KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Product ID to update" },
data: { type: "object", description: "Product data to update (JSON)", additionalProperties: true }
},
required: ["id", "data"]
},
};
const DELETE_PRODUCT_TOOL: Tool = {
name: "payload_delete_product",
description: "Delete a product from KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Product ID to delete" }
},
required: ["id"]
},
};
const LIST_LEADS_TOOL: Tool = {
name: "payload_list_leads",
description: "List recent lead inquiries and contact requests",
inputSchema: {
type: "object",
properties: {
limit: { type: "number", description: "Maximum number of leads" },
},
},
};
const GET_LEAD_TOOL: Tool = {
name: "payload_get_lead",
description: "Get a specific lead by ID",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Lead ID" }
},
required: ["id"]
},
};
const CREATE_LEAD_TOOL: Tool = {
name: "payload_create_lead",
description: "Create a new lead in KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
email: { type: "string", description: "Lead email address" },
data: { type: "object", description: "Additional lead data (JSON)", additionalProperties: true }
},
required: ["email"]
},
};
const UPDATE_LEAD_TOOL: Tool = {
name: "payload_update_lead",
description: "Update an existing lead in KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Lead ID to update" },
data: { type: "object", description: "Lead data to update (JSON)", additionalProperties: true }
},
required: ["id", "data"]
},
};
const DELETE_LEAD_TOOL: Tool = {
name: "payload_delete_lead",
description: "Delete a lead from KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Lead ID to delete" }
},
required: ["id"]
},
};
const LIST_PAGES_TOOL: Tool = {
name: "payload_list_pages",
description: "List pages from KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
limit: { type: "number", description: "Maximum number of pages" },
},
},
};
const GET_PAGE_TOOL: Tool = {
name: "payload_get_page",
description: "Get a specific page by its slug or ID",
inputSchema: {
type: "object",
properties: {
slug: { type: "string", description: "Page slug" },
id: { type: "string", description: "Page ID (if slug is not used)" }
},
},
};
const LIST_POSTS_TOOL: Tool = {
name: "payload_list_posts",
description: "List posts/articles from KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
limit: { type: "number", description: "Maximum number of posts" },
},
},
};
const GET_POST_TOOL: Tool = {
name: "payload_get_post",
description: "Get a specific post by its slug or ID",
inputSchema: {
type: "object",
properties: {
slug: { type: "string", description: "Post slug" },
id: { type: "string", description: "Post ID (if slug is not used)" }
},
},
};
const CREATE_PAGE_TOOL: Tool = {
name: "payload_create_page",
description: "Create a new page in KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Page title" },
slug: { type: "string", description: "Page slug" },
data: { type: "object", description: "Additional page data (JSON)", additionalProperties: true }
},
required: ["title"]
},
};
const UPDATE_PAGE_TOOL: Tool = {
name: "payload_update_page",
description: "Update an existing page in KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Page ID to update" },
data: { type: "object", description: "Page data to update (JSON)", additionalProperties: true }
},
required: ["id", "data"]
},
};
const DELETE_PAGE_TOOL: Tool = {
name: "payload_delete_page",
description: "Delete a page from KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Page ID to delete" }
},
required: ["id"]
},
};
const CREATE_POST_TOOL: Tool = {
name: "payload_create_post",
description: "Create a new post in KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Post title" },
slug: { type: "string", description: "Post slug" },
data: { type: "object", description: "Additional post data (JSON)", additionalProperties: true }
},
required: ["title"]
},
};
const UPDATE_POST_TOOL: Tool = {
name: "payload_update_post",
description: "Update an existing post in KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Post ID to update" },
data: { type: "object", description: "Post data to update (JSON)", additionalProperties: true }
},
required: ["id", "data"]
},
};
const DELETE_POST_TOOL: Tool = {
name: "payload_delete_post",
description: "Delete a post from KLZ Payload CMS",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Post ID to delete" }
},
required: ["id"]
},
};
const server = new Server(
{ name: "klz-payload-mcp", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
SEARCH_PRODUCTS_TOOL,
GET_PRODUCT_TOOL,
CREATE_PRODUCT_TOOL,
UPDATE_PRODUCT_TOOL,
DELETE_PRODUCT_TOOL,
LIST_LEADS_TOOL,
GET_LEAD_TOOL,
CREATE_LEAD_TOOL,
UPDATE_LEAD_TOOL,
DELETE_LEAD_TOOL,
LIST_PAGES_TOOL,
GET_PAGE_TOOL,
CREATE_PAGE_TOOL,
UPDATE_PAGE_TOOL,
DELETE_PAGE_TOOL,
LIST_POSTS_TOOL,
GET_POST_TOOL,
CREATE_POST_TOOL,
UPDATE_POST_TOOL,
DELETE_POST_TOOL
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "payload_search_products") {
const { query, limit = 10 } = request.params.arguments as any;
try {
const res = await payloadClient.get('/products', {
params: {
where: query ? {
or: [
{ title: { contains: query } },
{ slug: { contains: query } },
{ description: { contains: query } }
]
} : {},
limit
}
});
return { content: [{ type: "text", text: JSON.stringify(res.data.docs, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] };
}
}
if (request.params.name === "payload_get_product") {
const { slug, id } = request.params.arguments as any;
try {
if (id) {
const res = await payloadClient.get(`/products/${id}`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} else if (slug) {
const res = await payloadClient.get('/products', { params: { where: { slug: { equals: slug } }, limit: 1 } });
return { content: [{ type: "text", text: JSON.stringify(res.data.docs[0] || {}, null, 2) }] };
}
return { isError: true, content: [{ type: "text", text: "Error: must provide slug or id" }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] };
}
}
if (request.params.name === "payload_create_product") {
const { title, slug, data = {} } = request.params.arguments as any;
try {
const payload = { title, slug, _status: 'draft', ...data };
const res = await payloadClient.post('/products', payload);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] };
}
}
if (request.params.name === "payload_update_product") {
const { id, data } = request.params.arguments as any;
try {
const res = await payloadClient.patch(`/products/${id}`, data);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] };
}
}
if (request.params.name === "payload_delete_product") {
const { id } = request.params.arguments as any;
try {
const res = await payloadClient.delete(`/products/${id}`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] };
}
}
if (request.params.name === "payload_list_leads") {
const { limit = 10 } = request.params.arguments as any;
try {
const res = await payloadClient.get('/leads', {
params: { limit, sort: '-createdAt' }
});
return { content: [{ type: "text", text: JSON.stringify(res.data.docs, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] };
}
}
if (request.params.name === "payload_get_lead") {
const { id } = request.params.arguments as any;
try {
const res = await payloadClient.get(`/leads/${id}`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] };
}
}
if (request.params.name === "payload_create_lead") {
const { email, data = {} } = request.params.arguments as any;
try {
const payload = { email, ...data };
const res = await payloadClient.post('/leads', payload);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] };
}
}
if (request.params.name === "payload_update_lead") {
const { id, data } = request.params.arguments as any;
try {
const res = await payloadClient.patch(`/leads/${id}`, data);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] };
}
}
if (request.params.name === "payload_delete_lead") {
const { id } = request.params.arguments as any;
try {
const res = await payloadClient.delete(`/leads/${id}`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] };
}
}
if (request.params.name === "payload_list_pages") {
const { limit = 10 } = request.params.arguments as any;
try {
const res = await payloadClient.get('/pages', { params: { limit } });
return { content: [{ type: "text", text: JSON.stringify(res.data.docs, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] };
}
}
if (request.params.name === "payload_get_page") {
const { slug, id } = request.params.arguments as any;
try {
if (id) {
const res = await payloadClient.get(`/pages/${id}`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} else if (slug) {
const res = await payloadClient.get('/pages', { params: { where: { slug: { equals: slug } }, limit: 1 } });
return { content: [{ type: "text", text: JSON.stringify(res.data.docs[0] || {}, null, 2) }] };
}
return { isError: true, content: [{ type: "text", text: "Error: must provide slug or id" }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] };
}
}
if (request.params.name === "payload_create_page") {
const { title, slug, data = {} } = request.params.arguments as any;
try {
const payload = { title, slug, _status: 'draft', ...data };
const res = await payloadClient.post('/pages', payload);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] };
}
}
if (request.params.name === "payload_update_page") {
const { id, data } = request.params.arguments as any;
try {
const res = await payloadClient.patch(`/pages/${id}`, data);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] };
}
}
if (request.params.name === "payload_delete_page") {
const { id } = request.params.arguments as any;
try {
const res = await payloadClient.delete(`/pages/${id}`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] };
}
}
if (request.params.name === "payload_list_posts") {
const { limit = 10 } = request.params.arguments as any;
try {
const res = await payloadClient.get('/posts', { params: { limit, sort: '-createdAt' } });
return { content: [{ type: "text", text: JSON.stringify(res.data.docs, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] };
}
}
if (request.params.name === "payload_get_post") {
const { slug, id } = request.params.arguments as any;
try {
if (id) {
const res = await payloadClient.get(`/posts/${id}`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} else if (slug) {
const res = await payloadClient.get('/posts', { params: { where: { slug: { equals: slug } }, limit: 1 } });
return { content: [{ type: "text", text: JSON.stringify(res.data.docs[0] || {}, null, 2) }] };
}
return { isError: true, content: [{ type: "text", text: "Error: must provide slug or id" }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] };
}
}
if (request.params.name === "payload_create_post") {
const { title, slug, data = {} } = request.params.arguments as any;
try {
const payload = { title, slug, _status: 'draft', ...data };
const res = await payloadClient.post('/posts', payload);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] };
}
}
if (request.params.name === "payload_update_post") {
const { id, data } = request.params.arguments as any;
try {
const res = await payloadClient.patch(`/posts/${id}`, data);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] };
}
}
if (request.params.name === "payload_delete_post") {
const { id } = request.params.arguments as any;
try {
const res = await payloadClient.delete(`/posts/${id}`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] };
}
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
async function run() {
const isStdio = process.argv.includes('--stdio');
if (isStdio) {
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('KLZ Payload MCP server is running on stdio');
} else {
const app = express();
const transports = new Map<string, SSEServerTransport>();
// Manual CORS middleware
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-mcp-protocol-version');
if (req.method === 'OPTIONS') {
res.status(204).end();
return;
}
next();
});
app.use((req, _res, next) => {
console.error(`${req.method} ${req.url}`);
next();
});
app.get('/sse', async (req, res) => {
const transport = new SSEServerTransport('/message', res as any);
const sessionId = transport.sessionId;
console.error(`New SSE connection: ${sessionId}`);
transports.set(sessionId, transport);
req.on('close', () => {
console.error(`SSE connection closed: ${sessionId}`);
transports.delete(sessionId);
});
await server.connect(transport);
await new Promise((resolve) => {
req.on('close', resolve);
});
});
app.post('/message', express.json(), async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
console.error(`POST /message/${sessionId} - Active transports: ${transports.size}`);
if (!transport) {
console.error(`No transport found for session: ${sessionId}`);
res.status(400).send('No active SSE connection for this session');
return;
}
await transport.handlePostMessage(req, res);
});
const PORT = Number(process.env.KLZ_PAYLOAD_MCP_PORT) || 3006;
app.listen(PORT, '0.0.0.0', () => {
console.error(`KLZ Payload MCP server running on http://0.0.0.0:${PORT}/sse`);
});
}
}
run().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});

View File

@@ -0,0 +1,13 @@
import { config } from 'dotenv';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
import('./index.js').catch(err => {
console.error('Failed to start KLZ Payload MCP Server:', err);
process.exit(1);
});

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*"
]
}

View File

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

View File

@@ -25,3 +25,4 @@ export * from "./templates/ConfirmationMessage";
export * from "./templates/FollowUpTemplate"; export * from "./templates/FollowUpTemplate";
export * from "./templates/ProjectEstimateTemplate"; export * from "./templates/ProjectEstimateTemplate";
export * from "./templates/SiteAuditTemplate"; export * from "./templates/SiteAuditTemplate";
export * from "./templates/AnalyticsTemplate";

View File

@@ -0,0 +1,197 @@
import {
Body,
Container,
Head,
Html,
Preview,
Section,
Text,
Img,
} from "@react-email/components";
import * as React from "react";
export interface GatekeeperLayoutProps {
preview: string;
projectName?: string;
subTitle?: string;
children: React.ReactNode;
}
export const GatekeeperLayout = ({ preview, projectName = "MINTEL", subTitle = "INFRASTRUCTURE", children }: GatekeeperLayoutProps) => {
return (
<Html>
<Head />
<Preview>{preview}</Preview>
<Body style={main}>
<Container style={container}>
{/* Top Icon Box */}
<Section style={iconBoxSection}>
<div style={iconBox}>
<Img
src="https://mintel.me/favicon-32x32.png"
width={28}
height={28}
alt="Mintel"
style={iconImg}
/>
</div>
</Section>
{/* Title Area */}
<Section style={titleSection}>
<Text style={gatekeeperTitle}>
{projectName} <span style={{ color: "rgba(0,0,0,0.3)" }}>GATEKEEPER</span>
</Text>
<table align="center" width="100%" border={0} cellPadding={0} cellSpacing={0} role="presentation">
<tbody>
<tr>
<td width="30%" align="right"><div style={lineGradientRight} /></td>
<td width="40%" align="center">
<Text style={gatekeeperSub}>
{subTitle}
</Text>
</td>
<td width="30%" align="left"><div style={lineGradientLeft} /></td>
</tr>
</tbody>
</table>
</Section>
{/* Main Content Area */}
<Section style={contentSection}>
{children}
</Section>
{/* Footer Area */}
<Section style={footerSection}>
<div style={lineGradientCenter} />
<div style={{ marginTop: "24px" }}>
<Img
src="https://mintel.me/favicon-32x32.png"
width={24}
height={24}
alt={projectName}
style={{ opacity: 0.3, margin: "0 auto", display: "block" }}
/>
</div>
<Text style={footerText}>
&copy; {new Date().getFullYear()} MINTEL
</Text>
</Section>
</Container>
</Body>
</Html>
);
};
const main = {
backgroundColor: "#f5f5f7",
color: "#111111",
fontFamily:
'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
WebkitFontSmoothing: "antialiased" as const,
};
const container = {
margin: "0 auto",
padding: "40px 20px 80px",
maxWidth: "420px",
};
const iconBoxSection = {
textAlign: "center" as const,
marginBottom: "40px",
};
const iconBox = {
width: "56px",
height: "56px",
backgroundColor: "#ffffff",
borderRadius: "16px",
margin: "0 auto",
display: "flex",
alignItems: "center",
justifyContent: "center",
border: "1px solid rgba(0,0,0,0.06)",
boxShadow: "0 10px 15px -3px rgba(0,0,0,0.06), 0 4px 6px -2px rgba(0,0,0,0.03)",
};
const iconImg = {
margin: "14px auto",
display: "block",
opacity: 0.8,
filter: "grayscale(100%) brightness(0)",
};
const titleSection = {
textAlign: "center" as const,
marginBottom: "32px",
};
const gatekeeperTitle = {
fontSize: "11px",
fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif',
fontWeight: "bold",
textTransform: "uppercase" as const,
letterSpacing: "0.5em",
color: "rgba(0,0,0,0.8)",
margin: "0 0 12px 0",
};
const gatekeeperSub = {
fontSize: "8px",
fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif',
textTransform: "uppercase" as const,
letterSpacing: "0.35em",
fontWeight: 600,
color: "rgba(0,0,0,0.3)",
margin: "0",
};
const lineGradientRight = {
height: "1px",
width: "32px",
backgroundColor: "rgba(0,0,0,0.1)",
marginLeft: "auto",
};
const lineGradientLeft = {
height: "1px",
width: "32px",
backgroundColor: "rgba(0,0,0,0.1)",
marginRight: "auto",
};
const contentSection = {
backgroundColor: "#ffffff",
padding: "32px",
borderRadius: "24px",
border: "1px solid rgba(0,0,0,0.06)",
boxShadow: "0 4px 6px -1px rgba(0,0,0,0.03)",
marginBottom: "32px",
};
const footerSection = {
textAlign: "center" as const,
paddingTop: "24px",
};
const lineGradientCenter = {
height: "1px",
width: "64px",
backgroundColor: "rgba(0,0,0,0.1)",
margin: "0 auto",
};
const footerText = {
fontSize: "7px",
fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif',
fontWeight: 600,
color: "rgba(0,0,0,0.25)",
textTransform: "uppercase" as const,
letterSpacing: "0.5em",
marginTop: "20px",
};

View File

@@ -0,0 +1,41 @@
import { render } from "@react-email/components";
import { describe, it, expect } from "vitest";
import { AnalyticsTemplate, PeriodData } from "./AnalyticsTemplate";
import * as React from "react";
describe("AnalyticsTemplate", () => {
it("renders entry and exit pages correctly", async () => {
const period: PeriodData = {
label: "Test Period",
visitors: 100,
visits: 120,
views: 500,
bounceRate: 50,
visitDuration: "2m",
topPages: [{ url: "/home", views: 50 }],
topReferrers: [{ url: "google.com", visitors: 30 }],
// These fields will cause a TS error because they don't exist yet,
// but we force it to see it fail.
topEntryPages: [{ url: "/entry", visitors: 40 }],
topExitPages: [{ url: "/exit", visitors: 20 }],
chartData: [],
} as any; // Cast as any to bypass TS compilation before we update the type
const html = await render(
<AnalyticsTemplate
greeting="Hallo Kunde,"
domain="example.com"
periods={[period]}
/>
);
// Assert that the new sections for entry and exit pages exist.
expect(html).toContain("Häufigste Einstiege");
expect(html).toContain("Häufigste Ausstiege");
expect(html).toContain("/entry");
expect(html).toContain("/exit");
// Assert that the badges have updated line heights
expect(html).toContain('line-height:1.5');
});
});

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/meme-generator", "name": "@mintel/meme-generator",
"version": "1.9.0", "version": "1.9.17",
"private": false, "private": false,
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -0,0 +1,18 @@
FROM node:20-bookworm-slim AS builder
WORKDIR /app
COPY package.json ./
RUN npm install -g pnpm@10.18.3 && pnpm install --ignore-workspace
RUN for dir in $(find /app/node_modules -type d -name "sharp" | grep "node_modules/sharp$"); do \
echo "module.exports = {};" > "$dir/lib/index.js" || true; \
done
COPY tsconfig.json ./
COPY src ./src
RUN pnpm build
FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
ENTRYPOINT ["node", "dist/start.js"]

View File

@@ -0,0 +1,29 @@
{
"name": "@mintel/memory-mcp",
"version": "1.9.17",
"description": "Local Qdrant-based Memory MCP server",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/start.js",
"dev": "tsx watch src/index.ts",
"test:unit": "vitest run"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.5.0",
"@qdrant/js-client-rest": "^1.12.0",
"@xenova/transformers": "^2.17.2",
"onnxruntime-node": "^1.14.0",
"dotenv": "^17.3.1",
"express": "^4.19.2",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.14.10",
"tsx": "^4.19.1",
"typescript": "^5.5.3",
"vitest": "^2.1.3"
}
}

View File

@@ -0,0 +1,153 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import express from 'express';
import crypto from 'crypto';
import { z } from 'zod';
import { QdrantMemoryService } from './qdrant.js';
async function main() {
const server = new McpServer({
name: '@mintel/memory-mcp',
version: '1.0.0',
});
const qdrantService = new QdrantMemoryService(process.env.QDRANT_URL || 'http://localhost:6333');
server.tool(
'store_memory',
'Store a new piece of knowledge/memory into the vector database. Use this to remember architectural decisions, preferences, aliases, etc.',
{
label: z.string().describe('A short, descriptive label or title for the memory (e.g., "Architektur-Entscheidungen")'),
content: z.string().describe('The actual content to remember (e.g., "In diesem Projekt nutzen wir lieber Composition over Inheritance.")'),
},
async (args) => {
const success = await qdrantService.storeMemory(args.label, args.content);
if (success) {
return {
content: [{ type: 'text', text: `Successfully stored memory: [${args.label}]` }],
};
} else {
return {
content: [{ type: 'text', text: `Failed to store memory: [${args.label}]` }],
isError: true,
};
}
}
);
server.tool(
'retrieve_memory',
'Retrieve relevant memories from the vector database based on a semantic search query.',
{
query: z.string().describe('The search query to find relevant memories.'),
limit: z.number().optional().describe('Maximum number of results to return (default: 5)'),
},
async (args) => {
const results = await qdrantService.retrieveMemory(args.query, args.limit || 5);
if (results.length === 0) {
return {
content: [{ type: 'text', text: 'No relevant memories found.' }],
};
}
const formattedResults = results
.map(r => `- [${r.label}] (Score: ${r.score.toFixed(3)}): ${r.content}`)
.join('\n');
return {
content: [{ type: 'text', text: `Found ${results.length} memories:\n\n${formattedResults}` }],
};
}
);
const isStdio = process.argv.includes('--stdio');
if (isStdio) {
// Connect Stdio FIRST to avoid handshake timeouts while loading model
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Memory MCP server is running on stdio');
// Initialize dependency after connection
try {
await qdrantService.initialize();
} catch (e) {
console.error('Failed to initialize local dependencies:', e);
}
} else {
const app = express();
const transports = new Map<string, SSEServerTransport>();
// Manual CORS middleware
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-mcp-protocol-version');
if (req.method === 'OPTIONS') {
res.status(204).end();
return;
}
next();
});
app.use((req, _res, next) => {
console.error(`${req.method} ${req.url}`);
next();
});
app.get('/sse', async (req, res) => {
const transport = new SSEServerTransport('/message', res as any);
const sessionId = transport.sessionId;
console.error(`New SSE connection: ${sessionId}`);
transports.set(sessionId, transport);
const heartbeatInterval = setInterval(() => {
res.write(": heartbeat\n\n");
}, 15000);
req.on('close', () => {
console.error(`SSE connection closed: ${sessionId}`);
clearInterval(heartbeatInterval);
transports.delete(sessionId);
});
await server.connect(transport);
await new Promise((resolve) => {
req.on('close', resolve);
});
});
app.post('/message', express.json(), async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
console.error(`POST /message/${sessionId} - Active transports: ${transports.size}`);
if (!transport) {
console.error(`No transport found for session: ${sessionId}`);
res.status(400).send('No active SSE connection for this session');
return;
}
await transport.handlePostMessage(req, res);
});
const PORT = Number(process.env.MEMORY_MCP_PORT) || 3002;
app.listen(PORT, '0.0.0.0', async () => {
console.error(`Memory MCP server running on http://0.0.0.0:${PORT}/sse`);
// Initialize dependencies in SSE mode on startup
try {
await qdrantService.initialize();
} catch (e) {
console.error('Failed to initialize local dependencies:', e);
}
});
}
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

View File

@@ -0,0 +1,89 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { QdrantMemoryService } from './qdrant.js';
vi.mock('@xenova/transformers', () => {
return {
env: { allowRemoteModels: false, localModelPath: './models' },
pipeline: vi.fn().mockResolvedValue(async (text: string) => {
// Mock embedding generation: returns an array of 384 numbers
return { data: new Float32Array(384).fill(0.1) };
}),
};
});
const mockCreateCollection = vi.fn();
const mockGetCollections = vi.fn().mockResolvedValue({ collections: [] });
const mockUpsert = vi.fn();
const mockSearch = vi.fn().mockResolvedValue([
{
id: 'test-id',
version: 1,
score: 0.9,
payload: { label: 'Test Label', content: 'Test Content' }
}
]);
vi.mock('@qdrant/js-client-rest', () => {
return {
QdrantClient: vi.fn().mockImplementation(() => {
return {
getCollections: mockGetCollections,
createCollection: mockCreateCollection,
upsert: mockUpsert,
search: mockSearch
};
})
};
});
describe('QdrantMemoryService', () => {
let service: QdrantMemoryService;
beforeEach(() => {
vi.clearAllMocks();
service = new QdrantMemoryService('http://localhost:6333');
});
it('should initialize and create collection if missing', async () => {
mockGetCollections.mockResolvedValueOnce({ collections: [] });
await service.initialize();
expect(mockGetCollections).toHaveBeenCalled();
expect(mockCreateCollection).toHaveBeenCalledWith('mcp_memory', expect.any(Object));
});
it('should not create collection if it already exists', async () => {
mockGetCollections.mockResolvedValueOnce({ collections: [{ name: 'mcp_memory' }] });
await service.initialize();
expect(mockCreateCollection).not.toHaveBeenCalled();
});
it('should store memory', async () => {
await service.initialize();
const result = await service.storeMemory('Design', 'Composition over Inheritance');
expect(result).toBe(true);
expect(mockUpsert).toHaveBeenCalledWith('mcp_memory', expect.objectContaining({
wait: true,
points: expect.arrayContaining([
expect.objectContaining({
payload: expect.objectContaining({
label: 'Design',
content: 'Composition over Inheritance'
})
})
])
}));
});
it('should retrieve memory', async () => {
await service.initialize();
const results = await service.retrieveMemory('Design');
expect(results).toHaveLength(1);
expect(results[0].label).toBe('Test Label');
expect(results[0].content).toBe('Test Content');
expect(results[0].score).toBe(0.9);
});
});

View File

@@ -0,0 +1,110 @@
import { pipeline, env } from '@xenova/transformers';
import { QdrantClient } from '@qdrant/js-client-rest';
// Be sure to set local caching options for transformers
env.allowRemoteModels = true;
env.localModelPath = './models';
export class QdrantMemoryService {
private client: QdrantClient;
private collectionName = 'mcp_memory';
private embedder: any = null;
constructor(url: string = 'http://localhost:6333') {
this.client = new QdrantClient({ url });
}
/**
* Initializes the embedding model and the Qdrant collection
*/
async initialize() {
// 1. Load the embedding model (using a lightweight model suitable for semantic search)
console.error('Loading embedding model...');
this.embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
// 2. Ensure collection exists
console.error(`Checking for collection: ${this.collectionName}`);
try {
const collections = await this.client.getCollections();
const exists = collections.collections.some(c => c.name === this.collectionName);
if (!exists) {
console.error(`Creating collection: ${this.collectionName}`);
await this.client.createCollection(this.collectionName, {
vectors: {
size: 384, // size for all-MiniLM-L6-v2
distance: 'Cosine'
}
});
console.error('Collection created successfully.');
}
} catch (e) {
console.error('Failed to initialize Qdrant collection:', e);
throw e;
}
}
/**
* Generates a vector embedding for the given text
*/
private async getEmbedding(text: string): Promise<number[]> {
if (!this.embedder) {
throw new Error('Embedder not initialized. Call initialize() first.');
}
const output = await this.embedder(text, { pooling: 'mean', normalize: true });
return Array.from(output.data);
}
/**
* Stores a memory entry into Qdrant
*/
async storeMemory(label: string, content: string): Promise<boolean> {
try {
const fullText = `${label}: ${content}`;
const vector = await this.getEmbedding(fullText);
const id = crypto.randomUUID();
await this.client.upsert(this.collectionName, {
wait: true,
points: [
{
id,
vector,
payload: {
label,
content,
timestamp: new Date().toISOString()
}
}
]
});
return true;
} catch (e) {
console.error('Failed to store memory:', e);
return false;
}
}
/**
* Retrieves memory entries relevant to the query
*/
async retrieveMemory(query: string, limit: number = 5): Promise<Array<{ label: string, content: string, score: number }>> {
try {
const vector = await this.getEmbedding(query);
const searchResults = await this.client.search(this.collectionName, {
vector,
limit,
with_payload: true
});
return searchResults.map(result => ({
label: String(result.payload?.label || ''),
content: String(result.payload?.content || ''),
score: result.score
}));
} catch (e) {
console.error('Failed to retrieve memory:', e);
return [];
}
}
}

View File

@@ -0,0 +1,16 @@
import { config } from 'dotenv';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
// Try to load .env.local first (contains credentials usually)
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
// Fallback to .env (contains defaults)
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
// Now boot the compiled MCP index
import('./index.js').catch(err => {
console.error('Failed to start MCP Server:', err);
process.exit(1);
});

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*"
]
}

View File

@@ -53,6 +53,31 @@ const withMintelConfig = (config) => {
let nextConfig = { ...baseNextConfig, ...config }; let nextConfig = { ...baseNextConfig, ...config };
// Explicitly merge rewrites
nextConfig.rewrites = async () => {
const defaultRewrites = await baseNextConfig.rewrites();
let customRewrites = {};
if (typeof config.rewrites === 'function') {
customRewrites = await config.rewrites();
} else if (Array.isArray(config.rewrites)) {
customRewrites = config.rewrites;
}
if (Array.isArray(customRewrites)) {
return {
beforeFiles: [...customRewrites, ...defaultRewrites],
afterFiles: [],
fallback: [],
};
}
return {
beforeFiles: [...(customRewrites.beforeFiles || []), ...defaultRewrites],
afterFiles: customRewrites.afterFiles || [],
fallback: customRewrites.fallback || [],
};
};
if (hasI18nConfig) { if (hasI18nConfig) {
const withNextIntl = createNextIntlPlugin(); const withNextIntl = createNextIntlPlugin();
nextConfig = withNextIntl(nextConfig); nextConfig = withNextIntl(nextConfig);
@@ -71,3 +96,4 @@ const withMintelConfig = (config) => {
}; };
export default withMintelConfig; export default withMintelConfig;

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/next-config", "name": "@mintel/next-config",
"version": "1.9.0", "version": "1.9.17",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm" "registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/next-feedback", "name": "@mintel/next-feedback",
"version": "1.9.0", "version": "1.9.17",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm" "registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/next-observability", "name": "@mintel/next-observability",
"version": "1.9.0", "version": "1.9.17",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm" "registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/next-utils", "name": "@mintel/next-utils",
"version": "1.9.0", "version": "1.9.17",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm" "registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/observability", "name": "@mintel/observability",
"version": "1.9.0", "version": "1.9.17",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm" "registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"

View File

@@ -0,0 +1,15 @@
FROM node:20-bookworm-slim AS builder
WORKDIR /app
COPY package.json ./
RUN npm install -g pnpm@10.18.3 && pnpm install --ignore-workspace
COPY tsconfig.json ./
COPY src ./src
RUN pnpm build
FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
ENTRYPOINT ["node", "dist/start.js"]

View File

@@ -0,0 +1,26 @@
{
"name": "@mintel/outline-mcp",
"version": "1.0.0",
"description": "Outline MCP server for Mintel infrastructure",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/start.js",
"dev": "tsx watch src/index.ts",
"test:unit": "vitest run"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.5.0",
"axios": "^1.7.2",
"dotenv": "^17.3.1",
"express": "^4.19.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.14.10",
"tsx": "^4.19.2",
"typescript": "^5.5.3",
"vitest": "^2.1.3"
}
}

View File

@@ -0,0 +1,91 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import axios from 'axios';
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
// Mock environment
process.env.OUTLINE_API_TOKEN = 'test-token';
process.env.OUTLINE_BASE_URL = 'https://outline.test';
// Mock axios
vi.mock('axios', () => {
const mockPost = vi.fn();
return {
default: {
create: vi.fn(() => ({
post: mockPost
}))
}
};
});
describe('Outline MCP Server Tools', () => {
let mockPost: any;
let requestHandler: any;
beforeEach(async () => {
vi.clearAllMocks();
// Extract the mock
const axiosInstance = axios.create();
mockPost = axiosInstance.post;
// Mock Server setRequestHandler
let capturedHandler: any;
vi.spyOn(Server.prototype, 'setRequestHandler').mockImplementation((schema: any, handler: any) => {
// The first call is usually ListToolsRequestSchema, second is CallToolRequestSchema
// We just store the last one which is CallToolRequestSchema
capturedHandler = handler;
return {} as any;
});
// Re-import to trigger registration
vi.resetModules();
await import('./index.js');
requestHandler = capturedHandler;
});
it('should search documents', async () => {
mockPost.mockResolvedValueOnce({ data: { data: [{ id: '1', title: 'Test' }] } });
const result = await requestHandler({
params: {
name: 'outline_search_documents',
arguments: { query: 'test' }
}
});
expect(mockPost).toHaveBeenCalledWith('/api/documents.search', { query: 'test', collectionId: undefined });
expect(result.content[0].text).toContain('Test');
});
it('should get a document', async () => {
mockPost.mockResolvedValueOnce({ data: { data: { id: 'doc1', title: 'Doc 1' } } });
const result = await requestHandler({
params: {
name: 'outline_get_document',
arguments: { id: 'doc1' }
}
});
expect(mockPost).toHaveBeenCalledWith('/api/documents.info', { id: 'doc1' });
expect(result.content[0].text).toContain('Doc 1');
});
it('should create a document', async () => {
mockPost.mockResolvedValueOnce({ data: { data: { id: 'new-doc' } } });
const result = await requestHandler({
params: {
name: 'outline_create_document',
arguments: { collectionId: 'col1', title: 'New Doc', text: 'Hello', publish: true }
}
});
expect(mockPost).toHaveBeenCalledWith('/api/documents.create', {
collectionId: 'col1', title: 'New Doc', text: 'Hello', publish: true
});
expect(result.content[0].text).toContain('new-doc');
});
});

View File

@@ -0,0 +1,184 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
// Environment variables
const OUTLINE_API_TOKEN = process.env.OUTLINE_API_TOKEN;
const OUTLINE_BASE_URL = process.env.OUTLINE_BASE_URL;
if (!OUTLINE_API_TOKEN || !OUTLINE_BASE_URL) {
console.error("Missing required environment variables: OUTLINE_API_TOKEN or OUTLINE_BASE_URL");
process.exit(1);
}
const axiosInstance = axios.create({
baseURL: OUTLINE_BASE_URL,
headers: {
Authorization: `Bearer ${OUTLINE_API_TOKEN}`,
"Content-Type": "application/json",
Accept: "application/json",
},
});
// Define tools
const tools: Tool[] = [
{
name: "outline_search_documents",
description: "Search for documents in Outline.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
collectionId: { type: "string", description: "Optional collection ID to scope search" },
},
required: ["query"],
},
},
{
name: "outline_get_document",
description: "Get a document by its ID.",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "The ID of the document" },
},
required: ["id"],
},
},
{
name: "outline_create_document",
description: "Create a new document.",
inputSchema: {
type: "object",
properties: {
collectionId: { type: "string", description: "The ID of the collection" },
title: { type: "string", description: "Document title" },
text: { type: "string", description: "Document content in Markdown" },
publish: { type: "boolean", description: "Whether to publish the document" },
},
required: ["collectionId", "title", "text"],
},
},
{
name: "outline_update_document",
description: "Update an existing document.",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "The ID of the document" },
title: { type: "string", description: "New title" },
text: { type: "string", description: "New content in Markdown" },
},
required: ["id"],
},
},
{
name: "outline_create_collection",
description: "Create a new collection.",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Collection name" },
description: { type: "string", description: "Collection description" },
},
required: ["name"],
},
},
];
const server = new Server(
{
name: "at-mintel-outline-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
switch (request.params.name) {
case "outline_search_documents": {
const { query, collectionId } = request.params.arguments as any;
const res = await axiosInstance.post("/api/documents.search", {
query,
collectionId,
});
return {
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
};
}
case "outline_get_document": {
const { id } = request.params.arguments as any;
const res = await axiosInstance.post("/api/documents.info", { id });
return {
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
};
}
case "outline_create_document": {
const { collectionId, title, text, publish } = request.params.arguments as any;
const res = await axiosInstance.post("/api/documents.create", {
collectionId,
title,
text,
publish: publish ?? true,
});
return {
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
};
}
case "outline_update_document": {
const { id, title, text } = request.params.arguments as any;
const res = await axiosInstance.post("/api/documents.update", {
id,
title,
text,
});
return {
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
};
}
case "outline_create_collection": {
const { name, description } = request.params.arguments as any;
const res = await axiosInstance.post("/api/collections.create", {
name,
description,
});
return {
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
};
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
} catch (error: any) {
const errorMsg = error.response?.data ? JSON.stringify(error.response.data) : error.message;
return {
content: [{ type: "text", text: `API Error: ${errorMsg}` }],
isError: true,
};
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Outline MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});

View File

@@ -0,0 +1,13 @@
import { config } from 'dotenv';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
import('./index.js').catch(err => {
console.error('Failed to start Outline MCP Server:', err);
process.exit(1);
});

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*"
]
}

View File

@@ -1,7 +1,6 @@
{ {
"name": "@mintel/page-audit", "name": "@mintel/page-audit",
"version": "1.9.0", "version": "1.9.17",
"private": true,
"description": "AI-powered website IST-analysis using DataForSEO and Gemini", "description": "AI-powered website IST-analysis using DataForSEO and Gemini",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -0,0 +1,2 @@
@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/
//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=263e7f75d8ada27f3a2e71fd6bd9d95298d48a4d

View File

@@ -0,0 +1,7 @@
# @mintel/payload-ai
## 1.1.0
### Minor Changes
- Initial release of the @mintel/payload-ai package

View File

@@ -0,0 +1,52 @@
{
"name": "@mintel/payload-ai",
"version": "1.9.18",
"description": "Reusable Payload CMS AI Extensions",
"type": "module",
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./components/FieldGenerators/*": "./dist/components/FieldGenerators/*.js",
"./components/*": "./dist/components/*.js",
"./actions/*": "./dist/actions/*.js",
"./globals/*": "./dist/globals/*.js",
"./endpoints/*": "./dist/endpoints/*.js",
"./utils/*": "./dist/utils/*.js",
"./tools/*": "./dist/tools/*.js"
},
"peerDependencies": {
"@payloadcms/next": ">=3.0.0",
"@payloadcms/ui": ">=3.0.0",
"payload": ">=3.0.0",
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"dependencies": {
"@ai-sdk/openai": "^3.0.39",
"@ai-sdk/react": "^3.0.110",
"@mintel/content-engine": "workspace:*",
"@mintel/thumbnail-generator": "workspace:*",
"@modelcontextprotocol/sdk": "^1.27.1",
"@qdrant/js-client-rest": "^1.17.0",
"ai": "^6.0.108",
"replicate": "^1.4.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@payloadcms/next": "3.77.0",
"@payloadcms/ui": "3.77.0",
"@types/node": "^20.17.17",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"next": "^15.1.0",
"payload": "3.77.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"typescript": "^5.7.3"
}
}

View File

@@ -0,0 +1,98 @@
import type { Plugin } from "payload";
import { AIChatPermissionsCollection } from "./collections/AIChatPermissions.js";
import type { PayloadChatPluginConfig } from "./types.js";
import { optimizePostEndpoint } from "./endpoints/optimizeEndpoint.js";
import {
generateSlugEndpoint,
generateThumbnailEndpoint,
generateSingleFieldEndpoint,
} from "./endpoints/generateEndpoints.js";
export const payloadChatPlugin =
(pluginOptions: PayloadChatPluginConfig): Plugin =>
(incomingConfig) => {
const config = { ...incomingConfig };
// If disabled, return config untouched
if (pluginOptions.enabled === false) {
return config;
}
// 1. Inject the Permissions Collection into the Schema
const existingCollections = config.collections || [];
const mcpServers = pluginOptions.mcpServers || [];
// Dynamically populate the select options for Collections and MCP Servers
const permissionCollection = { ...AIChatPermissionsCollection };
const collectionField = permissionCollection.fields.find(
(f) => "name" in f && f.name === "allowedCollections",
) as any;
if (collectionField) {
collectionField.options = existingCollections.map((c) => ({
label: c.labels?.singular || c.slug,
value: c.slug,
}));
}
const mcpField = permissionCollection.fields.find(
(f) => "name" in f && f.name === "allowedMcpServers",
) as any;
if (mcpField) {
mcpField.options = mcpServers.map((s) => ({
label: s.name,
value: s.name,
}));
}
config.collections = [...existingCollections, permissionCollection];
// 2. Register Custom API Endpoint for the AI Chat
config.endpoints = [
...(config.endpoints || []),
{
path: "/api/mcp-chat",
method: "post",
handler: async (_req) => {
// Fallback simple handler while developing endpoint logic
return Response.json({ message: "Chat endpoint active" });
},
},
{
path: "/api/mintel-ai/optimize",
method: "post",
handler: optimizePostEndpoint,
},
{
path: "/api/mintel-ai/generate-slug",
method: "post",
handler: generateSlugEndpoint,
},
{
path: "/api/mintel-ai/generate-thumbnail",
method: "post",
handler: generateThumbnailEndpoint,
},
{
path: "/api/mintel-ai/generate-single-field",
method: "post",
handler: generateSingleFieldEndpoint,
},
];
// 3. Inject Chat React Component into Admin UI
if (pluginOptions.renderChatBubble !== false) {
config.admin = {
...(config.admin || {}),
components: {
...(config.admin?.components || {}),
providers: [
...(config.admin?.components?.providers || []),
"@mintel/payload-ai/components/ChatWindow/index#ChatWindowProvider",
],
},
};
}
return config;
};

View File

@@ -0,0 +1,69 @@
import type { CollectionConfig } from 'payload'
/**
* A central collection to manage which AI Tools/MCPs a User or Role is allowed to use.
*/
export const AIChatPermissionsCollection: CollectionConfig = {
slug: 'ai-chat-permissions',
labels: {
singular: 'AI Chat Permission',
plural: 'AI Chat Permissions',
},
admin: {
useAsTitle: 'description',
group: 'AI & Tools',
},
fields: [
{
name: 'description',
type: 'text',
required: true,
admin: {
description: 'E.g. "Editors default AI permissions"',
},
},
{
type: 'row',
fields: [
{
name: 'targetUser',
type: 'relationship',
relationTo: 'users',
hasMany: false,
admin: {
description: 'Apply these permissions to a specific user (optional).',
},
},
{
name: 'targetRole',
type: 'select',
options: [
{ label: 'Admin', value: 'admin' },
{ label: 'Editor', value: 'editor' },
], // Ideally this is dynamically populated in a real scenario, but we hardcode standard roles for now
admin: {
description: 'Apply these permissions to all users with this role.',
},
},
],
},
{
name: 'allowedCollections',
type: 'select',
hasMany: true,
options: [], // Will be populated dynamically in the plugin init based on actual collections
admin: {
description: 'Which Payload collections is the AI allowed to read/write on behalf of this user?',
},
},
{
name: 'allowedMcpServers',
type: 'select',
hasMany: true,
options: [], // Will be populated dynamically based on plugin config
admin: {
description: 'Which external MCP Servers is the AI allowed to execute tools from?',
},
}
],
}

View File

@@ -0,0 +1,163 @@
"use client";
import React, { useState } from "react";
import { useDocumentInfo, toast } from "@payloadcms/ui";
type Action = "upscale" | "recover";
interface ActionState {
loading: boolean;
resultId?: string | number;
}
export const AiMediaButtons: React.FC = () => {
const { id } = useDocumentInfo();
const [upscale, setUpscale] = useState<ActionState>({ loading: false });
const [recover, setRecover] = useState<ActionState>({ loading: false });
if (!id) return null; // Only show on existing documents
const runAction = async (action: Action) => {
const setter = action === "upscale" ? setUpscale : setRecover;
setter({ loading: true });
const label = action === "upscale" ? "AI Upscale" : "AI Recover";
toast.info(
`${label} started this can take 3090 seconds, please wait…`,
);
try {
// The API path is hardcoded here and assuming that's where the host app registers the endpoint.
const response = await fetch(`/api/media/${id}/ai-process`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action }),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || `${label} failed`);
}
setter({ loading: false, resultId: result.mediaId });
toast.success(
`${label} erfolgreich! Neues Bild (ID: ${result.mediaId}) wurde gespeichert.`,
);
} catch (err: any) {
console.error(`[AiMediaButtons] ${action} error:`, err);
toast.error(
err instanceof Error ? err.message : `${label} fehlgeschlagen`,
);
setter({ loading: false });
}
};
const buttonStyle: React.CSSProperties = {
background: "var(--theme-elevation-150)",
border: "1px solid var(--theme-elevation-200)",
color: "var(--theme-text)",
padding: "8px 14px",
borderRadius: "4px",
fontSize: "13px",
fontWeight: 500,
display: "inline-flex",
alignItems: "center",
gap: "6px",
transition: "opacity 0.15s ease",
};
const disabledStyle: React.CSSProperties = {
opacity: 0.55,
cursor: "not-allowed",
};
return (
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: "10px",
marginBottom: "1.5rem",
marginTop: "0.5rem",
}}
>
{/* AI Upscale */}
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<button
type="button"
disabled={upscale.loading || recover.loading}
onClick={() => runAction("upscale")}
style={{
...buttonStyle,
...(upscale.loading || recover.loading ? disabledStyle : { cursor: "pointer" }),
}}
>
{upscale.loading ? "⏳ AI Upscale läuft…" : "✨ AI Upscale"}
</button>
{upscale.resultId && (
<a
href={`/admin/collections/media/${upscale.resultId}`}
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: "12px",
color: "var(--theme-elevation-500)",
textDecoration: "underline",
}}
>
Neues Bild öffnen (ID: {upscale.resultId})
</a>
)}
</div>
{/* AI Recover */}
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<button
type="button"
disabled={upscale.loading || recover.loading}
onClick={() => runAction("recover")}
style={{
...buttonStyle,
...(upscale.loading || recover.loading ? disabledStyle : { cursor: "pointer" }),
}}
>
{recover.loading ? "⏳ AI Recover läuft…" : "🔄 AI Recover"}
</button>
{recover.resultId && (
<a
href={`/admin/collections/media/${recover.resultId}`}
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: "12px",
color: "var(--theme-elevation-500)",
textDecoration: "underline",
}}
>
Neues Bild öffnen (ID: {recover.resultId})
</a>
)}
</div>
<p
style={{
width: "100%",
fontSize: "0.8rem",
color: "var(--theme-elevation-500)",
margin: 0,
lineHeight: 1.4,
}}
>
<strong>AI Upscale</strong> verbessert die Auflösung via{" "}
<code>google/upscaler</code>. <strong>AI Recover</strong> restauriert
alte/beschädigte Fotos via{" "}
<code>microsoft/bringing-old-photos-back-to-life</code>. Das
Ergebnis wird als neues Medium gespeichert.
</p>
</div>
);
};

View File

@@ -0,0 +1,156 @@
"use client";
import React, { useState, useEffect } from "react";
import { useChat } from "@ai-sdk/react";
export const ChatWindowProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
return (
<>
{children}
<ChatWindow />
</>
);
};
const ChatWindow: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const [pageContext, setPageContext] = useState<any>({ url: "" });
useEffect(() => {
if (typeof window !== "undefined") {
const path = window.location.pathname;
let collectionSlug = null;
let id = null;
// Payload admin URLs are usually /admin/collections/:slug/:id
const match = path.match(/\/collections\/([^/]+)(?:\/([^/]+))?/);
if (match) {
collectionSlug = match[1];
if (match[2] && match[2] !== "create") {
id = match[2];
}
}
setPageContext({
url: window.location.href,
title: document.title,
collectionSlug,
id,
});
}
}, [isOpen]); // Refresh context when chat is opened
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: "/api/mcp-chat",
initialMessages: [],
body: {
pageContext,
},
} as any) as any;
// Basic implementation to toggle chat window and submit messages
return (
<div className="payload-mcp-chat-container">
<button
className="payload-mcp-chat-toggle"
onClick={() => setIsOpen(!isOpen)}
style={{
position: "fixed",
bottom: "20px",
right: "20px",
zIndex: 9999,
padding: "12px 24px",
backgroundColor: "#000",
color: "#fff",
borderRadius: "8px",
border: "none",
cursor: "pointer",
fontWeight: "bold",
}}
>
{isOpen ? "Close AI Chat" : "Ask AI"}
</button>
{isOpen && (
<div
className="payload-mcp-chat-window"
style={{
position: "fixed",
bottom: "80px",
right: "20px",
width: "450px",
height: "650px",
backgroundColor: "#fff",
border: "1px solid #eaeaea",
borderRadius: "12px",
zIndex: 9999,
display: "flex",
flexDirection: "column",
boxShadow: "0 10px 40px rgba(0,0,0,0.1)",
}}
>
<div
className="chat-header"
style={{
padding: "16px",
borderBottom: "1px solid #eaeaea",
backgroundColor: "#f9f9f9",
borderTopLeftRadius: "12px",
borderTopRightRadius: "12px",
}}
>
<h3 style={{ margin: 0, fontSize: "16px" }}>Payload MCP Chat</h3>
</div>
<div
className="chat-messages"
style={{ flex: 1, padding: "16px", overflowY: "auto" }}
>
{messages.map((m: any) => (
<div
key={m.id}
style={{
marginBottom: "12px",
textAlign: m.role === "user" ? "right" : "left",
}}
>
<div
style={{
display: "inline-block",
padding: "8px 12px",
borderRadius: "8px",
backgroundColor: m.role === "user" ? "#000" : "#f0f0f0",
color: m.role === "user" ? "#fff" : "#000",
maxWidth: "80%",
}}
>
{m.role === "user" ? "G: " : "AI: "}
{m.content}
</div>
</div>
))}
</div>
<form
onSubmit={handleSubmit}
style={{ padding: "16px", borderTop: "1px solid #eaeaea" }}
>
<input
value={input}
placeholder="Ask me anything or use /commands..."
onChange={handleInputChange}
style={{
width: "100%",
padding: "12px",
borderRadius: "8px",
border: "1px solid #eaeaea",
boxSizing: "border-box",
}}
/>
</form>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,141 @@
"use client";
import React, { useState } from "react";
import { useField, useDocumentInfo, useForm } from "@payloadcms/ui";
export function AiFieldButton({ path, field }: { path: string; field: any }) {
const [isGenerating, setIsGenerating] = useState(false);
const [instructions, setInstructions] = useState("");
const [showInstructions, setShowInstructions] = useState(false);
// Payload hooks
const { value, setValue } = useField<string>({ path });
const { title } = useDocumentInfo();
const { fields } = useForm();
const extractText = (lexicalRoot: any): string => {
if (!lexicalRoot) return "";
let text = "";
const iterate = (node: any) => {
if (node.text) text += node.text + " ";
if (node.children) node.children.forEach(iterate);
};
iterate(lexicalRoot);
return text;
};
const handleGenerate = async (e: React.MouseEvent) => {
e.preventDefault();
const lexicalValue = fields?.content?.value as any;
const legacyValue = fields?.legacyMdx?.value as string;
let draftContent = legacyValue || "";
if (!draftContent && lexicalValue?.root) {
draftContent = extractText(lexicalValue.root);
}
setIsGenerating(true);
try {
// Field name is passed as a label usually, fallback to path
const fieldName = typeof field?.label === "string" ? field.label : path;
const fieldDescription =
typeof field?.admin?.description === "string"
? field.admin.description
: "";
const resData = await fetch("/api/api/mintel-ai/generate-single-field", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
documentTitle: (title as string) || "",
documentContent: draftContent,
fieldName,
fieldDescription,
instructions,
}),
});
const res = await resData.json();
if (res.success && res.text) {
setValue(res.text);
} else {
alert("Fehler: " + res.error);
}
} catch (e: any) {
console.error(e)
alert("Fehler bei der Generierung.");
} finally {
setIsGenerating(false);
setShowInstructions(false);
}
};
return (
<div
style={{
marginTop: "8px",
marginBottom: "8px",
display: "flex",
flexDirection: "column",
gap: "8px",
}}
>
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<button
type="button"
onClick={handleGenerate}
disabled={isGenerating}
style={{
background: "var(--theme-elevation-150)",
border: "1px solid var(--theme-elevation-200)",
color: "var(--theme-text)",
padding: "4px 12px",
borderRadius: "4px",
fontSize: "12px",
cursor: isGenerating ? "not-allowed" : "pointer",
display: "flex",
alignItems: "center",
gap: "6px",
opacity: isGenerating ? 0.6 : 1,
}}
>
{isGenerating ? "✨ AI arbeitet..." : "✨ AI Ausfüllen"}
</button>
<button
type="button"
onClick={(e) => {
e.preventDefault();
setShowInstructions(!showInstructions);
}}
style={{
background: "transparent",
border: "none",
color: "var(--theme-elevation-500)",
fontSize: "12px",
cursor: "pointer",
textDecoration: "underline",
}}
>
{showInstructions ? "Prompt verbergen" : "Mit Prompt..."}
</button>
</div>
{showInstructions && (
<textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="Eigene Anweisung an AI (z.B. 'als catchy slogan')"
disabled={isGenerating}
style={{
width: "100%",
padding: "6px 8px",
fontSize: "12px",
borderRadius: "4px",
border: "1px solid var(--theme-elevation-200)",
background: "var(--theme-elevation-50)",
color: "var(--theme-text)",
}}
rows={2}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,111 @@
"use client";
import React, { useState, useEffect } from "react";
import { useForm, useField } from "@payloadcms/ui";
export function GenerateSlugButton({ path }: { path: string }) {
const [isGenerating, setIsGenerating] = useState(false);
const [instructions, setInstructions] = useState("");
useEffect(() => {
if (!isGenerating) return;
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue =
"Slug-Generierung läuft noch. Wenn Sie neu laden, bricht der Vorgang ab!";
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [isGenerating]);
const { fields, replaceState } = useForm();
const { value, initialValue, setValue } = useField({ path });
const extractText = (lexicalRoot: any): string => {
if (!lexicalRoot) return "";
let text = "";
const iterate = (node: any) => {
if (node.text) text += node.text + " ";
if (node.children) node.children.forEach(iterate);
};
iterate(lexicalRoot);
return text;
};
const handleGenerate = async () => {
const title = (fields?.title?.value as string) || "";
const lexicalValue = fields?.content?.value as any;
const legacyValue = fields?.legacyMdx?.value as string;
let draftContent = legacyValue || "";
if (!draftContent && lexicalValue?.root) {
draftContent = extractText(lexicalValue.root);
}
setIsGenerating(true);
try {
const resData = await fetch("/api/api/mintel-ai/generate-slug", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title,
draftContent,
oldSlug: initialValue as string,
instructions,
}),
});
const res = await resData.json();
if (res.success && res.slug) {
setValue(res.slug);
} else {
alert("Fehler: " + res.error);
}
} catch (e: any) {
console.error(e);
alert("Unerwarteter Fehler.");
} finally {
setIsGenerating(false);
}
};
return (
<div className="flex gap-2 items-center mb-4">
<textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="Optionale AI Anweisung für den Slug..."
disabled={isGenerating}
style={{
width: "100%",
minHeight: "40px",
padding: "8px 12px",
fontSize: "14px",
borderRadius: "4px",
border: "1px solid var(--theme-elevation-200)",
background: "var(--theme-elevation-50)",
color: "var(--theme-text)",
marginBottom: "8px",
}}
/>
<button
type="button"
onClick={handleGenerate}
disabled={isGenerating}
className="btn btn--icon-style-none btn--size-medium ml-auto"
style={{
background: "var(--theme-elevation-150)",
border: "1px solid var(--theme-elevation-200)",
color: "var(--theme-text)",
boxShadow: "0 2px 4px rgba(0,0,0,0.05)",
transition: "all 0.2s ease",
opacity: isGenerating ? 0.6 : 1,
cursor: isGenerating ? "not-allowed" : "pointer",
}}
>
<span className="btn__content">
{isGenerating ? "✨ Generiere (ca 10s)..." : "✨ AI Slug Generieren"}
</span>
</button>
</div>
);
}

View File

@@ -0,0 +1,112 @@
"use client";
import React, { useState, useEffect } from "react";
import { useForm, useField } from "@payloadcms/ui";
export function GenerateThumbnailButton({ path }: { path: string }) {
const [isGenerating, setIsGenerating] = useState(false);
const [instructions, setInstructions] = useState("");
useEffect(() => {
if (!isGenerating) return;
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue =
"Bild-Generierung läuft noch (dies dauert bis zu 2 Minuten). Wenn Sie neu laden, bricht der Vorgang ab!";
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [isGenerating]);
const { fields } = useForm();
const { value, setValue } = useField({ path });
const extractText = (lexicalRoot: any): string => {
if (!lexicalRoot) return "";
let text = "";
const iterate = (node: any) => {
if (node.text) text += node.text + " ";
if (node.children) node.children.forEach(iterate);
};
iterate(lexicalRoot);
return text;
};
const handleGenerate = async () => {
const title = (fields?.title?.value as string) || "";
const lexicalValue = fields?.content?.value as any;
const legacyValue = fields?.legacyMdx?.value as string;
let draftContent = legacyValue || "";
if (!draftContent && lexicalValue?.root) {
draftContent = extractText(lexicalValue.root);
}
setIsGenerating(true);
try {
const resData = await fetch("/api/api/mintel-ai/generate-thumbnail", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
draftContent,
title,
instructions,
}),
});
const res = await resData.json();
if (res.success && res.mediaId) {
setValue(res.mediaId);
} else {
alert("Fehler: " + res.error);
}
} catch (e: any) {
console.error(e);
alert("Unerwarteter Fehler.");
} finally {
setIsGenerating(false);
}
};
return (
<div className="flex gap-2 items-center mt-2 mb-4">
<textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="Optionale Thumbnail-Detailanweisung (Farben, Stimmung, etc.)..."
disabled={isGenerating}
style={{
width: "100%",
minHeight: "40px",
padding: "8px 12px",
fontSize: "14px",
borderRadius: "4px",
border: "1px solid var(--theme-elevation-200)",
background: "var(--theme-elevation-50)",
color: "var(--theme-text)",
marginBottom: "8px",
}}
/>
<button
type="button"
onClick={handleGenerate}
disabled={isGenerating}
className="btn btn--icon-style-none btn--size-medium"
style={{
background: "var(--theme-elevation-150)",
border: "1px solid var(--theme-elevation-200)",
color: "var(--theme-text)",
boxShadow: "0 2px 4px rgba(0,0,0,0.05)",
transition: "all 0.2s ease",
opacity: isGenerating ? 0.6 : 1,
cursor: isGenerating ? "not-allowed" : "pointer",
}}
>
<span className="btn__content">
{isGenerating
? "✨ AI arbeitet (dauert ca. 1-2 Min)..."
: "✨ AI Thumbnail Generieren"}
</span>
</button>
</div>
);
}

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