diff --git a/.gitattributes b/.gitattributes index 28b9760b6bf139db2b8fcb9810034d6cedbdc9bd..0d37516c18a06f522621be1e26519e5b35934251 100644 --- a/.gitattributes +++ b/.gitattributes @@ -67,3 +67,89 @@ Assets/Projects/Ark.Portfolio/Admin_Projects.PNG filter=lfs diff=lfs merge=lfs - Assets/Projects/Ark.Portfolio/Admin_ResumeManager.PNG filter=lfs diff=lfs merge=lfs -text Assets/Projects/Ark.Portfolio/portfolio-hero.png filter=lfs diff=lfs merge=lfs -text Assets/Site/Icon.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/data/ark_portfolio.sqlite filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Bryan.jfif filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Lenny[[:space:]]Avril.jpeg filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Mario.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance/Ark_Alliance_1.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance/Ark_Alliance_Hero.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.React.Component/components-hero.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.StartupCms.AI/ArkAllianceStartupCmsAi_Hero.Png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.StartupCms.AI/ArkAllianceStartupCmsAiHero.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.StartupCms.AI/Google_AI_Studio_2026-01-08T00_41_57.111Z.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/bot-hero.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/bot1.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot10.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot11.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot12.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot13.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot14.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot2.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot3.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot4.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot5.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot6.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot7.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot8.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Capture.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Providers.Lib/providers-hero.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Providers.Lib/trading-hero.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.TrendsCalculator/trends-hero.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.TrendsCalculator/trends-hero.png.jpg filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_Dashobard.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_Hero_Carrousel.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_Projects.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_ResumeManager.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/portfolio-hero.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Site/Icon.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Backend/uploads/word/1d04f81e-fd89-4974-878e-449b90adfc5f.docx filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/App/Background.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/App/LogoArkAlliance.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/bot1.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/Bot10.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/Bot11.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/Bot12.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/Bot13.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/Bot14.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/Bot2.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/Bot3.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/Bot4.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/Bot5.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/Bot6.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/Bot7.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/Bot8.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.Share/assets/Capture.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/bot1.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot10.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot11.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot12.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot13.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot14.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot2.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot3.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot4.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot5.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot6.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot7.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot8.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Capture.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/Assets/icon/LogoArkAlliance.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/App/LogoArkAlliance.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/bot1.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot10.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot11.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot12.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot13.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot14.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot2.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot3.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot4.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot5.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot6.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot7.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot8.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/Capture.PNG filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/assets/Icon.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/logo.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/public/LogoArkAlliance.png filter=lfs diff=lfs merge=lfs -text +Ark.Alliance.StartupCms.Ai.UI/src/Assets/LogoArkAlliance.png filter=lfs diff=lfs merge=lfs -text diff --git a/Ark.Alliance.StartupCms.Ai.Backend/.env b/Ark.Alliance.StartupCms.Ai.Backend/.env new file mode 100644 index 0000000000000000000000000000000000000000..25fc0a43829f1273afbeab8df6953c02469d8a63 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/.env @@ -0,0 +1,8 @@ +PORT=3085 +DB_TYPE=sqlite +# DB_HOST=localhost +# DB_PORT=5432 +# DB_USER=postgres +# DB_PASSWORD=postgres +# DB_NAME=ark_portfolio + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/.env.example b/Ark.Alliance.StartupCms.Ai.Backend/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..049725a3c5b6312fee003374627ad7029b37fb98 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/.env.example @@ -0,0 +1,49 @@ +# ═══════════════════════════════════════════════════════════════════════ +# Ark.Portfolio Backend - Environment Configuration +# ═══════════════════════════════════════════════════════════════════════ +# Copy this file to .env and customize as needed + +# ───────────────────────────────────────────────────────────────────────── +# Server Configuration +# ───────────────────────────────────────────────────────────────────────── +# Port for the backend server (default: 3085) +PORT=3085 + +# Node environment: development | production | test +NODE_ENV=development + +# ───────────────────────────────────────────────────────────────────────── +# Protocol Configuration +# ───────────────────────────────────────────────────────────────────────── +# Use HTTPS instead of HTTP (default: false) +# When true, auto-generates self-signed certificate if none exists +USE_HTTPS=false + +# ───────────────────────────────────────────────────────────────────────── +# Database Configuration +# ───────────────────────────────────────────────────────────────────────── +DATABASE_TYPE=sqlite +DATABASE_NAME=portfolio.db + +# ───────────────────────────────────────────────────────────────────────── +# Authentication +# ───────────────────────────────────────────────────────────────────────── +# IMPORTANT: Change this in production! +JWT_SECRET=change-this-secret-in-production +JWT_EXPIRES_IN=24h + +# Admin password for initial user (defaults to Admin1234 if not set) +# IMPORTANT: Set this via GitHub Secrets (ADMIN_KEY_CMS) in production! +ADMIN_KEY_CMS= + +# ───────────────────────────────────────────────────────────────────────── +# AI Services (Optional - encrypted in database) +# ───────────────────────────────────────────────────────────────────────── +# OPENAI_API_KEY=sk-... +# ANTHROPIC_API_KEY=sk-ant-... +# GOOGLE_AI_API_KEY=... + +# ───────────────────────────────────────────────────────────────────────── +# CORS Configuration +# ───────────────────────────────────────────────────────────────────────── +CORS_ORIGIN=http://localhost:3080 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/.gitignore b/Ark.Alliance.StartupCms.Ai.Backend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..ba74aa156b367c1130d008dfe9b12bfaacfe0991 Binary files /dev/null and b/Ark.Alliance.StartupCms.Ai.Backend/.gitignore differ diff --git a/Ark.Alliance.StartupCms.Ai.Backend/Certificate/server.crt b/Ark.Alliance.StartupCms.Ai.Backend/Certificate/server.crt new file mode 100644 index 0000000000000000000000000000000000000000..116300a99e3a6787a2be308b571578c9dadc7ba9 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/Certificate/server.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDUDCCAjigAwIBAgIBATANBgkqhkiG9w0BAQsFADBFMRIwEAYDVQQDEwlsb2Nh +bGhvc3QxIjAgBgNVBAoTGUFyay5Qb3J0Zm9saW8gRGV2ZWxvcG1lbnQxCzAJBgNV +BAYTAkNBMB4XDTI1MTIzMTE4MzA0M1oXDTI2MTIzMTE4MzA0M1owRTESMBAGA1UE +AxMJbG9jYWxob3N0MSIwIAYDVQQKExlBcmsuUG9ydGZvbGlvIERldmVsb3BtZW50 +MQswCQYDVQQGEwJDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK0X +nHahsBsDzxdBr9t7K4lGH0Giw42jgmaUsVQ9LdrD+pol5UoW2F4W6ERrEud9m4zT +TfSMFsTs4Iw72CPuc1rK8TKyt1P+r2fAUj+d6OImkVwABpJJkaXhG7NjBias6pdM +5xpHS2LsoFfVQEDL827R74PWUR3o8tvbS8frsa+w+FOXGYjnVGzSG/O/m5GYGKzX +TzAB594S0T8oOlKaIu1aSKLXLKhjd2tYX3Yv+yenGSc0gN+9E4ybLlw4gpub4jjW +R+j85Qzdw+6BzYHrVpbRXiunN611Ir9d8vKniuEYMs3SGaiamaWA0nbVsDbJei7t +jZxoHl4VCMV9BpriSEsCAwEAAaNLMEkwCQYDVR0TBAIwADALBgNVHQ8EBAMCBaAw +EwYDVR0lBAwwCgYIKwYBBQUHAwEwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAAB +MA0GCSqGSIb3DQEBCwUAA4IBAQBYbck//uesmaRr1BebLGmYce4+TrGfAp49BA6h +fbjhlrSqSpXBZi1JrqWWgPvux23NcMA5Pn0TAHTQViiOagSefdgZUFY73ytAB2PH +AMoTyI34HhqCnSggvBDwJ3O5iX5Y1txcqNRq9frxUmBHNSFfYexKqNh71GMJA9/P +Zv4c40wQK3Yt3+sM/8H9R9x+FPSYPTk+0ycJuEydc6HH/VEbb0OTPhzf6/+zHMW2 +3fDDCSxp0B64LBEwjnEsbGlKm/vOvrEizkXfcW5jpHgJVSo+7R8h3Sd1U3n5N74p +zkshCRbIzr5PVEE+D20YNvwypKP/5VK4UxLdAof9FWezIgjN +-----END CERTIFICATE----- diff --git a/Ark.Alliance.StartupCms.Ai.Backend/Certificate/server.key b/Ark.Alliance.StartupCms.Ai.Backend/Certificate/server.key new file mode 100644 index 0000000000000000000000000000000000000000..5611ba5fd5839f27902113746fc972cbbde1ee21 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/Certificate/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEArRecdqGwGwPPF0Gv23sriUYfQaLDjaOCZpSxVD0t2sP6miXl +ShbYXhboRGsS532bjNNN9IwWxOzgjDvYI+5zWsrxMrK3U/6vZ8BSP53o4iaRXAAG +kkmRpeEbs2MGJqzql0znGkdLYuygV9VAQMvzbtHvg9ZRHejy29tLx+uxr7D4U5cZ +iOdUbNIb87+bkZgYrNdPMAHn3hLRPyg6Upoi7VpIotcsqGN3a1hfdi/7J6cZJzSA +370TjJsuXDiCm5viONZH6PzlDN3D7oHNgetWltFeK6c3rXUiv13y8qeK4RgyzdIZ +qJqZpYDSdtWwNsl6Lu2NnGgeXhUIxX0GmuJISwIDAQABAoIBAFTNjxouIFYz+Qen +Zo3NHCjewpyJLm7b75zrd6/SY39dag+QxnQUmkAK0BQJM5NX3lhSBEzuP4+OkHsU +LNJ51mliqWZR2fDchDp8Jq/FYV4/UBM3bgDI64NONSCfwNk8+ZhUFJuy11Ppo6s0 +rwyv/53JXCQG4eoaAbZGjCuwJXdyGQKC6fl5H5oN6PasFXVuYp1IqXA8E5kqufuj +FQzszHxmxtmANe+emZjaBxNPH3u/mSVUZboQ1sRmSQNjQ8pevCiwN7SRKS22BAL+ +KuMU3cJZcxoKtf4KQxjUpYPuN0Qv9vWS1RIGy1IxWnJsE+mtkDcEbj0oFlbPdviR +Z3bYcikCgYEA6K9H8+vJO1jrVKLWEOuDYNndXUlfloVuiBBUyenh/OfL6D4Z7C4R +fiE7WI4Sb7Gf9eA0s4+aKx1YxNp3RBvN9ApIUxVS0+sHFWNKRO04oXrRJfbH5M69 +MYYfB81csdT9ypJW+pbb7qUiR7lpLzijyXeuD5PAU/l2eGiscm3tW1MCgYEAvm+x +MtZ6cbpNGn5OVLAGQ0EHa+RhrqfrGLlsaTKMV34Lp1zXcIhAr8mAHpyT8/SthL1N +QD5fP6o5G77A+cJcz0mHLiBSTG2brx8thTrvt8ZNQDINKfySPrhZT27aZUK5Xvm1 +qWbC+X7SuhXP3D3HmB1VLT89YB09LgstM1R5uCkCgYB7wP3b1YPpdJl1IkYIKbpu +QHFjtCqKu9zVsRnnaeUvxXjFxIG1A6t6EeKmbqmPjkEtXFmrRq0QUUNtL2RPbRpU +uUNOLQEohM/3qB9QGXsNJ20la+NU5j/pnxPR6n9qdYWlv79S9/lxK5LX4tz2qIE0 +HdsNnd+kygEgeUt+cMjU0QKBgFdZyjDkemOiLe3CE9H6r9S3hlzx8/B2K3s6ykRy +oDcdpODO0C9ZADrhtXfVIHRdPh5N6ppWQcBlJy7Xz5KAmaunMW9x+e3+tNOd/HZJ +M13bguG4U5t3s+k7DBRIZ7rc4UR0S+R5M2PXzXb3vFFssRnQEprfkBp/LunozIHn +9LEJAoGAWEzXrCAvBs6g4QU0/D3yMpjBZV9DQ3ndMC2osrkBWJnyUNg9p9mvwzgU +CMwopkXH33YKazTiPaEPCBXtMibArYSS81ey3C4asCKFADiVelnU0yqWgx5PrM6D +OSQGFJZO8TCTkiDrnq5w20bXyPJGbrAfcwavnL0xUIPGnLhuOZE= +-----END RSA PRIVATE KEY----- diff --git a/Ark.Alliance.StartupCms.Ai.Backend/README.md b/Ark.Alliance.StartupCms.Ai.Backend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4d3d78223f7520394043a737d12be03b115a20d5 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/README.md @@ -0,0 +1,293 @@ +# Ark.Alliance.StartupCms.AI.Backend + +
+ +![Node.js](https://img.shields.io/badge/Node.js-18+-339933?style=for-the-badge&logo=node.js) +![Express](https://img.shields.io/badge/Express-4.x-black?style=for-the-badge&logo=express) +![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue?style=for-the-badge&logo=typescript) +![SQLite](https://img.shields.io/badge/SQLite-TypeORM-003B57?style=for-the-badge&logo=sqlite) + +**RESTful API Backend for Ark.Alliance.StartupCms.AI** + +*Express • TypeORM • JWT Auth • Multi-Provider AI • Team Management* + +
+ +--- + +The **Backend** is a robust Node.js/Express application providing RESTful APIs for the AI-powered startup CMS ecosystem. It features TypeORM for database interactions, multi-provider AI services, JWT authentication, hierarchical team management, and comprehensive resume data handling. + +## 📦 Functional Capabilities + +The backend serves as the secure, intelligent core of the ecosystem. + +| Domain | Capability | Description | Code Reference | +| :--- | :--- | :--- | :--- | +| **API** | **REST Endpoints** | Structured controllers for Auth, Team, Resume, Projects, and AI. | `src/controllers/*.controller.ts` | +| **Security** | **Authentication** | JWT-based auth with bcrypt hashing, RBAC, and Helmet headers. | `src/middleware/auth.middleware.ts` | +| **AI** | **Multi-Provider AI** | Unified interface for OpenAI, Anthropic, and Google Gemini with prompt templates. | `src/services/ai.service.ts` | +| **Team** | **Hierarchy Management** | Org chart with hierarchical relationships and reporting structure. | `src/services/collaborator.service.ts` | +| **Resume** | **Profile System** | Comprehensive resume data: experiences, skills, education, languages, hobbies, domains. | `src/services/resume.service.ts` | +| **Media** | **Asset Management** | Avatar uploads with base64 encoding and automatic synchronization. | `src/services/media.service.ts` | +| **Data** | **High-Performance Persistence** | TypeORM with SQLite, indexed queries, and optimized entity relationships. | `src/database/entities/` | +| **Tasks** | **Growth Tracking** | Task management with lessons learned and partial public visibility. | `src/services/task.service.ts` | +| **DevOps** | **Auto-Seeding** | Automatic DB population with team members, skills, and sample data. | `src/database/seeds/seed.ts` | + +--- + +## 🏗️ Project Structure + +```text +Ark.Alliance.StartupCms.Ai.Backend/ +├── 📁 src/ +│ ├── config/ # Configuration modules +│ │ ├── database.ts # TypeORM DataSource +│ │ ├── swagger.config.ts # API documentation +│ │ └── cors.config.ts # CORS whitelist +│ │ +│ ├── controllers/ # Request handlers +│ │ ├── auth.controller.ts # Authentication +│ │ ├── collaborator.controller.ts # Team/Org Chart +│ │ ├── resume.controller.ts # Resume Data API +│ │ ├── project.controller.ts # Project CRUD +│ │ ├── task.controller.ts # Task Management +│ │ └── ai-profile.controller.ts # AI Profile Generation +│ │ +│ ├── database/ +│ │ ├── entities/ # TypeORM entities with indexes +│ │ │ ├── User.ts # User authentication +│ │ │ ├── Collaborator.ts # Team members +│ │ │ ├── Experience.ts # Work history +│ │ │ ├── Skill.ts # Technical skills +│ │ │ ├── Education.ts # Academic background +│ │ │ ├── Language.ts # Language proficiency +│ │ │ ├── Hobby.ts # Personal interests +│ │ │ ├── BusinessDomain.ts # Industry expertise +│ │ │ ├── Task.ts # Task tracking +│ │ │ └── PromptTemplate.ts # AI prompt templates +│ │ ├── seeds/ # Data seeding +│ │ │ └── seeders/ +│ │ │ ├── organization.seeder.ts +│ │ │ ├── bryan-ceo.seeder.ts +│ │ │ ├── armand-architect.seeder.ts +│ │ │ ├── radu-director.seeder.ts +│ │ │ ├── mario-consultant.seeder.ts +│ │ │ ├── lenny-csm.seeder.ts +│ │ │ └── avatar.seeder.ts +│ │ └── InitDbAsset/ # JSON seed data +│ │ +│ ├── middleware/ # Express middleware +│ │ ├── auth.middleware.ts # JWT verification +│ │ └── error.middleware.ts # Global error handler +│ │ +│ ├── services/ # Business logic +│ │ ├── auth.service.ts # Authentication +│ │ ├── collaborator.service.ts # Team hierarchy +│ │ ├── resume.service.ts # Resume aggregation +│ │ ├── ai-profile.service.ts # AI-assisted profile generation +│ │ ├── task.service.ts # Task management +│ │ └── prompt-template.service.ts # AI prompt management +│ │ +│ └── index.ts # Entry point +│ +├── 📁 Assets/ +│ └── Profil_Avatars/ # Team member avatars +│ +└── 📄 data/ + └── ark_portfolio.sqlite # SQLite database +``` + +--- + +## 📐 Architecture + +### Request Flow + +```mermaid +sequenceDiagram + participant Client + participant Express as Middleware Stack + participant Controller + participant Service + participant Repository + participant DB as Database + + Client->>Express: HTTP Request + Express->>Express: CORS, Helmet, Auth + Express->>Controller: Route Handler + Controller->>Service: Business Logic + Service->>Repository: Data Access + Repository->>DB: Indexed Query + DB-->>Repository: Result + Repository-->>Service: Entity + Service-->>Controller: DTO + Controller-->>Express: Response + Express-->>Client: JSON +``` + +### Database Performance Optimizations + +All resume entities have **database indexes** on `userId` and `displayOrder` columns for optimal query performance: + +- `Experience`: `IDX_experience_userId`, `IDX_experience_displayOrder` +- `Skill`: `IDX_skills_userId`, `IDX_skills_displayOrder` +- `Education`: `IDX_education_userId` +- `Language`: `IDX_languages_userId`, `IDX_languages_displayOrder` +- `Hobby`: `IDX_hobbies_userId`, `IDX_hobbies_displayOrder` +- `BusinessDomain`: `IDX_business_domains_userId`, `IDX_business_domains_displayOrder` + +--- + +## 🔌 API Endpoints + +### Public Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/team` | Get organization chart (hierarchical) | +| `GET` | `/api/team/:id` | Get collaborator profile | +| `GET` | `/api/resume` | Get resume data (defaults to Armand) | +| `GET` | `/api/resume?userId=:id` | Get specific user's resume | +| `GET` | `/api/projects` | List all projects | +| `GET` | `/api/projects/:id` | Get project details | +| `GET` | `/api/organization` | Get organization info | +| `GET` | `/api/tasks` | List public tasks | + +### Authentication + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/api/auth/login` | User login | +| `POST` | `/api/auth/logout` | User logout | +| `GET` | `/api/auth/verify` | Verify JWT token | + +### Admin Endpoints (Protected) + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/admin/projects` | List projects (admin) | +| `POST` | `/api/admin/projects` | Create project | +| `PUT` | `/api/admin/projects/:id` | Update project | +| `DELETE` | `/api/admin/projects/:id` | Delete project | +| `POST` | `/api/admin/media/upload` | Upload avatar/media | +| `POST` | `/api/ai-profile/generate` | Generate AI profile | +| `GET` | `/api/prompt-templates` | List AI prompt templates | + +--- + +## ⚙️ Configuration + +### Environment Variables + +Create a `.env` file in the Backend root: + +```env +# Server +PORT=3085 +NODE_ENV=development + +# Database +DATABASE_TYPE=sqlite +DATABASE_NAME=ark_portfolio.sqlite + +# Authentication +JWT_SECRET=your-256-bit-secret-key +JWT_EXPIRES_IN=24h +BCRYPT_ROUNDS=12 +ADMIN_KEY_CMS=your-admin-password + +# AI Providers +OPENAI_API_KEY=sk-... +ANTHROPIC_API_KEY=sk-ant-... +GOOGLE_AI_API_KEY=... + +# CORS +CORS_ORIGIN=http://localhost:3080 + +# File Upload +MAX_FILE_SIZE=10485760 +UPLOAD_PATH=./Assets/Profil_Avatars +``` + +--- + +## 🚀 Usage + +### Prerequisites + +- Node.js v18+ +- `@arkalliance/startupcms-ai-share` built and available + +### Development Server + +```bash +npm install +npm run dev +# Server runs on http://localhost:3085 +``` + +### Production Build + +```bash +npm run build +npm start +``` + +### Database Seeding + +```bash +npm run seed +# Seeds organization, team members (Bryan, Armand, Radu, Mario, Lenny), +# experiences, skills, languages, hobbies, business domains, and avatars +``` + +### API Documentation + +Once running, visit: [http://localhost:3085/api-docs](http://localhost:3085/api-docs) + +--- + +## 🔒 Security Features + +| Feature | Implementation | +|---------|----------------| +| Password Hashing | bcrypt (12 rounds) | +| JWT Tokens | HS256, 24h expiry | +| Security Headers | Helmet.js | +| CORS | Configured whitelist | +| Input Validation | DTO validation | +| Database Indexes | Performance + timing attack prevention | + +--- + +## 🧪 Testing + +```bash +# Run tests +cd ../Ark.Alliance.StartupCms.Ai.Tests +npm test -- --testPathPattern=Backend +``` + +--- + +## 📚 Related Documentation + +| Document | Location | Purpose | +|----------|----------|---------| +| Main README | `../README.md` | Project overview, features, philosophy | +| Share Layer | `../Ark.Alliance.StartupCms.Ai.Share/README.md` | DTOs, enums consumed by API | +| UI Layer | `../Ark.Alliance.StartupCms.Ai.UI/README.md` | Frontend integration | +| Tests Layer | `../Ark.Alliance.StartupCms.Ai.Tests/README.md` | Backend test patterns | + +--- + +
+ +**Ark.Alliance.StartupCms.AI.Backend** — Part of the Ark Alliance Ecosystem + + +Armand Richelet-Kleinberg © M2H.IO
+AI-assisted development with Anthropic Claude & Google Gemini +
+ +
diff --git a/Ark.Alliance.StartupCms.Ai.Backend/convert-imports.ps1 b/Ark.Alliance.StartupCms.Ai.Backend/convert-imports.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..2dcf11c36778340ed0184bc8448c22787fd25207 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/convert-imports.ps1 @@ -0,0 +1,46 @@ +# PowerShell script to convert package imports to relative imports +# Converts '@ark/portfolio-share' to relative path based on file location + +$files = @( + "src/controllers/admin.controller.ts", + "src/database/entities/style-config.entity.ts", + "src/database/repositories/menu.repository.ts", + "src/services/admin-carousel.service.ts", + "src/services/admin-cv.service.ts", + "src/services/admin-media.service.ts", + "src/services/admin-menu.service.ts", + "src/services/admin-project.service.ts", + "src/services/admin-style.service.ts", + "src/services/admin-widget.service.ts", + "src/services/ai.service.ts", + "src/services/dashboard.service.ts", + "src/services/profile.service.ts" +) + +foreach ($file in $files) { + $fullPath = "c:/Users/Criprtoswiss/source/repos/Ark.Portfolio/Ark.Portfolio.Backend/$file" + + if (Test-Path $fullPath) { + $content = Get-Content $fullPath -Raw + + # Calculate relative path from file to Share layer + # All these files are 3 levels deep from Backend root + $relativePath = if ($file -match "src/controllers/") { + "../../Ark.Portfolio.Share" + } elseif ($file -match "src/database/") { + "../../../Ark.Portfolio.Share" + } elseif ($file -match "src/services/") { + "../../Ark.Portfolio.Share" + } else { + "../../Ark.Portfolio.Share" + } + + # Replace package import with relative import + $content = $content -replace "from '@ark/portfolio-share'", "from '$relativePath'" + + Set-Content -Path $fullPath -Value $content -NoNewline + Write-Host "Updated: $file" + } +} + +Write-Host "Done! Converted all files to use relative imports." diff --git a/Ark.Alliance.StartupCms.Ai.Backend/data/ark_portfolio.sqlite b/Ark.Alliance.StartupCms.Ai.Backend/data/ark_portfolio.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..eb7b93fb7b3c217a50905585bfd1a6db6b3f56ba --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/data/ark_portfolio.sqlite @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0bd68477ae4ecaaf62fab987191cd8049b980b8551f9fc405ab29846134a89ce +size 2736128 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/docker-compose.yml b/Ark.Alliance.StartupCms.Ai.Backend/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..03811da5f56d10f572367e677894d572d734aeed --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3.8' +services: + db: + image: postgres:15-alpine + container_name: ark_portfolio_db + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: ark_portfolio + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: diff --git a/Ark.Alliance.StartupCms.Ai.Backend/final-fix-paths.ps1 b/Ark.Alliance.StartupCms.Ai.Backend/final-fix-paths.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..c4d1e4a8c7a12ab021ab64d9ebd2bcb0eda68a54 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/final-fix-paths.ps1 @@ -0,0 +1,27 @@ +# Final fix: Point to the actual index.ts file in Ark.Portfolio.Share + +Get-ChildItem -Path "src" -Recurse -Include *.ts | ForEach-Object { + $content = Get-Content $_.FullName -Raw + + # Calculate correct relative path based on file depth + $relativePath = $_.FullName -replace [regex]::Escape("c:/Users/Criprtoswiss/source/repos/Ark.Portfolio/Ark.Portfolio.Backend/"), "" + $depth = ($relativePath -split "/").Count - 1 + + # Build correct path to Share index.ts + $shareIndexPath = switch ($depth) { + 2 { "../../Ark.Portfolio.Share/index" } # src/services/ + 3 { "../../../Ark.Portfolio.Share/index" } # src/database/repositories/ + default { "../../Ark.Portfolio.Share/index" } + } + + # Replace the import + $newContent = $content -replace "from '\.\./\.\./Ark\.Portfolio\.Share'", "from '$shareIndexPath'" + $newContent = $newContent -replace "from '\.\./\.\./\.\./Ark\.Portfolio\.Share'", "from '$shareIndexPath'" + + if ($newContent -ne $content) { + Set-Content -Path $_.FullName -Value $newContent -NoNewline + Write-Host "Fixed: $relativePath -> $shareIndexPath" + } +} + +Write-Host "All paths now point to Ark.Portfolio.Share/index" diff --git a/Ark.Alliance.StartupCms.Ai.Backend/fix-paths.ps1 b/Ark.Alliance.StartupCms.Ai.Backend/fix-paths.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..73bd7f17c097403bc8a9bdaebdd490096828cada --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/fix-paths.ps1 @@ -0,0 +1,43 @@ +# Fix relative paths - services are 2 levels deep from src, need 3 ../ to reach parent +# Structure: Ark.Portfolio.Backend/src/services/*.ts -> ../../../Ark.Portfolio.Share + +$filesToFix = @{ + # Controllers: src/controllers/ -> need ../../ + "src/controllers/admin.controller.ts" = "../../Ark.Portfolio.Share" + + # Repositories: src/database/repositories/ -> need ../../../ + "src/database/repositories/menu.repository.ts" = "../../../Ark.Portfolio.Share" + + # Services: src/services/ -> need ../../ + "src/services/admin-carousel.service.ts" = "../../Ark.Portfolio.Share" + "src/services/admin-cv.service.ts" = "../../Ark.Portfolio.Share" + "src/services/admin-media.service.ts" = "../../Ark.Portfolio.Share" + "src/services/admin-menu.service.ts" = "../../Ark.Portfolio.Share" + "src/services/admin-project.service.ts" = "../../Ark.Portfolio.Share" + "src/services/admin-style.service.ts" = "../../Ark.Portfolio.Share" + "src/services/admin-widget.service.ts" = "../../Ark.Portfolio.Share" + "src/services/ai.service.ts" = "../../Ark.Portfolio.Share" + "src/services/dashboard.service.ts" = "../../Ark.Portfolio.Share" + "src/services/profile.service.ts" = "../../Ark.Portfolio.Share" +} + +foreach ($file in $filesToFix.Keys) { + $fullPath = "c:/Users/Criprtoswiss/source/repos/Ark.Portfolio/Ark.Portfolio.Backend/$file" + $correctPath = $filesToFix[$file] + + if (Test-Path $fullPath) { + $content = Get-Content $fullPath -Raw + + # Fix any incorrect paths + $content = $content -replace "from '\.\./\.\./Ark\.Portfolio\.Share'", "from '$correctPath'" + $content = $content -replace "from '\.\./\.\./\.\./Ark\.Portfolio\.Share'", "from '$correctPath'" + + Set-Content -Path $fullPath -Value $content -NoNewline + Write-Host "Fixed: $file -> $correctPath" + } + else { + Write-Host "Not found: $file" + } +} + +Write-Host "Done! All relative paths corrected." diff --git a/Ark.Alliance.StartupCms.Ai.Backend/package-lock.json b/Ark.Alliance.StartupCms.Ai.Backend/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..c412ad50e5086dd78595f424007a869eecc5ee96 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/package-lock.json @@ -0,0 +1,5619 @@ +{ + "name": "@ark-alliance-startupcms-ia/backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@ark-alliance-startupcms-ia/backend", + "version": "1.0.0", + "dependencies": { + "@types/archiver": "^7.0.0", + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.0.0", + "@types/node-forge": "^1.3.14", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", + "archiver": "^7.0.1", + "bcryptjs": "^3.0.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "cors": "^2.8.5", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "helmet": "^6.0.1", + "jsonwebtoken": "^9.0.3", + "multer": "^2.0.2", + "node-forge": "^1.3.3", + "pg": "^8.10.0", + "reflect-metadata": "^0.1.13", + "selfsigned": "^5.4.0", + "sqlite3": "^5.1.7", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "typeorm": "^0.3.12" + }, + "devDependencies": { + "@types/cors": "^2.8.13", + "@types/express": "^4.17.17", + "@types/node": "^18.15.11", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz", + "integrity": "sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.0.tgz", + "integrity": "sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.0.tgz", + "integrity": "sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.0.tgz", + "integrity": "sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.0.tgz", + "integrity": "sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.0.tgz", + "integrity": "sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pfx": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.0.tgz", + "integrity": "sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.0.tgz", + "integrity": "sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.0.tgz", + "integrity": "sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.2.tgz", + "integrity": "sha512-r2w1Hg6pODDs0zfAKHkSS5HLkOLSeburtcgwvlLLWWCixw+MmW3U6kD5ddyvc2Y2YdbGuVwCF2S2ASoU1cFAag==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@peculiar/x509/node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/archiver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", + "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", + "license": "MIT", + "dependencies": { + "@types/readdir-glob": "*" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "devOptional": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asn1js": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", + "integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", + "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.20" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "devOptional": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-6.2.0.tgz", + "integrity": "sha512-DWlwuXLLqbrIOltR6tFQXShj/+7Cyp0gLi6uAb8qMdFh/YBBFbKSgQ6nbXmScYd8emMctuthmgIa7tUfo9Rtyg==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.33", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.33.tgz", + "integrity": "sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkijs": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.3.3.tgz", + "integrity": "sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==", + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.4.0.tgz", + "integrity": "sha512-Yn8qZOOJv+NhcGY19iC+ngW6hlUCNpvWEkrKllXNhmkLgR9fcErm8EqZ/wev7/tiwjKC9qj17Fa/PtBNzb6q8g==", + "license": "MIT", + "dependencies": { + "@peculiar/x509": "^1.14.2", + "pkijs": "^3.3.3" + }, + "engines": { + "node": ">=15.6.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz", + "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-node-dev/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typeorm": { + "version": "0.3.28", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", + "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", + "license": "MIT", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "ansis": "^4.2.0", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "dayjs": "^1.11.19", + "debug": "^4.4.3", + "dedent": "^1.7.0", + "dotenv": "^16.6.1", + "glob": "^10.5.0", + "reflect-metadata": "^0.2.2", + "sha.js": "^2.4.12", + "sql-highlight": "^6.1.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@sap/hana-client": "^2.14.22", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/typeorm/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/typeorm/node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + } + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/package.json b/Ark.Alliance.StartupCms.Ai.Backend/package.json new file mode 100644 index 0000000000000000000000000000000000000000..9c4724d12c3c5e7a7792926b95bd5083233b9a5d --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/package.json @@ -0,0 +1,48 @@ +{ + "name": "@ark-alliance-startupcms-ia/backend", + "version": "1.0.0", + "description": "Backend for Ark Portfolio", + "main": "dist/index.js", + "scripts": { + "start": "node dist/index.js", + "dev": "ts-node-dev -r tsconfig-paths/register --respawn --transpile-only src/index.ts", + "build": "tsc", + "seed": "ts-node -r tsconfig-paths/register src/database/seeds/seed.ts" + }, + "dependencies": { + "@types/archiver": "^7.0.0", + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.0.0", + "@types/node-forge": "^1.3.14", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", + "archiver": "^7.0.1", + "bcryptjs": "^3.0.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "cors": "^2.8.5", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "helmet": "^6.0.1", + "jsonwebtoken": "^9.0.3", + "multer": "^2.0.2", + "node-forge": "^1.3.3", + "pg": "^8.10.0", + "reflect-metadata": "^0.1.13", + "selfsigned": "^5.4.0", + "sqlite3": "^5.1.7", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "typeorm": "^0.3.12" + }, + "devDependencies": { + "@types/cors": "^2.8.13", + "@types/express": "^4.17.17", + "@types/node": "^18.15.11", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.0.0" + } +} \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/revert-to-alias.ps1 b/Ark.Alliance.StartupCms.Ai.Backend/revert-to-alias.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..56f6bfed5d24ccae6beee8261919709fe7c4ea55 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/revert-to-alias.ps1 @@ -0,0 +1,19 @@ +# Revert to using tsconfig path alias since relative paths don't work in Node.js +# The path mapping in tsconfig.json already points @ark/portfolio-share to ../Ark.Portfolio.Share/index.ts + +Get-ChildItem -Path "src" -Recurse -Include *.ts | ForEach-Object { + $content = Get-Content $_.FullName -Raw + + # Replace all relative Share imports back to the alias + $newContent = $content -replace "from '\.\./\.\./Ark\.Portfolio\.Share/index'", "from '@ark/portfolio-share'" + $newContent = $newContent -replace "from '\.\./\.\./\.\./Ark\.Portfolio\.Share/index'", "from '@ark/portfolio-share'" + $newContent = $newContent -replace "from '\.\./\.\./\.\./\.\./Ark\.Portfolio\.Share/enums/admin\.enums'", "from '@ark/portfolio-share'" + + if ($newContent -ne $content) { + Set-Content -Path $_.FullName -Value $newContent -NoNewline + $relativePath = $_.FullName -replace [regex]::Escape("C:\Users\Criprtoswiss\source\repos\Ark.Portfolio\Ark.Portfolio.Backend\"), "" + Write-Host "Reverted: $relativePath" + } +} + +Write-Host "All imports now use @ark/portfolio-share alias (tsconfig path mapping)" diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Armand.jfif b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Armand.jfif new file mode 100644 index 0000000000000000000000000000000000000000..25c22ddaf415a736e6899871f7458de2c9359c22 Binary files /dev/null and b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Armand.jfif differ diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Bryan.jfif b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Bryan.jfif new file mode 100644 index 0000000000000000000000000000000000000000..25d21585feb4c42bcd1dad6655c34abcff00cd64 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Bryan.jfif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4c4a229f87235a174f89f547103440c9c10a9119b7293882d79bfc77d639e4b +size 100388 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Lenny Avril.jpeg b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Lenny Avril.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..88f3fa9e01111f973ab8f20bc10a7bc9558928d3 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Lenny Avril.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a8566d631ef4e43cf817d9cf4704d1d84cbde8ae820eaf21a18189be762525a +size 127588 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Mario.png b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Mario.png new file mode 100644 index 0000000000000000000000000000000000000000..21439f6d4ff46469b5b09f5e333288d9fc22bcb6 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/Mario.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b760e2d9907193ba96910cd3b7856e8862e739bb236bbf7b349cca94ce830210 +size 492061 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/RaduDinulescu.jfif b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/RaduDinulescu.jfif new file mode 100644 index 0000000000000000000000000000000000000000..629faa011b88ea176c40f8632868bf1c277292e7 Binary files /dev/null and b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Profil_Avatars/RaduDinulescu.jfif differ diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.React.Component/components-hero.png b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.React.Component/components-hero.png new file mode 100644 index 0000000000000000000000000000000000000000..6095f22f5d09f6461c42e353af03364a53292100 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.React.Component/components-hero.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:336b801f8ab538800e00f69180727b3ab5a8aba113f4f59b1808dbb7e7b5cd96 +size 4075992 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.StartupCms.AI/ArkAllianceStartupCmsAiHero.png b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.StartupCms.AI/ArkAllianceStartupCmsAiHero.png new file mode 100644 index 0000000000000000000000000000000000000000..4747cce51a26162d827db95c25b9baee45296233 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.StartupCms.AI/ArkAllianceStartupCmsAiHero.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc699238f96ac2a690579685416f3ee31564c5ad4d461309db73c5e11c3bb207 +size 384501 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.StartupCms.AI/ArkAllianceStartupCmsAi_Hero.Png b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.StartupCms.AI/ArkAllianceStartupCmsAi_Hero.Png new file mode 100644 index 0000000000000000000000000000000000000000..9596ab58d11c7823926ccfea3a9339e4dd627261 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.StartupCms.AI/ArkAllianceStartupCmsAi_Hero.Png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad36b3d343cb4fde6896ea5eef472bb0068f94abb1079ae561fc40923aece2cf +size 1309535 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.StartupCms.AI/Google_AI_Studio_2026-01-08T00_41_57.111Z.png b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.StartupCms.AI/Google_AI_Studio_2026-01-08T00_41_57.111Z.png new file mode 100644 index 0000000000000000000000000000000000000000..10d81472dde868f661e7b4541cd9854b8f96504b --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.StartupCms.AI/Google_AI_Studio_2026-01-08T00_41_57.111Z.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5cd68e2a95d444d0016b6ae00830a4bfebe9b802aad2202cdaa150608117187 +size 1276740 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot10.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot10.PNG new file mode 100644 index 0000000000000000000000000000000000000000..545e15c4e95512074d58c91907db7d9c32986997 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot10.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9975e1830cb087bc177751ac162554ebe09ea1ee02dc881b0499954cc6fa13bc +size 321808 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot11.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot11.PNG new file mode 100644 index 0000000000000000000000000000000000000000..2ec8207c17cd6559d1c86786ecd1e013a71d9142 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot11.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39a5c84b9a23784f6ee68772b3292945d557d4681538e1bbe762a50fbaf49747 +size 378429 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot12.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot12.PNG new file mode 100644 index 0000000000000000000000000000000000000000..2ff7f228e3e9359e8a81847e0877c389b4acb3e0 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot12.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:459bb1c112c7a10eb22eb9c9a0d132c4badeaf78e23319e439c119bdc787f376 +size 155251 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot13.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot13.PNG new file mode 100644 index 0000000000000000000000000000000000000000..068b233c1fca51af8aaceef21e3a1862529f0c43 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot13.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:614315756f28c729ae556a3b284460a0d7f63e8e9e3f00f10d752f9dcd840127 +size 170264 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot14.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot14.PNG new file mode 100644 index 0000000000000000000000000000000000000000..0cf2808307669682bfced7468a04ef85b9ae622a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot14.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ac92fa503174a5c4c6685c731a06b77dfe9aa7ff094f8ab3034d68fc8a0c0e1 +size 459307 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot2.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot2.PNG new file mode 100644 index 0000000000000000000000000000000000000000..75fc7e8910a064c50afad413ef8c27bcbf56ae99 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot2.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c25f04b3a256ba08022b0a14e7f3fb8d4c8d5fdb68ab8e5e9390cee46dc753a +size 465243 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot3.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot3.PNG new file mode 100644 index 0000000000000000000000000000000000000000..a5c10232d8c105d6396bc1d03f9a7cecc61dc804 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot3.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b7e290bd1e3c51a836ab9ab613c594a1aa57315dc9a76f961899692e675ed3a +size 397617 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot4.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot4.PNG new file mode 100644 index 0000000000000000000000000000000000000000..6c845c86ac723bbb1e00fc05a65f4c9ed3273a98 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot4.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f322678e0f5a499352eccd3a6a89e0190274fc19ab03ab08ed36c2d8e0731675 +size 213861 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot5.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot5.PNG new file mode 100644 index 0000000000000000000000000000000000000000..d05306ce7a2306af63c75d852f8deedf9544320f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot5.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a74f2a561083dc5041332b70f1ef267ebb3ba2b2fbf72767a84ff7b7e7dec4c +size 215587 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot6.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot6.PNG new file mode 100644 index 0000000000000000000000000000000000000000..4207ff17a1c808bb56db7d3eb524d98fdd33738e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot6.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66435ea297e9f7466e72a3cd1f4671e1e4da1f914521786a3cdcd4987624aad6 +size 505004 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot7.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot7.PNG new file mode 100644 index 0000000000000000000000000000000000000000..fc843ea6466d799b3b000fdf29411bf71a00e75a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot7.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2cea73cf7bf2a1e4dedadad32fc30e2b6720ed513cb0b3c2ee0679aaea46d5a +size 369687 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot8.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot8.PNG new file mode 100644 index 0000000000000000000000000000000000000000..3d7c3f49fa5ae9495584308cf7b4eb1376640a9a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Bot8.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b554f447aa539e11f277200d0979cd6409c6f49bbb09968d043efce36b6ac12 +size 271418 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Capture.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Capture.PNG new file mode 100644 index 0000000000000000000000000000000000000000..fc843ea6466d799b3b000fdf29411bf71a00e75a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/Capture.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2cea73cf7bf2a1e4dedadad32fc30e2b6720ed513cb0b3c2ee0679aaea46d5a +size 369687 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/bot-hero.png b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/bot-hero.png new file mode 100644 index 0000000000000000000000000000000000000000..0f8c5ac5542ac129ee845d2e9b938c9925fefbc8 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/bot-hero.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57a8dd5a0928f512d557c043fa10d68c6b4106fda7dcf61b8a57f538c800d9b9 +size 1070123 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/bot1.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/bot1.PNG new file mode 100644 index 0000000000000000000000000000000000000000..d44cf89a01e264a1cdb87affc4ac800d3606fc40 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Bot/bot1.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcc256615a841b8686d03fa6159e33d37b1a40ccd0365288c946dd9740f0b2de +size 258622 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Providers.Lib/providers-hero.png b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Providers.Lib/providers-hero.png new file mode 100644 index 0000000000000000000000000000000000000000..19506bfe8c45bb2595ab2708c0be1c7ab927b7d1 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Providers.Lib/providers-hero.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fd701b06d1369df334415dddbe30c52070f5df207a025173ef8c65e7715f7c3 +size 1125206 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Providers.Lib/trading-hero.png b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Providers.Lib/trading-hero.png new file mode 100644 index 0000000000000000000000000000000000000000..3cf9cc1d696fb6da18376777e0cfcaa7c1f883b8 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.Trading.Providers.Lib/trading-hero.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83efb733710f1a58fba2767716bd3f17e6c795afd9cd1691adc6c7ef4d58aed7 +size 695430 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.TrendsCalculator/trends-hero.png b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.TrendsCalculator/trends-hero.png new file mode 100644 index 0000000000000000000000000000000000000000..ac9d932c48cb030163a11aceeb5e9b3fec12e204 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.TrendsCalculator/trends-hero.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19b5c66ce45f76dc2214f5e2cb9cebe7daaf44ca32ef8fb4d2d161b695833289 +size 4056660 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.TrendsCalculator/trends-hero.png.jpg b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.TrendsCalculator/trends-hero.png.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ac9d932c48cb030163a11aceeb5e9b3fec12e204 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance.TrendsCalculator/trends-hero.png.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19b5c66ce45f76dc2214f5e2cb9cebe7daaf44ca32ef8fb4d2d161b695833289 +size 4056660 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance/Ark_Alliance_1.png b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance/Ark_Alliance_1.png new file mode 100644 index 0000000000000000000000000000000000000000..a4a551805bd97952063b9e984352f85bd1d119db --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance/Ark_Alliance_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2e202213ddc181cdfcea31d207cdfc5697d0ac9f88415e7250a875f4f92aeb4 +size 2640749 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance/Ark_Alliance_Hero.png b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance/Ark_Alliance_Hero.png new file mode 100644 index 0000000000000000000000000000000000000000..9a26be5f6ba257bf2f7c09eb630bf36459db81ca --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Alliance/Ark_Alliance_Hero.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:119048c8b09932e7ba69e38073e74ff89cb2fe95021c8e6fd11708399322b352 +size 1181782 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_Dashobard.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_Dashobard.PNG new file mode 100644 index 0000000000000000000000000000000000000000..bfd362fe59d376c3280459cb1ddd3ac4e1c11456 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_Dashobard.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5f6573f8cf1d13dfca66b52329455f733935c65ddab4d567656e916ff39f738 +size 170191 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_Hero_Carrousel.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_Hero_Carrousel.PNG new file mode 100644 index 0000000000000000000000000000000000000000..be2323bd1b7e3f8868398a95a24d2ee2602043e0 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_Hero_Carrousel.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fa16b5a96617ec4785fa1070ba68414b6e5e39c031044f774ec298f41d0b62c +size 244080 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_Projects.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_Projects.PNG new file mode 100644 index 0000000000000000000000000000000000000000..99aa3c4d2e5c20ddaac48612147bdf259107e869 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_Projects.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c863dcb37da3cae3e73422ff0a23b80d59af76565922c52b1d1f5291aab5fbbd +size 704598 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_ResumeManager.PNG b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_ResumeManager.PNG new file mode 100644 index 0000000000000000000000000000000000000000..46d988f7247f458eb92c93d6ff3c3e189dad6495 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/Admin_ResumeManager.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55c991692dc9d16b626e2021d9bf3154180ffc5ae18d6ca8e18905bf5b4113fb +size 216510 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/portfolio-hero.png b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/portfolio-hero.png new file mode 100644 index 0000000000000000000000000000000000000000..333e6a393cd893c0acef6a61b3148102c0c1a660 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Projects/Ark.Portfolio/portfolio-hero.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc29d203e8ea95c84865d68f7762c5b23efa501678a8bec35c8bbcbf2bc212b6 +size 4157416 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Site/Icon.png b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Site/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6174b39adc62e80e8d5a1dd409d3af3ba4aa4398 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/Assets/Site/Icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac6bf4eb19783054f288a240b7a185984d3967f25d80ab59fb7e75d9dca2f631 +size 101421 diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/config/database.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/config/database.ts new file mode 100644 index 0000000000000000000000000000000000000000..74346da7097781fe3cb5fb274b087deab6bbf6b5 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/config/database.ts @@ -0,0 +1,107 @@ +/** + * @fileoverview Database Configuration + * TypeORM DataSource configuration with support for SQLite and PostgreSQL. + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import dotenv from 'dotenv'; +import { Profile } from '../database/entities/profile.entity'; +import { Project } from '../database/entities/project.entity'; +import { ProjectTechnology } from '../database/entities/project-technology.entity'; +import { Education } from '../database/entities/education.entity'; +import { Experience } from '../database/entities/experience.entity'; +import { Skill } from '../database/entities/skill.entity'; +import { SkillCategory } from '../database/entities/skill-category.entity'; +import { Widget } from '../database/entities/widget.entity'; +import { Media } from '../database/entities/media.entity'; +import { ProjectPage } from '../database/entities/project-page.entity'; +import { ProjectFeature } from '../database/entities/project-feature.entity'; +import { ProjectController } from '../database/entities/project-controller.entity'; +import { ProjectEndpoint } from '../database/entities/project-endpoint.entity'; +import { TeamMember } from '../database/entities/team-member.entity'; +import { MenuItem } from '../database/entities/menu-item.entity'; +import { StyleConfig } from '../database/entities/style-config.entity'; +import { CarouselItem } from '../database/entities/carousel-item.entity'; +import { User } from '../database/entities/user.entity'; +import { UserRole } from '../database/entities/user-role.entity'; +import { RefreshToken } from '../database/entities/refresh-token.entity'; +import { AuditLog } from '../database/entities/audit-log.entity'; +import { AiSettings } from '../database/entities/ai-settings.entity'; +import { Outbox } from '../database/entities/outbox.entity'; +import { Technology } from '../database/entities/technology.entity'; +import { Language } from '../database/entities/language.entity'; +import { Hobby } from '../database/entities/hobby.entity'; +import { BusinessDomain } from '../database/entities/business-domain.entity'; +import { PageDefinition } from '../database/entities/page-definition.entity'; +import { Theme } from '../database/entities/theme.entity'; +import { Organization } from '../database/entities/organization.entity'; +import { Collaborator } from '../database/entities/collaborator.entity'; +import { Task } from '../database/entities/task.entity'; +import { PromptTemplate } from '../database/entities/prompt-template.entity'; +import { Page } from '../database/entities/page.entity'; +import { SeoMeta } from '../database/entities/seo-meta.entity'; +import { StructuredData } from '../database/entities/structured-data.entity'; +import { SiteSettings } from '../database/entities/site-settings.entity'; + +dotenv.config(); + +/** Database type from environment, defaults to SQLite */ +const dbType = (process.env.DB_TYPE || 'sqlite') as 'sqlite' | 'postgres'; + +/** + * Base TypeORM configuration shared across database types. + */ +const baseConfig = { + synchronize: true, + logging: false, + entities: [ + // User & Auth + User, UserRole, RefreshToken, AuditLog, + // Organization & Team + Organization, Collaborator, TeamMember, Task, + // Profile & Content + Profile, Project, ProjectTechnology, Technology, Education, Experience, + Skill, SkillCategory, Widget, Media, Language, Hobby, BusinessDomain, + // Project Details + ProjectPage, ProjectFeature, ProjectController, ProjectEndpoint, + // UI & Settings + MenuItem, StyleConfig, CarouselItem, PageDefinition, Theme, + // SEO & CMS + Page, SeoMeta, StructuredData, SiteSettings, + // System & AI + AiSettings, Outbox, PromptTemplate + ], + subscribers: [], + migrations: [], +}; + +/** + * PostgreSQL-specific configuration. + */ +const postgresConfig = { + type: 'postgres' as const, + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432'), + username: process.env.DB_USER || 'postgres', + password: process.env.DB_PASSWORD || 'postgres', + database: process.env.DB_NAME || 'ark_portfolio', +}; + +/** + * SQLite-specific configuration. + */ +const sqliteConfig = { + type: 'sqlite' as const, + database: process.env.DB_KEY || './data/ark_portfolio.sqlite', +}; + +/** + * Application DataSource for TypeORM. + * @remarks Automatically selects SQLite or PostgreSQL based on DB_TYPE env variable. + */ +export const AppDataSource = new DataSource({ + ...baseConfig, + ...(dbType === 'postgres' ? postgresConfig : sqliteConfig), +}); + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/config/swagger.config.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/config/swagger.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..897390cdb1d066cc2e7c4d9d6d98cf26661b60ae --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/config/swagger.config.ts @@ -0,0 +1,195 @@ +import swaggerJsdoc from 'swagger-jsdoc'; + +const options: swaggerJsdoc.Options = { + definition: { + openapi: '3.0.0', + info: { + title: 'Ark Portfolio API', + version: '1.0.0', + description: ` +## Ark.Alliance.StartupCms.Ai Backend API + +Complete REST API for the Ark.Alliance.StartupCms.Ai application. + +### API Groups: +- **Public**: Profile, Projects, Resume, Carousel, Technologies +- **Auth**: Login, Token Management +- **Admin**: Full CRUD for all entities (requires JWT) + +### Authentication: +Admin endpoints require JWT Bearer token. Use \`/api/auth/login\` to obtain a token. + `, + contact: { + name: 'Armand Richelet-Kleinberg', + email: 'arkleinberg@gmail.com' + }, + license: { + name: 'MIT', + url: 'https://opensource.org/licenses/MIT' + } + }, + servers: [ + { + url: '/api', + description: 'Current Server (relative)' + }, + { + url: 'http://localhost:3085/api', + description: 'Local Development (HTTP)' + }, + { + url: 'https://localhost:3085/api', + description: 'Local Development (HTTPS)' + } + ], + tags: [ + { name: 'Public - Profile', description: 'Portfolio owner profile information' }, + { name: 'Public - Projects', description: 'Portfolio projects and presentations' }, + { name: 'Public - Resume', description: 'CV/Resume data (experiences, skills, education)' }, + { name: 'Public - Carousel', description: 'Homepage carousel items' }, + { name: 'Public - Technologies', description: 'Technology catalog and metadata' }, + { name: 'Public - Dashboard', description: 'Dashboard and widget data' }, + { name: 'Auth', description: 'Authentication endpoints' }, + { name: 'Admin - Projects', description: 'Project management (CRUD)' }, + { name: 'Admin - Resume', description: 'Resume/CV management' }, + { name: 'Admin - Media', description: 'Media asset management' }, + { name: 'Admin - Carousel', description: 'Carousel item management' }, + { name: 'Admin - Widgets', description: 'Widget configuration' }, + { name: 'Admin - Menu', description: 'Navigation menu management' }, + { name: 'Admin - Styles', description: 'Theme and style configuration' }, + { name: 'Admin - AI', description: 'AI integration settings' } + ], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'JWT token obtained from /api/auth/login' + } + }, + schemas: { + Error: { + type: 'object', + properties: { + message: { type: 'string' }, + error: { type: 'string' } + } + }, + Profile: { + type: 'object', + properties: { + id: { type: 'integer' }, + firstName: { type: 'string' }, + lastName: { type: 'string' }, + title: { type: 'string' }, + overview: { type: 'string' }, + email: { type: 'string' }, + githubUrl: { type: 'string' }, + linkedinUrl: { type: 'string' }, + avatarUrl: { type: 'string' } + } + }, + Project: { + type: 'object', + properties: { + id: { type: 'string' }, + slug: { type: 'string' }, + title: { type: 'string' }, + subtitle: { type: 'string' }, + description: { type: 'string' }, + status: { type: 'string', enum: ['active', 'completed', 'archived'] }, + isFeatured: { type: 'boolean' }, + repositoryUrl: { type: 'string' }, + demoUrl: { type: 'string' }, + packageUrl: { type: 'string' } + } + }, + CarouselItem: { + type: 'object', + properties: { + id: { type: 'integer' }, + title: { type: 'string' }, + subtitle: { type: 'string' }, + imageUrl: { type: 'string' }, + linkUrl: { type: 'string' }, + order: { type: 'integer' } + } + }, + Technology: { + type: 'object', + properties: { + id: { type: 'integer' }, + key: { type: 'string' }, + name: { type: 'string' }, + label: { type: 'string' }, + category: { type: 'string' }, + description: { type: 'string' }, + icon: { type: 'string' }, + color: { type: 'string' }, + website: { type: 'string' } + } + }, + Experience: { + type: 'object', + properties: { + id: { type: 'integer' }, + company: { type: 'string' }, + position: { type: 'string' }, + description: { type: 'string' }, + startDate: { type: 'string', format: 'date' }, + endDate: { type: 'string', format: 'date', nullable: true } + } + }, + Skill: { + type: 'object', + properties: { + id: { type: 'integer' }, + name: { type: 'string' }, + level: { type: 'string' }, + categoryId: { type: 'integer' } + } + }, + Education: { + type: 'object', + properties: { + id: { type: 'integer' }, + institution: { type: 'string' }, + degree: { type: 'string' }, + fieldOfStudy: { type: 'string' }, + startDate: { type: 'string', format: 'date' }, + endDate: { type: 'string', format: 'date' } + } + }, + LoginRequest: { + type: 'object', + required: ['username', 'password'], + properties: { + username: { type: 'string' }, + password: { type: 'string' } + } + }, + LoginResponse: { + type: 'object', + properties: { + token: { type: 'string' }, + user: { + type: 'object', + properties: { + id: { type: 'integer' }, + username: { type: 'string' } + } + } + } + } + } + } + }, + apis: [ + './src/routes/*.ts', + './src/controllers/*.ts' + ] +}; + +export const swaggerSpec = swaggerJsdoc(options); + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/admin.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/admin.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..d4fb14deaa783e232726c12c4ebc47d3e64f028f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/admin.controller.ts @@ -0,0 +1,682 @@ +/** + * @fileoverview Admin Controller + * Handles all administrative API endpoints. + * + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { BaseController } from './base.controller'; +import { AdminProjectService } from '../services/admin-project.service'; +import { AdminResumeService } from '../services/admin-resume.service'; +import { AdminWidgetService } from '../services/admin-widget.service'; +import { AdminMenuService } from '../services/admin-menu.service'; +import { AdminStyleService } from '../services/admin-style.service'; +import { AdminMediaService } from '../services/admin-media.service'; +import { AdminCarouselService } from '../services/admin-carousel.service'; +import { AdminThemeService, AdminThemeDto } from '../services/admin-theme.service'; +import { ThemeAiService, ThemeAiRequest } from '../services/theme-ai.service'; +import { AiService } from '../services/ai.service'; +import { + AdminProjectDto, AdminResumeDto, AdminWidgetDto, AdminMenuItemDto, + AdminStyleConfigDto, UploadMediaDto, ReorderWidgetsDto, ReorderMenuItemsDto, + AdminCarouselItemDto, ReorderCarouselDto, AdminExperienceDto, AdminEducationDto, + AdminSkillDto, SkillCategoryDto, ReorderSkillsDto, AiSettingsDto, + LanguageDto, HobbyDto, BusinessDomainDto +} from '@arkalliance/startupcms-ai-share'; + +export class AdminController extends BaseController { + private projectService: AdminProjectService; + private resumeService: AdminResumeService; + private widgetService: AdminWidgetService; + private menuService: AdminMenuService; + private styleService: AdminStyleService; + private mediaService: AdminMediaService; + private carouselService: AdminCarouselService; + private themeService: AdminThemeService; + private themeAiService: ThemeAiService; + private aiService: AiService; + + constructor() { + super(); + this.projectService = new AdminProjectService(); + this.resumeService = new AdminResumeService(); + this.widgetService = new AdminWidgetService(); + this.menuService = new AdminMenuService(); + this.styleService = new AdminStyleService(); + this.mediaService = new AdminMediaService(); + this.carouselService = new AdminCarouselService(); + this.themeService = new AdminThemeService(); + this.themeAiService = new ThemeAiService(); + this.aiService = new AiService(); + } + + // ============================================ + // Project Endpoints + // ============================================ + + async getProjects(req: Request, res: Response) { + const page = parseInt(req.query.page as string) || 1; + const pageSize = parseInt(req.query.pageSize as string) || 10; + const search = req.query.search as string; + const result = await this.projectService.getAllProjects(page, pageSize, search); + return this.ok(res, result); + } + + async getProject(req: Request, res: Response) { + const result = await this.projectService.getProjectById(req.params.id); + if (!result) return this.notFound(res, 'Project not found'); + return this.ok(res, result); + } + + async createProject(req: Request, res: Response) { + const dto = req.body as AdminProjectDto; + const result = await this.projectService.createProject(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async updateProject(req: Request, res: Response) { + const dto = req.body as AdminProjectDto; + const result = await this.projectService.updateProject(req.params.id, dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async deleteProject(req: Request, res: Response) { + const result = await this.projectService.deleteProject(req.params.id); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // ============================================ + // RESUME Endpoints + // ============================================ + + async getResume(req: Request, res: Response) { + const result = await this.resumeService.getFullResume(); + return this.ok(res, result); + } + + async updateProfile(req: Request, res: Response) { + const dto = req.body as AdminResumeDto['profile']; + const result = await this.resumeService.updateProfile(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // Experience CRUD + async createExperience(req: Request, res: Response) { + const dto = req.body as AdminExperienceDto; + const result = await this.resumeService.createExperience(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async updateExperience(req: Request, res: Response) { + const dto = req.body as AdminExperienceDto; + const result = await this.resumeService.updateExperience(parseInt(req.params.id), dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async deleteExperience(req: Request, res: Response) { + const result = await this.resumeService.deleteExperience(parseInt(req.params.id)); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // Education CRUD + async createEducation(req: Request, res: Response) { + const dto = req.body as AdminEducationDto; + const result = await this.resumeService.createEducation(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async updateEducation(req: Request, res: Response) { + const dto = req.body as AdminEducationDto; + const result = await this.resumeService.updateEducation(parseInt(req.params.id), dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async deleteEducation(req: Request, res: Response) { + const result = await this.resumeService.deleteEducation(parseInt(req.params.id)); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async reorderExperiences(req: Request, res: Response) { + const { experienceIds } = req.body as { experienceIds: number[] }; + const result = await this.resumeService.reorderExperiences(experienceIds); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async reorderEducation(req: Request, res: Response) { + const { educationIds } = req.body as { educationIds: number[] }; + const result = await this.resumeService.reorderEducation(educationIds); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // Skills CRUD + async createSkill(req: Request, res: Response) { + const dto = req.body as AdminSkillDto; + const result = await this.resumeService.createSkill(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async updateSkill(req: Request, res: Response) { + const dto = req.body as AdminSkillDto; + const result = await this.resumeService.updateSkill(parseInt(req.params.id), dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async deleteSkill(req: Request, res: Response) { + const result = await this.resumeService.deleteSkill(parseInt(req.params.id)); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async reorderSkills(req: Request, res: Response) { + const dto = req.body as ReorderSkillsDto; + const result = await this.resumeService.reorderSkills(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // Skill Categories CRUD + async getSkillCategories(req: Request, res: Response) { + const result = await this.resumeService.getSkillCategories(); + return this.ok(res, result); + } + + async createSkillCategory(req: Request, res: Response) { + const dto = req.body as SkillCategoryDto; + const result = await this.resumeService.createSkillCategory(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async updateSkillCategory(req: Request, res: Response) { + const dto = req.body as SkillCategoryDto; + const result = await this.resumeService.updateSkillCategory(parseInt(req.params.id), dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async deleteSkillCategory(req: Request, res: Response) { + const result = await this.resumeService.deleteSkillCategory(parseInt(req.params.id)); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // ============================================ + // Language Endpoints + // ============================================ + + async createLanguage(req: Request, res: Response) { + const dto = req.body as LanguageDto; + const result = await this.resumeService.createLanguage(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async updateLanguage(req: Request, res: Response) { + const dto = req.body as LanguageDto; + const result = await this.resumeService.updateLanguage(parseInt(req.params.id), dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async deleteLanguage(req: Request, res: Response) { + const result = await this.resumeService.deleteLanguage(parseInt(req.params.id)); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async reorderLanguages(req: Request, res: Response) { + const { languageIds } = req.body as { languageIds: number[] }; + const result = await this.resumeService.reorderLanguages(languageIds); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // ============================================ + // Hobby Endpoints + // ============================================ + + async createHobby(req: Request, res: Response) { + const dto = req.body as HobbyDto; + const result = await this.resumeService.createHobby(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async updateHobby(req: Request, res: Response) { + const dto = req.body as HobbyDto; + const result = await this.resumeService.updateHobby(parseInt(req.params.id), dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async deleteHobby(req: Request, res: Response) { + const result = await this.resumeService.deleteHobby(parseInt(req.params.id)); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async reorderHobbies(req: Request, res: Response) { + const { hobbyIds } = req.body as { hobbyIds: number[] }; + const result = await this.resumeService.reorderHobbies(hobbyIds); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // ============================================ + // Business Domain Endpoints + // ============================================ + + async createBusinessDomain(req: Request, res: Response) { + const dto = req.body as BusinessDomainDto; + const result = await this.resumeService.createBusinessDomain(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async updateBusinessDomain(req: Request, res: Response) { + const dto = req.body as BusinessDomainDto; + const result = await this.resumeService.updateBusinessDomain(parseInt(req.params.id), dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async deleteBusinessDomain(req: Request, res: Response) { + const result = await this.resumeService.deleteBusinessDomain(parseInt(req.params.id)); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async reorderBusinessDomains(req: Request, res: Response) { + const { domainIds } = req.body as { domainIds: number[] }; + const result = await this.resumeService.reorderBusinessDomains(domainIds); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // ============================================ + // AI Endpoints + // ============================================ + + async getAiSettings(req: Request, res: Response) { + const result = await this.aiService.getSettings(); + return this.ok(res, result); + } + + async updateAiSettings(req: Request, res: Response) { + const dto = req.body as AiSettingsDto; + const result = await this.aiService.updateSettings(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async testAiConnection(req: Request, res: Response) { + const result = await this.aiService.testConnection(); + return this.ok(res, result); + } + + async getAiProviderModels(req: Request, res: Response) { + const provider = req.params.provider; + const models = await this.aiService.getProviderModels(provider); + return this.ok(res, models); + } + + async organizeSkillsWithAi(req: Request, res: Response) { + const skills = req.body.skills; + const result = await this.aiService.organizeSkills(skills); + return this.ok(res, result); + } + + async improveTextWithAi(req: Request, res: Response) { + const { text, context } = req.body; + const result = await this.aiService.improveDescription(text, context); + return this.ok(res, { result }); + } + + // ============================================ + // Widget Endpoints + // ============================================ + + async getWidgets(req: Request, res: Response) { + const pageId = req.query.pageId as string; + const result = await this.widgetService.getWidgets(pageId); + return this.ok(res, result); + } + + async createWidget(req: Request, res: Response) { + const dto = req.body as AdminWidgetDto; + const result = await this.widgetService.createWidget(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async updateWidget(req: Request, res: Response) { + const dto = req.body as AdminWidgetDto; + const result = await this.widgetService.updateWidget(req.params.id, dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async deleteWidget(req: Request, res: Response) { + const result = await this.widgetService.deleteWidget(req.params.id); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async reorderWidgets(req: Request, res: Response) { + const dto = req.body as ReorderWidgetsDto; + const result = await this.widgetService.reorderWidgets(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // ============================================ + // Menu Endpoints + // ============================================ + + async getMenu(req: Request, res: Response) { + const position = req.query.position as any; + const result = await this.menuService.getMenuByPosition(position); + return this.ok(res, result); + } + + async createMenuItem(req: Request, res: Response) { + const dto = req.body as AdminMenuItemDto; + const result = await this.menuService.createMenuItem(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async updateMenuItem(req: Request, res: Response) { + const dto = req.body as AdminMenuItemDto; + const result = await this.menuService.updateMenuItem(parseInt(req.params.id), dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async deleteMenuItem(req: Request, res: Response) { + const result = await this.menuService.deleteMenuItem(parseInt(req.params.id)); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async reorderMenu(req: Request, res: Response) { + const dto = req.body as ReorderMenuItemsDto; + const result = await this.menuService.reorderMenuItems(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // ============================================ + // Style Endpoints + // ============================================ + + async getStyles(req: Request, res: Response) { + const result = await this.styleService.getAllStyles(); + return this.ok(res, result); + } + + async getActiveStyle(req: Request, res: Response) { + const result = await this.styleService.getActiveStyle(); + return this.ok(res, result); + } + + async createStyle(req: Request, res: Response) { + const dto = req.body as AdminStyleConfigDto; + const result = await this.styleService.createStyle(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async updateStyle(req: Request, res: Response) { + const dto = req.body as AdminStyleConfigDto; + const result = await this.styleService.updateStyle(parseInt(req.params.id), dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async deleteStyle(req: Request, res: Response) { + const result = await this.styleService.deleteStyle(parseInt(req.params.id)); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async activateStyle(req: Request, res: Response) { + const result = await this.styleService.activateStyle(parseInt(req.params.id)); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // ============================================ + // Media Endpoints + // ============================================ + + /** + * Get all media with optional filtering. + * @param req - Express request with query params (type, search, tags, page, pageSize) + * @param res - Express response + */ + async getMedia(req: Request, res: Response) { + const params = { + type: req.query.type as any, + search: req.query.search as string, + tags: req.query.tags ? (req.query.tags as string).split(',') : undefined, + page: parseInt(req.query.page as string) || 1, + pageSize: parseInt(req.query.pageSize as string) || 50 + }; + const result = await this.mediaService.getAllMedia(params); + return this.ok(res, result); + } + + /** + * Get media by unique key. + * @param req - Express request with key param + * @param res - Express response + */ + async getMediaByKey(req: Request, res: Response) { + const result = await this.mediaService.getByKey(req.params.key); + if (!result) return this.notFound(res, 'Media not found'); + return this.ok(res, result); + } + + /** + * Get all available tags. + * @param req - Express request + * @param res - Express response + */ + async getMediaTags(req: Request, res: Response) { + const result = await this.mediaService.getAllTags(); + return this.ok(res, result); + } + + /** + * Upload a new media file. + * @param req - Express request with file and body data + * @param res - Express response + */ + async uploadMedia(req: Request, res: Response) { + if (!req.file) { + return this.fail(res, new Error('No file uploaded')); + } + const data = { + name: req.body.name || req.file.originalname, + key: req.body.key, + altText: req.body.altText, + description: req.body.description, + tags: req.body.tags ? JSON.parse(req.body.tags) : [], + metadata: req.body.metadata ? JSON.parse(req.body.metadata) : undefined, + isPublic: req.body.isPublic !== 'false' + }; + const result = await this.mediaService.createFromUpload(req.file, data); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + /** + * Create media from external URL. + * @param req - Express request with URL and metadata + * @param res - Express response + */ + async createMediaFromUrl(req: Request, res: Response) { + const { url, type, name, key, altText, description, tags, metadata } = req.body; + if (!url || !type || !name) { + return this.fail(res, new Error('url, type, and name are required')); + } + const result = await this.mediaService.createFromUrl(url, type, { + name, key, altText, description, tags, metadata + }); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + /** + * Update media metadata. + * @param req - Express request with id param and body data + * @param res - Express response + */ + async updateMedia(req: Request, res: Response) { + const { name, key, altText, description, tags, metadata, isPublic } = req.body; + const result = await this.mediaService.updateMedia(req.params.id, { + name, key, altText, description, tags, metadata, isPublic + }); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + /** + * Delete media by ID. + * @param req - Express request with id param + * @param res - Express response + */ + async deleteMedia(req: Request, res: Response) { + const result = await this.mediaService.deleteMedia(req.params.id); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // ============================================ + // Carousel Endpoints + // ============================================ + + async getCarousel(req: Request, res: Response) { + const result = await this.carouselService.getAllCarouselItems(); + return this.ok(res, result); + } + + async createCarouselItem(req: Request, res: Response) { + const dto = req.body as AdminCarouselItemDto; + const result = await this.carouselService.createCarouselItem(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async updateCarouselItem(req: Request, res: Response) { + const dto = req.body as AdminCarouselItemDto; + const result = await this.carouselService.updateCarouselItem(parseInt(req.params.id), dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async deleteCarouselItem(req: Request, res: Response) { + const result = await this.carouselService.deleteCarouselItem(parseInt(req.params.id)); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async reorderCarousel(req: Request, res: Response) { + const dto = req.body as ReorderCarouselDto; + const result = await this.carouselService.reorderCarouselItems(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // ============================================ + // Theme Management Endpoints + // ============================================ + + async getThemes(req: Request, res: Response) { + const result = await this.themeService.getAllThemes(); + return this.ok(res, result); + } + + async getTheme(req: Request, res: Response) { + const result = await this.themeService.getThemeById(parseInt(req.params.id)); + if (!result) return this.notFound(res, 'Theme not found'); + return this.ok(res, result); + } + + async createTheme(req: Request, res: Response) { + const dto = req.body as AdminThemeDto; + const result = await this.themeService.createTheme(dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async updateTheme(req: Request, res: Response) { + const dto = req.body as AdminThemeDto; + const result = await this.themeService.updateTheme(parseInt(req.params.id), dto); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async deleteTheme(req: Request, res: Response) { + const result = await this.themeService.deleteTheme(parseInt(req.params.id)); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async setDefaultTheme(req: Request, res: Response) { + const result = await this.themeService.setDefaultTheme(parseInt(req.params.id)); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + async reorderThemes(req: Request, res: Response) { + const { themeIds } = req.body as { themeIds: number[] }; + const result = await this.themeService.reorderThemes(themeIds); + if (!result.success) return this.fail(res, new Error(result.message)); + return this.ok(res, result); + } + + // ============================================ + // Theme AI Endpoints + // ============================================ + + async themeAiChat(req: Request, res: Response) { + const request = req.body as ThemeAiRequest; + const result = await this.themeAiService.chat(request); + if (result.error) return this.fail(res, new Error(result.error)); + return this.ok(res, result); + } + + async themeAiGenerate(req: Request, res: Response) { + const { description } = req.body as { description: string }; + const result = await this.themeAiService.generateTheme(description); + if (result.error) return this.fail(res, new Error(result.error)); + return this.ok(res, result); + } + + async themeAiSuggest(req: Request, res: Response) { + const { css, goal } = req.body as { css: string; goal?: string }; + const result = await this.themeAiService.suggestImprovements(css, goal); + if (result.error) return this.fail(res, new Error(result.error)); + return this.ok(res, result); + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/ai-profile.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/ai-profile.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..e7092f32c353e55cf24b5675f1c4bca8a7338839 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/ai-profile.controller.ts @@ -0,0 +1,188 @@ +/** + * @fileoverview AI Profile Controller + * API endpoints for AI-assisted profile generation and prompt template management. + * + * @module controllers/ai-profile + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { BaseController } from './base.controller'; +import { aiProfileService, AiProfileInput } from '../services/ai-profile.service'; +import { promptTemplateService } from '../services/prompt-template.service'; + +/** + * Controller for AI profile generation endpoints. + * + * @class AiProfileController + * @extends BaseController + */ +export class AiProfileController extends BaseController { + /** + * Generate a profile using AI from description. + * + * @route POST /api/ai/generate-profile + * @access Admin + * + * @param req - Express request with profile input + * @param res - Express response + * @returns Generated profile result + * + * @example + * // Request body + * { + * "firstName": "John", + * "lastName": "Doe", + * "function": "Software Engineer", + * "managerName": "Armand Richelet-Kleinberg", + * "description": "Experienced developer with...", + * "urls": "https://linkedin.com/in/johndoe", + * "avatarBase64": "data:image/jpeg;base64,..." + * } + */ + generateProfile = async (req: Request, res: Response) => { + try { + const input: AiProfileInput = { + firstName: req.body.firstName, + lastName: req.body.lastName, + function: req.body.function, + managerName: req.body.managerName, + description: req.body.description, + urls: req.body.urls, + avatarBase64: req.body.avatarBase64, + userId: req.body.userId, + promptName: req.body.promptName + }; + + // Validate required fields + if (!input.firstName || !input.lastName) { + return this.fail(res, new Error('firstName and lastName are required')); + } + if (!input.function) { + return this.fail(res, new Error('function (job title) is required')); + } + if (!input.description) { + return this.fail(res, new Error('description is required')); + } + + const result = await aiProfileService.generateProfile(input); + + if (result.success) { + return this.ok(res, result); + } else { + return this.fail(res, new Error(result.message)); + } + } catch (error) { + return this.fail(res, error as Error); + } + }; + + /** + * Get all prompt templates. + * + * @route GET /api/ai/prompts + * @access Admin + */ + getPromptTemplates = async (req: Request, res: Response) => { + try { + const templates = await promptTemplateService.getAll(); + return this.ok(res, templates); + } catch (error) { + return this.fail(res, error as Error); + } + }; + + /** + * Get a prompt template by name. + * + * @route GET /api/ai/prompts/:name + * @access Admin + */ + getPromptByName = async (req: Request, res: Response) => { + try { + const { name } = req.params; + const template = await promptTemplateService.getByName(name); + + if (!template) { + return this.notFound(res, 'Prompt template not found'); + } + + return this.ok(res, template); + } catch (error) { + return this.fail(res, error as Error); + } + }; + + /** + * Create a new prompt template. + * + * @route POST /api/ai/prompts + * @access Admin + */ + createPromptTemplate = async (req: Request, res: Response) => { + try { + const { name, description, systemPrompt, clientPromptTemplate, outputFormat } = req.body; + + if (!name || !systemPrompt || !clientPromptTemplate) { + return this.fail(res, new Error('name, systemPrompt, and clientPromptTemplate are required')); + } + + const template = await promptTemplateService.create({ + name, + description, + systemPrompt, + clientPromptTemplate, + outputFormat + }); + + return res.status(201).json(template); + } catch (error) { + return this.fail(res, error as Error); + } + }; + + /** + * Update a prompt template. + * + * @route PUT /api/ai/prompts/:id + * @access Admin + */ + updatePromptTemplate = async (req: Request, res: Response) => { + try { + const id = parseInt(req.params.id); + const template = await promptTemplateService.update(id, req.body); + + if (!template) { + return this.notFound(res, 'Prompt template not found'); + } + + return this.ok(res, template); + } catch (error) { + return this.fail(res, error as Error); + } + }; + + /** + * Delete a prompt template. + * + * @route DELETE /api/ai/prompts/:id + * @access Admin + */ + deletePromptTemplate = async (req: Request, res: Response) => { + try { + const id = parseInt(req.params.id); + const success = await promptTemplateService.delete(id); + + if (!success) { + return this.notFound(res, 'Prompt template not found'); + } + + return this.ok(res, { message: 'Prompt template deleted' }); + } catch (error) { + return this.fail(res, error as Error); + } + }; +} + +// Singleton instance +export const aiProfileController = new AiProfileController(); diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/audit-log.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/audit-log.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..9384b31cdc6f695d6c8a92b8b8fc9603f9a2b166 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/audit-log.controller.ts @@ -0,0 +1,85 @@ +/** + * @fileoverview Audit Log Controller + * API endpoints for viewing audit logs. + * + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { BaseController } from './base.controller'; +import { AuditLogService } from '../services/audit-log.service'; +import { AuthenticatedRequest } from '../middleware/auth.middleware'; +import { AuditLogQueryDto } from '@arkalliance/startupcms-ai-share'; + +/** + * Controller for audit log endpoints. + */ +export class AuditLogController extends BaseController { + private auditLogService: AuditLogService; + + constructor() { + super(); + this.auditLogService = new AuditLogService(); + } + + /** + * GET /api/admin/audit-logs + * Query audit logs with filters (supervisor+ only). + */ + async query(req: AuthenticatedRequest, res: Response) { + try { + const query: AuditLogQueryDto = { + userId: req.query.userId as string, + actions: req.query.actions ? (req.query.actions as string).split(',') as any : undefined, + startDate: req.query.startDate as string, + endDate: req.query.endDate as string, + success: req.query.success !== undefined ? req.query.success === 'true' : undefined, + ipAddress: req.query.ipAddress as string, + page: req.query.page ? parseInt(req.query.page as string) : 1, + limit: req.query.limit ? parseInt(req.query.limit as string) : 50 + }; + + const result = await this.auditLogService.query(query); + return this.ok(res, result); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * GET /api/admin/audit-logs/user/:userId + * Get login history for a specific user (supervisor+ only). + */ + async getUserLoginHistory(req: AuthenticatedRequest, res: Response) { + try { + const { userId } = req.params; + const limit = req.query.limit ? parseInt(req.query.limit as string) : 10; + + const history = await this.auditLogService.getLoginHistory(userId, limit); + return this.ok(res, history); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * GET /api/admin/audit-logs/my-history + * Get current user's own login history. + */ + async getMyLoginHistory(req: AuthenticatedRequest, res: Response) { + try { + const userId = req.user?.id; + if (!userId) { + return this.unauthorized(res, 'Not authenticated'); + } + + const limit = req.query.limit ? parseInt(req.query.limit as string) : 10; + const history = await this.auditLogService.getLoginHistory(userId, limit); + return this.ok(res, history); + } catch (error: any) { + return this.fail(res, error.message); + } + } +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/auth.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/auth.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..92027ec951de4557341ca1c4b22fa35daac09676 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/auth.controller.ts @@ -0,0 +1,64 @@ +import { Request, Response, NextFunction } from 'express'; +import { BaseController } from './base.controller'; +import { AuthService } from '../services/auth.service'; + +export class AuthController extends BaseController { + private authService: AuthService; + + constructor() { + super(); + this.authService = new AuthService(); + } + + async login(req: Request, res: Response) { + const { username, password } = req.body; + + if (!username || !password) { + return this.clientError(res, 'Username and password are required'); + } + + const result = await this.authService.login(username, password); + + if (!result) { + return this.unauthorized(res, 'Invalid credentials'); + } + + return this.ok(res, result); + } + + async getMe(req: Request, res: Response) { + // The user is attached to the request by the auth middleware + const user = (req as any).user; + if (!user) { + return this.unauthorized(res, 'Not authenticated'); + } + return this.ok(res, { + id: user.id, + username: user.username, + role: user.role + }); + } + + async changePassword(req: Request, res: Response) { + const user = (req as any).user; + if (!user) { + return this.unauthorized(res, 'Not authenticated'); + } + + const { oldPassword, newPassword } = req.body; + if (!oldPassword || !newPassword) { + return this.clientError(res, 'Old password and new password are required'); + } + + try { + const success = await this.authService.changePassword(user.id, oldPassword, newPassword); + if (!success) { + return this.clientError(res, 'Invalid old password'); + } + return this.ok(res, { message: 'Password changed successfully' }); + } catch (error: any) { + return this.fail(res, error.message); + } + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/base.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/base.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..a13bd713380c0de7904c0c9c5cc0aae0be55f7a3 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/base.controller.ts @@ -0,0 +1,97 @@ +/** + * @fileoverview Base Controller + * Provides common HTTP response helper methods for all API controllers. + * Implements a consistent response format across the application. + * + * @author Armand Richelet-Kleinberg + */ + +import { Response } from 'express'; + +/** + * Abstract base controller with standardized HTTP response methods. + * All API controllers should extend this class to ensure consistent + * error handling and response formatting. + * + * @example + * ```typescript + * class MyController extends BaseController { + * async getResource(req: Request, res: Response) { + * const data = await service.getData(); + * return data ? this.ok(res, data) : this.notFound(res); + * } + * } + * ``` + */ +export abstract class BaseController { + /** + * Sends a 200 OK response with optional data payload. + * @param res - Express response object + * @param dto - Optional data to include in response body + */ + protected ok(res: Response, dto?: T) { + if (!!dto) { + return res.status(200).json(dto); + } else { + return res.sendStatus(200); + } + } + + /** + * Sends a 201 Created response. + * @param res - Express response object + */ + protected created(res: Response) { + return res.sendStatus(201); + } + + /** + * Sends a 400 Bad Request response. + * @param res - Express response object + * @param message - Optional error message (defaults to 'Bad Request') + */ + protected clientError(res: Response, message?: string) { + return res.status(400).json({ message: message ? message : 'Bad Request' }); + } + + /** + * Sends a 401 Unauthorized response. + * @param res - Express response object + * @param message - Optional error message (defaults to 'Unauthorized') + */ + protected unauthorized(res: Response, message?: string) { + return res.status(401).json({ message: message ? message : 'Unauthorized' }); + } + + /** + * Sends a 403 Forbidden response. + * @param res - Express response object + * @param message - Optional error message (defaults to 'Forbidden') + */ + protected forbidden(res: Response, message?: string) { + return res.status(403).json({ message: message ? message : 'Forbidden' }); + } + + /** + * Sends a 404 Not Found response. + * @param res - Express response object + * @param message - Optional error message (defaults to 'Not Found') + */ + protected notFound(res: Response, message?: string) { + return res.status(404).json({ message: message ? message : 'Not Found' }); + } + + /** + * Sends a 500 Internal Server Error response. + * Logs the error to console for debugging. + * @param res - Express response object + * @param error - Error object or message string + */ + protected fail(res: Response, error: Error | string) { + console.error(error); + return res.status(500).json({ + message: error.toString() + }); + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/carousel.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/carousel.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..e00cd8e6e0a8c3538a91bd7216dc744379078f87 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/carousel.controller.ts @@ -0,0 +1,56 @@ +/** + * @fileoverview Carousel Controller + * Public endpoint for carousel data on homepage. + * + * @author Ark.Alliance + */ + +import { Request, Response } from 'express'; +import { BaseController } from './base.controller'; +import { AdminCarouselService } from '../services/admin-carousel.service'; +import { CarouselSlideDto } from '@arkalliance/startupcms-ai-share'; + +/** + * Public Carousel Controller + * + * Provides public (unauthenticated) access to carousel data for homepage. + * Uses the same data source as admin but only returns active, public-safe fields. + */ +export class CarouselController extends BaseController { + private carouselService: AdminCarouselService; + + constructor() { + super(); + this.carouselService = new AdminCarouselService(); + } + + /** + * Get all active carousel items for public display. + * Maps admin carousel items to public CarouselSlideDto. + * + * @param req - Express request + * @param res - Express response + * @returns Array of CarouselSlideDto + */ + async getCarousel(req: Request, res: Response) { + const items = await this.carouselService.getAllCarouselItems(); + + // Filter only active items and map to public DTO + const slides: CarouselSlideDto[] = items + .filter((item: any) => item.isActive) + .sort((a: any, b: any) => a.order - b.order) + .map((item: any) => ({ + id: item.id, + title: item.title, + subtitle: item.subtitle, + description: item.description, + imageUrl: item.imageUrl, + ctaLabel: item.linkText || 'Learn More', + ctaLink: item.linkUrl || (item.projectId ? `/projects/${item.projectId}` : '/projects'), + })); + + return this.ok(res, slides); + } +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/collaborator.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/collaborator.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..9dfab87490317cf228608c938d6d6bb4ee672e5b --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/collaborator.controller.ts @@ -0,0 +1,135 @@ +/** + * @fileoverview Collaborator Controller + * API endpoints for team member management. + * + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { BaseController } from './base.controller'; +import { CollaboratorService } from '../services/collaborator.service'; +import { AuthenticatedRequest } from '../middleware/auth.middleware'; + +/** + * Controller for collaborator/team endpoints. + */ +export class CollaboratorController extends BaseController { + private collaboratorService: CollaboratorService; + + constructor() { + super(); + this.collaboratorService = new CollaboratorService(); + } + + /** + * GET /api/team + * Get organization chart (public). + * If organizationId is not provided, uses the default (first) organization. + */ + async getOrgChart(req: Request, res: Response) { + try { + let organizationId = req.query.organizationId as string | undefined; + + // If no organizationId provided, get the default organization + if (!organizationId) { + const defaultOrg = await this.collaboratorService.getDefaultOrganization(); + if (!defaultOrg) { + return this.notFound(res, 'No organization found'); + } + organizationId = defaultOrg.id; + } + + const orgChart = await this.collaboratorService.getOrgChart(organizationId); + if (!orgChart) { + return this.notFound(res, 'Organization not found'); + } + return this.ok(res, orgChart); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * GET /api/team/:id + * Get collaborator details (public). + */ + async getById(req: Request, res: Response) { + try { + const { id } = req.params; + const collaborator = await this.collaboratorService.getWithReports(id); + if (!collaborator) { + return this.notFound(res, 'Collaborator not found'); + } + return this.ok(res, collaborator); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * GET /api/admin/collaborators + * List all collaborators (supervisor+ only). + */ + async list(req: AuthenticatedRequest, res: Response) { + try { + const { organizationId } = req.query; + if (!organizationId || typeof organizationId !== 'string') { + return this.clientError(res, 'organizationId query parameter is required'); + } + + const collaborators = await this.collaboratorService.getByOrganization(organizationId); + return this.ok(res, collaborators); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * POST /api/admin/collaborators + * Create a new collaborator (supervisor+ only). + */ + async create(req: AuthenticatedRequest, res: Response) { + try { + const collaborator = await this.collaboratorService.create(req.body); + return this.ok(res, collaborator); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * PUT /api/admin/collaborators/:id + * Update a collaborator (supervisor+ only). + */ + async update(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + const collaborator = await this.collaboratorService.update(id, req.body); + if (!collaborator) { + return this.notFound(res, 'Collaborator not found'); + } + return this.ok(res, collaborator); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * PUT /api/admin/collaborators/:id/hierarchy + * Update collaborator's manager (supervisor+ only). + */ + async updateHierarchy(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + const { reportsToId } = req.body; + + const success = await this.collaboratorService.updateHierarchy(id, reportsToId || null); + if (!success) { + return this.notFound(res, 'Collaborator not found'); + } + return this.ok(res, { message: 'Hierarchy updated successfully' }); + } catch (error: any) { + return this.fail(res, error.message); + } + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/dashboard.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/dashboard.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..edbb83ca5a90e3e1d7b5b35fb34c5bfdaae27a56 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/dashboard.controller.ts @@ -0,0 +1,37 @@ +/** + * @fileoverview Dashboard Controller + * Handles HTTP requests for dashboard/statistics endpoints. + * + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { DashboardService } from '../services/dashboard.service'; +import { BaseController } from './base.controller'; + +/** + * Controller for dashboard endpoints. + * Provides aggregated statistics and activity data. + */ +export class DashboardController extends BaseController { + private dashboardService: DashboardService; + + constructor() { + super(); + this.dashboardService = new DashboardService(); + } + + /** + * GET /api/dashboard + * Retrieves dashboard data including statistics and activity graphs. + */ + public getDashboardData = async (req: Request, res: Response) => { + try { + const data = await this.dashboardService.getDashboardData(); + return this.ok(res, data); + } catch (error) { + return this.fail(res, error as Error); + } + }; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/organization.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/organization.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..9887547c0e150fd9240d9adcf231ff7a70356e70 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/organization.controller.ts @@ -0,0 +1,90 @@ +/** + * @fileoverview Organization Controller + * API endpoints for organization management. + * + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { BaseController } from './base.controller'; +import { OrganizationService } from '../services/organization.service'; +import { AuthenticatedRequest } from '../middleware/auth.middleware'; + +/** + * Controller for organization endpoints. + */ +export class OrganizationController extends BaseController { + private organizationService: OrganizationService; + + constructor() { + super(); + this.organizationService = new OrganizationService(); + } + + /** + * GET /api/organization + * Get the default organization (public). + */ + async getDefault(req: Request, res: Response) { + try { + const org = await this.organizationService.getDefault(); + if (!org) { + return this.notFound(res, 'No organization found'); + } + return this.ok(res, org); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * GET /api/organization/:id + * Get organization by ID. + */ + async getById(req: Request, res: Response) { + try { + const { id } = req.params; + const org = await this.organizationService.getById(id); + if (!org) { + return this.notFound(res, 'Organization not found'); + } + return this.ok(res, org); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * GET /api/admin/organization/:id/stats + * Get organization with statistics (admin only). + */ + async getWithStats(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + const org = await this.organizationService.getWithStats(id); + if (!org) { + return this.notFound(res, 'Organization not found'); + } + return this.ok(res, org); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * PUT /api/admin/organization/:id + * Update organization (admin only). + */ + async update(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + const org = await this.organizationService.update(id, req.body); + if (!org) { + return this.notFound(res, 'Organization not found'); + } + return this.ok(res, org); + } catch (error: any) { + return this.fail(res, error.message); + } + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/page.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/page.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..821e1b531e9fa1abf2340287d532126aa6037d0a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/page.controller.ts @@ -0,0 +1,354 @@ +/** + * @fileoverview Page Controller + * @description REST API controller for CMS page management with SEO support. + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { PageService } from '../services/page.service'; +import { PageType } from '../database/entities/page.entity'; + +const pageService = new PageService(); + +/** + * @swagger + * /api/pages: + * get: + * summary: Get all published pages + * tags: [Pages] + * responses: + * 200: + * description: List of published pages with SEO data + */ +export async function getAllPages(req: Request, res: Response): Promise { + try { + const pages = await pageService.getAllPublishedPages(); + res.json(pages); + } catch (error) { + console.error('Error fetching pages:', error); + res.status(500).json({ error: 'Failed to fetch pages' }); + } +} + +/** + * @swagger + * /api/pages/{slug}: + * get: + * summary: Get page by slug + * tags: [Pages] + * parameters: + * - in: path + * name: slug + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Page with SEO metadata and structured data + * 404: + * description: Page not found + */ +export async function getPageBySlug(req: Request, res: Response): Promise { + try { + const { slug } = req.params; + const page = await pageService.getPageBySlug(slug); + + if (!page) { + res.status(404).json({ error: 'Page not found' }); + return; + } + + res.json(page); + } catch (error) { + console.error('Error fetching page:', error); + res.status(500).json({ error: 'Failed to fetch page' }); + } +} + +/** + * @swagger + * /api/pages/type/{pageType}: + * get: + * summary: Get pages by type + * tags: [Pages] + * parameters: + * - in: path + * name: pageType + * required: true + * schema: + * type: string + * enum: [home, about, blog, project, team, contact, custom] + * responses: + * 200: + * description: List of pages of specified type + */ +export async function getPagesByType(req: Request, res: Response): Promise { + try { + const { pageType } = req.params; + const pages = await pageService.getPagesByType(pageType as PageType); + res.json(pages); + } catch (error) { + console.error('Error fetching pages by type:', error); + res.status(500).json({ error: 'Failed to fetch pages' }); + } +} + +/** + * @swagger + * /api/admin/pages: + * get: + * summary: Get all pages (admin) + * tags: [Pages, Admin] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: List of all pages including unpublished + */ +export async function getAllPagesAdmin(req: Request, res: Response): Promise { + try { + const pages = await pageService.getAllPages(); + res.json(pages); + } catch (error) { + console.error('Error fetching all pages:', error); + res.status(500).json({ error: 'Failed to fetch pages' }); + } +} + +/** + * @swagger + * /api/admin/pages: + * post: + * summary: Create new page + * tags: [Pages, Admin] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - slug + * - title + * - content + * responses: + * 201: + * description: Page created successfully + * 400: + * description: Invalid input or slug already exists + */ +export async function createPage(req: Request, res: Response): Promise { + try { + const { slug, title, content, pageType, isPublished, seoMeta, structuredData } = req.body; + + // Validate required fields + if (!slug || !title || !content) { + res.status(400).json({ error: 'Missing required fields: slug, title, content' }); + return; + } + + // Check if slug is available + const slugAvailable = await pageService.isSlugAvailable(slug); + if (!slugAvailable) { + res.status(400).json({ error: 'Slug already exists' }); + return; + } + + const authorId = (req as any).user?.id; // From auth middleware + + const page = await pageService.createPage({ + slug, + title, + content, + pageType, + isPublished, + authorId, + seoMeta, + structuredData + }); + + res.status(201).json(page); + } catch (error) { + console.error('Error creating page:', error); + res.status(500).json({ error: 'Failed to create page' }); + } +} + +/** + * @swagger + * /api/admin/pages/{id}: + * put: + * summary: Update page + * tags: [Pages, Admin] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Page updated successfully + * 404: + * description: Page not found + */ +export async function updatePage(req: Request, res: Response): Promise { + try { + const { id } = req.params; + const updates = req.body; + + // If updating slug, check availability + if (updates.slug) { + const slugAvailable = await pageService.isSlugAvailable(updates.slug, id); + if (!slugAvailable) { + res.status(400).json({ error: 'Slug already exists' }); + return; + } + } + + const page = await pageService.updatePage(id, updates); + + if (!page) { + res.status(404).json({ error: 'Page not found' }); + return; + } + + res.json(page); + } catch (error) { + console.error('Error updating page:', error); + res.status(500).json({ error: 'Failed to update page' }); + } +} + +/** + * @swagger + * /api/admin/pages/{id}: + * delete: + * summary: Delete page + * tags: [Pages, Admin] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 204: + * description: Page deleted successfully + * 404: + * description: Page not found + */ +export async function deletePage(req: Request, res: Response): Promise { + try { + const { id } = req.params; + const success = await pageService.deletePage(id); + + if (!success) { + res.status(404).json({ error: 'Page not found' }); + return; + } + + res.status(204).send(); + } catch (error) { + console.error('Error deleting page:', error); + res.status(500).json({ error: 'Failed to delete page' }); + } +} + +/** + * @swagger + * /api/admin/pages/{pageId}/seo: + * put: + * summary: Update SEO metadata for a page + * tags: [Pages, Admin, SEO] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: pageId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: SEO metadata updated successfully + * 404: + * description: Page not found + */ +export async function updatePageSeo(req: Request, res: Response): Promise { + try { + const { pageId } = req.params; + const seoData = req.body; + + const seoMeta = await pageService.updateSeoMeta(pageId, seoData); + + if (!seoMeta) { + res.status(404).json({ error: 'Page not found' }); + return; + } + + res.json(seoMeta); + } catch (error) { + console.error('Error updating SEO metadata:', error); + res.status(500).json({ error: 'Failed to update SEO metadata' }); + } +} + +/** + * @swagger + * /api/admin/pages/{pageId}/structured-data: + * post: + * summary: Add structured data to a page + * tags: [Pages, Admin, SEO] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: pageId + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - schemaType + * - schemaData + * responses: + * 201: + * description: Structured data added successfully + * 404: + * description: Page not found + */ +export async function addStructuredData(req: Request, res: Response): Promise { + try { + const { pageId } = req.params; + const { schemaType, schemaData } = req.body; + + if (!schemaType || !schemaData) { + res.status(400).json({ error: 'Missing required fields: schemaType, schemaData' }); + return; + } + + const structuredData = await pageService.addStructuredData(pageId, schemaType, schemaData); + + if (!structuredData) { + res.status(404).json({ error: 'Page not found' }); + return; + } + + res.status(201).json(structuredData); + } catch (error) { + console.error('Error adding structured data:', error); + res.status(500).json({ error: 'Failed to add structured data' }); + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/profile.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/profile.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..853447dd7968346373fe7e3936bf4267ff71d422 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/profile.controller.ts @@ -0,0 +1,35 @@ +/** + * @fileoverview Profile Controller + * Handles HTTP requests for profile/contact info endpoints. + * + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { ProfileService } from '../services/profile.service'; +import { BaseController } from './base.controller'; + +const profileService = new ProfileService(); + +/** + * Controller for profile endpoints. + * Provides access to portfolio owner's personal information. + */ +export class ProfileController extends BaseController { + /** + * GET /api/profile + * Retrieves the portfolio owner's profile information. + */ + async getProfile(req: Request, res: Response) { + try { + const profile = await profileService.getProfile(); + if (!profile) { + return this.notFound(res, 'Profile not found'); + } + return this.ok(res, profile); + } catch (error) { + return this.fail(res, error as Error); + } + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/project-presentation.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/project-presentation.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..18d6aa858f3d24a2c825a60a3d04a2a7d8d364d2 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/project-presentation.controller.ts @@ -0,0 +1,35 @@ +import { Request, Response } from 'express'; +import { ProjectPresentationService } from '../services/project-presentation.service'; +import { BaseController } from './base.controller'; + +export class ProjectPresentationController extends BaseController { + private service: ProjectPresentationService; + + constructor() { + super(); + this.service = new ProjectPresentationService(); + } + + getFullProject = async (req: Request, res: Response) => { + try { + const projectId = req.params.id; + const data = await this.service.getFullProject(projectId); + if (!data) { + return this.notFound(res, 'Project presentation not found'); + } + return this.ok(res, data); + } catch (error) { + return this.fail(res, error as Error); + } + } + + getAllProjects = async (req: Request, res: Response) => { + try { + const projects = await this.service.getAllProjectsSummary(); + res.json(projects); + } catch (error) { + res.status(500).json({ message: 'Error fetching projects', error }); + } + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/project.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/project.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..52261096c84b18f21159687bc66305b0be003fdb --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/project.controller.ts @@ -0,0 +1,63 @@ +/** + * @fileoverview Project Controller + * Handles HTTP requests for project-related endpoints. + * + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { ProjectService } from '../services/project.service'; +import { BaseController } from './base.controller'; + +const projectService = new ProjectService(); + +/** + * Controller for project endpoints. + * Provides CRUD operations for portfolio projects. + */ +export class ProjectController extends BaseController { + /** + * GET /api/projects + * Retrieves all projects with relations. + */ + async getAllProjects(req: Request, res: Response) { + try { + const projects = await projectService.getAllProjects(); + return this.ok(res, projects); + } catch (error) { + return this.fail(res, error as Error); + } + } + + /** + * GET /api/projects/featured + * Retrieves featured projects for homepage carousel. + */ + async getFeaturedProjects(req: Request, res: Response) { + try { + const projects = await projectService.getFeaturedProjects(); + return this.ok(res, projects); + } catch (error) { + return this.fail(res, error as Error); + } + } + + /** + * GET /api/projects/:id + * Retrieves a single project by ID or slug. + * Supports both UUID format and URL-friendly slugs (e.g., 'ark-alliance'). + */ + async getProjectById(req: Request, res: Response) { + try { + const idOrSlug = req.params.id; + const project = await projectService.getProjectByIdOrSlug(idOrSlug); + if (!project) { + return this.notFound(res, 'Project not found'); + } + return this.ok(res, project); + } catch (error) { + return this.fail(res, error as Error); + } + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/resume.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/resume.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..5151ded78b6299fb53d8f40d52e43b8335056da2 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/resume.controller.ts @@ -0,0 +1,34 @@ +/** + * @fileoverview Resume Controller + * Handles HTTP requests for Resume endpoints. + * + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { ResumeService } from '../services/resume.service'; +import { BaseController } from './base.controller'; + +const resumeService = new ResumeService(); + +/** + * Controller for Resume endpoints. + * Provides access to education, experience, and skills data. + */ +export class ResumeController extends BaseController { + /** + * GET /api/resume + * Retrieves complete Resume data including education, experience, and skills. + * @query userId - Optional user ID to fetch resume for specific user + */ + getResume = async (req: Request, res: Response) => { + try { + const userId = req.query.userId as string | undefined; + const resume = await resumeService.getResume(userId); + return this.ok(res, resume); + } catch (error) { + return this.fail(res, error as Error); + } + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/site-settings.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/site-settings.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..5cd15bf879c8d35d59f785c115e5b4323423f86d --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/site-settings.controller.ts @@ -0,0 +1,99 @@ +/** + * @fileoverview Site Settings Controller + * @description REST API controller for global site configuration. + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { SiteSettingsService } from '../services/site-settings.service'; + +const siteSettingsService = new SiteSettingsService(); + +/** + * @swagger + * /api/site-settings: + * get: + * summary: Get site settings + * tags: [Settings] + * responses: + * 200: + * description: Global site settings + */ +export async function getSettings(req: Request, res: Response): Promise { + try { + const settings = await siteSettingsService.getSettings(); + res.json(settings); + } catch (error) { + console.error('Error fetching site settings:', error); + res.status(500).json({ error: 'Failed to fetch site settings' }); + } +} + +/** + * @swagger + * /api/admin/site-settings: + * put: + * summary: Update site settings + * tags: [Settings, Admin] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * responses: + * 200: + * description: Settings updated successfully + */ +export async function updateSettings(req: Request, res: Response): Promise { + try { + const updates = req.body; + const settings = await siteSettingsService.updateSettings(updates); + res.json(settings); + } catch (error) { + console.error('Error updating site settings:', error); + res.status(500).json({ error: 'Failed to update site settings' }); + } +} + +/** + * @swagger + * /api/seo/organization-schema: + * get: + * summary: Get Organization JSON-LD schema + * tags: [SEO] + * responses: + * 200: + * description: Organization schema in JSON-LD format + */ +export async function getOrganizationSchema(req: Request, res: Response): Promise { + try { + const schema = await siteSettingsService.getOrganizationSchema(); + res.json(schema); + } catch (error) { + console.error('Error generating organization schema:', error); + res.status(500).json({ error: 'Failed to generate organization schema' }); + } +} + +/** + * @swagger + * /api/seo/website-schema: + * get: + * summary: Get WebSite JSON-LD schema + * tags: [SEO] + * responses: + * 200: + * description: WebSite schema in JSON-LD format + */ +export async function getWebSiteSchema(req: Request, res: Response): Promise { + try { + const schema = await siteSettingsService.getWebSiteSchema(); + res.json(schema); + } catch (error) { + console.error('Error generating website schema:', error); + res.status(500).json({ error: 'Failed to generate website schema' }); + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/sitemap.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/sitemap.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8b818d144df517922105c2cbae626b450014162 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/sitemap.controller.ts @@ -0,0 +1,155 @@ +/** + * @fileoverview Sitemap & Robots Controller + * @description Generates dynamic sitemap.xml and robots.txt for SEO crawlers. + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { PageService } from '../services/page.service'; +import { ProjectService } from '../services/project.service'; +import { SiteSettingsService } from '../services/site-settings.service'; + +const pageService = new PageService(); +const projectService = new ProjectService(); +const siteSettingsService = new SiteSettingsService(); + +/** + * Generate sitemap JSON for frontend sitemap module + * @swagger + * /api/sitemap: + * get: + * summary: Get sitemap data + * tags: [SEO] + * responses: + * 200: + * description: Array of URL objects for sitemap + */ +export async function getSitemapData(req: Request, res: Response): Promise { + try { + const settings = await siteSettingsService.getSettings(); + const baseUrl = settings.siteUrl; + + const urls: Array<{ + loc: string; + lastmod?: string; + changefreq?: string; + priority?: number; + }> = []; + + // Add pages + const pages = await pageService.getAllPublishedPages(); + for (const page of pages) { + urls.push({ + loc: `${baseUrl}/${page.slug}`, + lastmod: page.updatedAt.toISOString(), + changefreq: page.pageType === 'home' ? 'weekly' : 'monthly', + priority: page.pageType === 'home' ? 1.0 : 0.8 + }); + } + + // Add projects + const projects = await projectService.getAllProjects(); + for (const project of projects) { + const slug = project.title + .toLowerCase() + .replace(/\./g, '-') + .replace(/\s+/g, '-') + .replace(/-+/g, '-'); + + urls.push({ + loc: `${baseUrl}/projects/${slug}`, + lastmod: project.endDate ? new Date(project.endDate).toISOString() : new Date().toISOString(), + changefreq: 'monthly', + priority: project.isFeatured ? 0.9 : 0.7 + }); + } + + // Add static pages + const staticPages = [ + { loc: `${baseUrl}/`, priority: 1.0, changefreq: 'weekly' }, + { loc: `${baseUrl}/projects`, priority: 0.9, changefreq: 'weekly' }, + { loc: `${baseUrl}/team`, priority: 0.8, changefreq: 'monthly' }, + { loc: `${baseUrl}/contact`, priority: 0.7, changefreq: 'monthly' } + ]; + + urls.push(...staticPages); + + res.json(urls); + } catch (error) { + console.error('Error generating sitemap:', error); + res.status(500).json({ error: 'Failed to generate sitemap' }); + } +} + +/** + * Generate robots.txt with AI crawler allowlist + * @swagger + * /robots.txt: + * get: + * summary: Get robots.txt + * tags: [SEO] + * responses: + * 200: + * description: robots.txt content + * content: + * text/plain: + * schema: + * type: string + */ +export async function getRobotsTxt(req: Request, res: Response): Promise { + try { + const settings = await siteSettingsService.getSettings(); + const sitemapUrl = `${settings.siteUrl}/sitemap.xml`; + + const robotsTxt = `# Ark Alliance CMS - SEO & AEO Optimized +# Allow all standard search engines and AI crawlers + +User-agent: * +Allow: / + +# Google +User-agent: Googlebot +Allow: / + +# Bing/Yahoo +User-agent: Bingbot +Allow: / + +# OpenAI (ChatGPT Search) +User-agent: GPTBot +Allow: / + +# Anthropic (Claude) +User-agent: ClaudeBot +Allow: / + +User-agent: claude-web +Allow: / + +User-agent: anthropic-ai +Allow: / + +# Perplexity +User-agent: PerplexityBot +Allow: / + +# Other AI Crawlers +User-agent: Bytespider +Allow: / + +User-agent: CCBot +Allow: / + +# Google-Extended (Gemini AI) - implicit allow via absence of Disallow + +# Sitemap +Sitemap: ${sitemapUrl} +`; + + res.setHeader('Content-Type', 'text/plain'); + res.send(robotsTxt); + } catch (error) { + console.error('Error generating robots.txt:', error); + res.status(500).send('# Error generating robots.txt'); + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/task.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/task.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7ec0a38a5e297256fc67717e47da1dec228fc30 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/task.controller.ts @@ -0,0 +1,174 @@ +/** + * @fileoverview Task Controller + * API endpoints for task/accomplishment management. + * + * Routes: + * - GET /api/tasks/public/:collaboratorId - Public accomplishments (Achieved + Lessons) + * - GET /api/tasks/public/:collaboratorId/summary - Public summary stats + * - GET /api/admin/tasks/:collaboratorId - All tasks (internal) + * - GET /api/admin/tasks/detail/:id - Single task detail + * - POST /api/admin/tasks/:collaboratorId - Create task + * - PUT /api/admin/tasks/:id - Update task + * - POST /api/admin/tasks/:id/rate - Add peer rating + * - DELETE /api/admin/tasks/:id - Delete task + * + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { BaseController } from './base.controller'; +import { TaskService } from '../services/task.service'; +import { AuthenticatedRequest } from '../middleware/auth.middleware'; + +/** + * Controller for task/accomplishment endpoints. + * Separates public (recognition) and admin (management) routes. + */ +export class TaskController extends BaseController { + private taskService: TaskService; + + constructor() { + super(); + this.taskService = new TaskService(); + } + + // ═══════════════════════════════════════════════════════════════════════ + // PUBLIC ROUTES (Recognition - External Resume View) + // ═══════════════════════════════════════════════════════════════════════ + + /** + * GET /api/tasks/public/:collaboratorId + * Get public accomplishments for a collaborator. + * Only returns Achieved tasks and Mistakes with lessons learned. + */ + async getPublicTasks(req: Request, res: Response) { + try { + const { collaboratorId } = req.params; + const tasks = await this.taskService.getPublicTasks(collaboratorId); + return this.ok(res, tasks); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * GET /api/tasks/public/:collaboratorId/summary + * Get public accomplishments summary statistics. + */ + async getPublicSummary(req: Request, res: Response) { + try { + const { collaboratorId } = req.params; + const summary = await this.taskService.getPublicSummary(collaboratorId); + return this.ok(res, summary); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // ADMIN ROUTES (Task Management - Authenticated) + // ═══════════════════════════════════════════════════════════════════════ + + /** + * GET /api/admin/tasks/:collaboratorId + * Get all tasks for a collaborator (internal view). + * Includes all statuses: backlog, ongoing, achieved, mistake. + */ + async getAllTasks(req: AuthenticatedRequest, res: Response) { + try { + const { collaboratorId } = req.params; + const tasks = await this.taskService.getAllTasks(collaboratorId); + return this.ok(res, tasks); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * GET /api/admin/tasks/detail/:id + * Get a single task by ID. + */ + async getById(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + const task = await this.taskService.getById(id); + if (!task) { + return this.notFound(res, 'Task not found'); + } + return this.ok(res, task); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * POST /api/admin/tasks/:collaboratorId + * Create a new task for a collaborator. + */ + async create(req: AuthenticatedRequest, res: Response) { + try { + const { collaboratorId } = req.params; + const task = await this.taskService.create(collaboratorId, req.body); + return this.ok(res, task); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * PUT /api/admin/tasks/:id + * Update a task. + */ + async update(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + const task = await this.taskService.update(id, req.body); + if (!task) { + return this.notFound(res, 'Task not found'); + } + return this.ok(res, task); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * POST /api/admin/tasks/:id/rate + * Add a peer rating to a task. + */ + async addRating(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + const { rating } = req.body; + + if (typeof rating !== 'number' || rating < 1 || rating > 5) { + return this.clientError(res, 'Rating must be a number between 1 and 5'); + } + + const task = await this.taskService.addRating(id, rating); + if (!task) { + return this.notFound(res, 'Task not found'); + } + return this.ok(res, task); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * DELETE /api/admin/tasks/:id + * Delete a task. + */ + async delete(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + const success = await this.taskService.delete(id); + if (!success) { + return this.notFound(res, 'Task not found'); + } + return this.ok(res, { message: 'Task deleted successfully' }); + } catch (error: any) { + return this.fail(res, error.message); + } + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/technology.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/technology.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab9b0aa9dd5237675ff70ef95892658f4e0ff4ae --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/technology.controller.ts @@ -0,0 +1,169 @@ +/** + * @fileoverview Technology API Controller + * Provides endpoints for retrieving technology master data + * + * @module controllers/technology.controller + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import * as fs from 'fs'; +import * as path from 'path'; +import { TechnologiesResponseDto, TechnologyDto, TechnologyCategoryDto } from '@arkalliance/startupcms-ai-share'; + +/** + * In-memory cache for technologies data + * Loaded once on first request to avoid repeated file reads + */ +let technologiesCache: TechnologiesResponseDto | null = null; + +/** + * Load technologies from JSON file + * Caches result for subsequent requests + */ +function loadTechnologies(): TechnologiesResponseDto { + if (technologiesCache) { + return technologiesCache; + } + + const dataPath = path.join(__dirname, '../database/InitDbAsset/JsonDatas/technologies.json'); + const rawData = fs.readFileSync(dataPath, 'utf-8'); + const data = JSON.parse(rawData); + + // Build response with categories and flat technology list + const categories: TechnologyCategoryDto[] = data.categories.map((cat: any) => ({ + id: cat.id, + name: cat.name, + description: cat.description, + order: cat.order, + technologies: data.technologies + .filter((tech: any) => tech.category === cat.id) + .map((tech: any) => ({ + key: tech.key, + name: tech.name, + label: tech.label, + category: tech.category, + description: tech.description, + icon: tech.icon, + color: tech.color, + website: tech.website, + versions: tech.versions + } as TechnologyDto)) + })); + + const allTechnologies: TechnologyDto[] = data.technologies.map((tech: any) => ({ + key: tech.key, + name: tech.name, + label: tech.label, + category: tech.category, + description: tech.description, + icon: tech.icon, + color: tech.color, + website: tech.website, + versions: tech.versions + })); + + technologiesCache = { + categories, + technologies: allTechnologies + }; + + return technologiesCache; +} + +/** + * GET /api/technologies + * Returns all technologies grouped by categories + * + * @param req - Express request + * @param res - Express response + * + * @swagger + * /api/technologies: + * get: + * summary: Get all technologies + * tags: [Technologies] + * responses: + * 200: + * description: Technologies grouped by categories + * content: + * application/json: + * schema: + * type: object + * properties: + * categories: + * type: array + * items: + * $ref: '#/components/schemas/TechnologyCategory' + * technologies: + * type: array + * items: + * $ref: '#/components/schemas/Technology' + */ +export const getAllTechnologies = async (req: Request, res: Response): Promise => { + try { + const data = loadTechnologies(); + res.json(data); + } catch (error) { + console.error('Error loading technologies:', error); + res.status(500).json({ error: 'Failed to load technologies' }); + } +}; + +/** + * GET /api/technologies/:key + * Returns a single technology by its key + * + * @param req - Express request with key parameter + * @param res - Express response + * + * @swagger + * /api/technologies/{key}: + * get: + * summary: Get technology by key + * tags: [Technologies] + * parameters: + * - in: path + * name: key + * required: true + * schema: + * type: string + * description: Technology key (e.g., 'react', 'typescript') + * responses: + * 200: + * description: Technology details + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Technology' + * 404: + * description: Technology not found + */ +export const getTechnologyByKey = async (req: Request, res: Response): Promise => { + try { + const { key } = req.params; + const data = loadTechnologies(); + + const technology = data.technologies.find(t => t.key === key); + + if (!technology) { + res.status(404).json({ error: `Technology '${key}' not found` }); + return; + } + + res.json(technology); + } catch (error) { + console.error('Error finding technology:', error); + res.status(500).json({ error: 'Failed to find technology' }); + } +}; + +/** + * Clear cache (for development/testing) + * Not exposed as public endpoint + */ +export const clearCache = (): void => { + technologiesCache = null; +}; + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/theme.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/theme.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..aeb54a70c42bc4ef073d5e68ba53daf0157087af --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/theme.controller.ts @@ -0,0 +1,65 @@ +/** + * @fileoverview Theme Controller + * Handles HTTP requests for theme-related endpoints. + * + * @module controllers/theme.controller + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { ThemeService } from '../services/theme.service'; +import { BaseController } from './base.controller'; + +const themeService = new ThemeService(); + +/** + * Controller for theme endpoints. + * Provides read-only access to themes for frontend consumption. + */ +export class ThemeController extends BaseController { + /** + * GET /api/themes + * Retrieves all active themes (without CSS content for performance). + */ + async listThemes(req: Request, res: Response) { + try { + const themes = await themeService.listThemes(); + return this.ok(res, themes); + } catch (error) { + return this.fail(res, error as Error); + } + } + + /** + * GET /api/themes/default + * Retrieves the default theme with full CSS content. + */ + async getDefaultTheme(req: Request, res: Response) { + try { + const theme = await themeService.getDefaultTheme(); + if (!theme) { + return this.notFound(res, 'No default theme configured'); + } + return this.ok(res, theme); + } catch (error) { + return this.fail(res, error as Error); + } + } + + /** + * GET /api/themes/:slug + * Retrieves a specific theme by slug with full CSS content. + */ + async getThemeBySlug(req: Request, res: Response) { + try { + const { slug } = req.params; + const theme = await themeService.getThemeBySlug(slug); + if (!theme) { + return this.notFound(res, `Theme '${slug}' not found`); + } + return this.ok(res, theme); + } catch (error) { + return this.fail(res, error as Error); + } + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/user-management.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/user-management.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea4e5bc70c1b0961c27e9bde3094890d2ae548cd --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/user-management.controller.ts @@ -0,0 +1,218 @@ +/** + * @fileoverview User Management Controller + * API endpoints for admin user and role management. + * + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { BaseController } from './base.controller'; +import { RoleService } from '../services/role.service'; +import { AuthenticatedRequest } from '../middleware/auth.middleware'; +import { Role } from '@arkalliance/startupcms-ai-share'; +import { AppDataSource } from '../config/database'; +import { User } from '../database/entities/user.entity'; + +/** + * Controller for user management endpoints. + */ +export class UserManagementController extends BaseController { + private roleService: RoleService; + + constructor() { + super(); + this.roleService = new RoleService(); + } + + /** + * GET /api/admin/users + * List all users (supervisor+ only). + */ + async list(req: AuthenticatedRequest, res: Response) { + try { + const userRepo = AppDataSource.getRepository(User); + const users = await userRepo.find({ + relations: ['userRoles'], + order: { createdAt: 'DESC' } + }); + + const result = users.map(user => ({ + id: user.id, + username: user.username, + email: user.email, + avatarUrl: user.avatarUrl, + roles: user.roles, + isActive: user.isActive, + emailConfirmed: user.emailConfirmed, + lastLogin: user.lastLogin?.toISOString(), + isLocked: user.isLocked, + createdAt: user.createdAt.toISOString() + })); + + return this.ok(res, result); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * GET /api/admin/users/:id + * Get user details (supervisor+ only). + */ + async getById(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + const userRepo = AppDataSource.getRepository(User); + const user = await userRepo.findOne({ + where: { id }, + relations: ['userRoles'] + }); + + if (!user) { + return this.notFound(res, 'User not found'); + } + + return this.ok(res, { + id: user.id, + username: user.username, + email: user.email, + avatarUrl: user.avatarUrl, + roles: user.roles, + isActive: user.isActive, + emailConfirmed: user.emailConfirmed, + lastLogin: user.lastLogin?.toISOString(), + failedLoginAttempts: user.failedLoginAttempts, + isLocked: user.isLocked, + lockoutUntil: user.lockoutUntil?.toISOString(), + collaboratorId: user.collaboratorId, + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt.toISOString() + }); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * POST /api/admin/users/:id/roles + * Assign a role to a user (supervisor+ only, with permission checks). + */ + async assignRole(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + const { role } = req.body; + + if (!role || !Object.values(Role).includes(role)) { + return this.clientError(res, 'Invalid role specified'); + } + + const currentUser = req.user!; + const result = await this.roleService.assignRole( + id, + role, + currentUser.id, + currentUser.roles, + req.ip + ); + + if (!result.success) { + return this.forbidden(res, result.error || 'Failed to assign role'); + } + + return this.ok(res, { message: 'Role assigned successfully' }); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * DELETE /api/admin/users/:id/roles/:role + * Revoke a role from a user (supervisor+ only, with permission checks). + */ + async revokeRole(req: AuthenticatedRequest, res: Response) { + try { + const { id, role } = req.params; + + if (!Object.values(Role).includes(role as Role)) { + return this.clientError(res, 'Invalid role specified'); + } + + const currentUser = req.user!; + const result = await this.roleService.revokeRole( + id, + role as Role, + currentUser.id, + currentUser.roles, + req.ip + ); + + if (!result.success) { + return this.forbidden(res, result.error || 'Failed to revoke role'); + } + + return this.ok(res, { message: 'Role revoked successfully' }); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * POST /api/admin/users/:id/unlock + * Unlock a locked user account (admin only). + */ + async unlockUser(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + const { resetFailedAttempts = true } = req.body; + + const userRepo = AppDataSource.getRepository(User); + const user = await userRepo.findOne({ where: { id } }); + + if (!user) { + return this.notFound(res, 'User not found'); + } + + user.lockoutUntil = undefined; + if (resetFailedAttempts) { + user.failedLoginAttempts = 0; + } + await userRepo.save(user); + + return this.ok(res, { message: 'User account unlocked' }); + } catch (error: any) { + return this.fail(res, error.message); + } + } + + /** + * POST /api/admin/users/:id/avatar + * Update user avatar URL (admin only). + */ + async updateAvatar(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + const { avatarUrl } = req.body; + + const userRepo = AppDataSource.getRepository(User); + const user = await userRepo.findOne({ where: { id } }); + + if (!user) { + return this.notFound(res, 'User not found'); + } + + user.avatarUrl = avatarUrl; + await userRepo.save(user); + + return this.ok(res, { + id: user.id, + username: user.username, + avatarUrl: user.avatarUrl, + message: 'Avatar updated successfully' + }); + } catch (error: any) { + return this.fail(res, error.message); + } + } +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/user-profile.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/user-profile.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..8ff3188e83f53bdf4ff0f7950781f9f27c15bb28 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/user-profile.controller.ts @@ -0,0 +1,371 @@ +/** + * @fileoverview User Profile Controller + * Complete CRUD operations for User with nested Collaborator and resume data. + * Includes avatar base64 conversion for API responses. + * + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response } from 'express'; +import { AppDataSource } from '../config/database'; +import { User } from '../database/entities/user.entity'; +import { Collaborator } from '../database/entities/collaborator.entity'; +import { Experience } from '../database/entities/experience.entity'; +import { Education } from '../database/entities/education.entity'; +import { Skill } from '../database/entities/skill.entity'; +import { Language } from '../database/entities/language.entity'; +import { Hobby } from '../database/entities/hobby.entity'; +import { BusinessDomain } from '../database/entities/business-domain.entity'; +import { UserRole } from '../database/entities/user-role.entity'; +import { BaseController } from './base.controller'; +import { AuthenticatedRequest } from '../middleware/auth.middleware'; +import bcrypt from 'bcryptjs'; +import { convertImageToBase64 } from '../utils/image-converter'; + +/** + * Controller for comprehensive user profile management + */ +export class UserProfileController extends BaseController { + + /** + * GET /api/users/:id/profile + * Get complete user profile with all nested data and avatar as base64 + */ + async getProfile(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + + const userRepo = AppDataSource.getRepository(User); + const user = await userRepo.findOne({ + where: { id }, + relations: [ + 'userRoles', + 'experiences', + 'educations', + 'skills', + 'languages', + 'hobbies', + 'businessDomains' + ] + }); + + if (!user) { + return this.notFound(res, 'User not found'); + } + + // Load collaborator if linked + let collaborator = null; + if (user.collaboratorId) { + const collabRepo = AppDataSource.getRepository(Collaborator); + collaborator = await collabRepo.findOne({ + where: { id: user.collaboratorId }, + relations: ['reportsTo', 'directReports', 'organization'] + }); + } + + // Convert avatar to base64 + let avatarBase64: string | null = null; + if (user.avatarUrl) { + try { + avatarBase64 = await convertImageToBase64(user.avatarUrl); + } catch (error) { + console.warn(`Failed to convert avatar for user ${id}:`, error); + } + } + + // Build response + const response = { + // User fields + id: user.id, + username: user.username, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + fullName: user.fullName, + bio: user.bio, + title: user.title, + avatarUrl: user.avatarUrl, + avatarBase64, // Base64-encoded avatar + linkedinUrl: user.linkedinUrl, + githubUrl: user.githubUrl, + twitterUrl: user.twitterUrl, + + // Account status + isActive: user.isActive, + emailConfirmed: user.emailConfirmed, + lastLogin: user.lastLogin?.toISOString(), + + // Roles + roles: user.roles, + + // Linked collaborator + collaboratorId: user.collaboratorId, + collaborator: collaborator ? { + id: collaborator.id, + firstName: collaborator.firstName, + lastName: collaborator.lastName, + fullName: collaborator.fullName, + email: collaborator.email, + position: collaborator.position, + department: collaborator.department, + bio: collaborator.bio, + avatarUrl: collaborator.avatarUrl, + linkedinUrl: collaborator.linkedinUrl, + githubUrl: collaborator.githubUrl, + phone: collaborator.phone, + hireDate: collaborator.hireDate?.toISOString(), + organizationId: collaborator.organizationId, + reportsToId: collaborator.reportsToId, + reportsTo: collaborator.reportsTo ? { + id: collaborator.reportsTo.id, + fullName: collaborator.reportsTo.fullName, + position: collaborator.reportsTo.position + } : null + } : null, + + // Resume data + experiences: user.experiences.map(exp => ({ + id: exp.id, + company: exp.company, + position: exp.position, + project: exp.project, + startDate: exp.startDate?.toISOString(), + endDate: exp.endDate?.toISOString(), + description: exp.description, + technologies: exp.technologies, + isHighlighted: exp.isHighlighted, + displayOrder: exp.displayOrder + })), + + educations: user.educations.map(edu => ({ + id: edu.id, + institution: edu.institution, + degree: edu.degree, + fieldOfStudy: edu.fieldOfStudy, + startDate: edu.startDate?.toISOString(), + endDate: edu.endDate?.toISOString(), + description: edu.description + })), + + skills: user.skills.map(skill => ({ + id: skill.id, + name: skill.name, + level: skill.level, + yearsOfExperience: skill.yearsOfExperience + })), + + languages: user.languages.map(lang => ({ + id: lang.id, + language: lang.language, + speaking: lang.speaking, + writing: lang.writing, + presenting: lang.presenting + })), + + hobbies: user.hobbies.map(hobby => ({ + id: hobby.id, + name: hobby.name, + description: hobby.description, + icon: hobby.icon, + displayOrder: hobby.displayOrder + })), + + businessDomains: user.businessDomains.map(domain => ({ + id: domain.id, + domain: domain.domain, + level: domain.level, + yearsOfExperience: domain.yearsOfExperience, + description: domain.description + })), + + // Timestamps + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt.toISOString() + }; + + return this.ok(res, response); + } catch (error: any) { + console.error('Error fetching user profile:', error); + return this.fail(res, error.message); + } + } + + /** + * POST /api/users/profile + * Create or update user with nested data + */ + async createOrUpdateProfile(req: AuthenticatedRequest, res: Response) { + try { + const data = req.body; + + return await AppDataSource.transaction(async (transactionalEntityManager) => { + const userRepo = transactionalEntityManager.getRepository(User); + const collaboratorRepo = transactionalEntityManager.getRepository(Collaborator); + const experienceRepo = transactionalEntityManager.getRepository(Experience); + const educationRepo = transactionalEntityManager.getRepository(Education); + const skillRepo = transactionalEntityManager.getRepository(Skill); + const languageRepo = transactionalEntityManager.getRepository(Language); + const hobbyRepo = transactionalEntityManager.getRepository(Hobby); + const domainRepo = transactionalEntityManager.getRepository(BusinessDomain); + const userRoleRepo = transactionalEntityManager.getRepository(UserRole); + + // Find or create user + let user: User; + let isNewUser = false; + + if (data.id) { + const existing = await userRepo.findOne({ where: { id: data.id } }); + if (!existing) { + return this.notFound(res, 'User not found'); + } + user = existing; + } else { + isNewUser = true; + user = userRepo.create(); + } + + // Update user fields + user.username = data.username || user.username; + user.email = data.email || user.email; + user.firstName = data.firstName !== undefined ? data.firstName : user.firstName; + user.lastName = data.lastName !== undefined ? data.lastName : user.lastName; + user.bio = data.bio !== undefined ? data.bio : user.bio; + user.title = data.title !== undefined ? data.title : user.title; + user.avatarUrl = data.avatarUrl !== undefined ? data.avatarUrl : user.avatarUrl; + user.linkedinUrl = data.linkedinUrl !== undefined ? data.linkedinUrl : user.linkedinUrl; + user.githubUrl = data.githubUrl !== undefined ? data.githubUrl : user.githubUrl; + user.twitterUrl = data.twitterUrl !== undefined ? data.twitterUrl : user.twitterUrl; + + // Hash password if provided + if (data.password) { + user.passwordHash = await bcrypt.hash(data.password, 10); + } + + await userRepo.save(user); + + // Handle roles + if (data.roles && Array.isArray(data.roles)) { + // Remove existing roles + await userRoleRepo.delete({ userId: user.id }); + + // Add new roles + for (const role of data.roles) { + const userRole = userRoleRepo.create({ + userId: user.id, + role + }); + await userRoleRepo.save(userRole); + } + } + + // Handle collaborator + if (data.collaborator) { + let collaborator: Collaborator; + + if (data.collaborator.id) { + const existing = await collaboratorRepo.findOne({ where: { id: data.collaborator.id } }); + if (!existing) { + return this.notFound(res, 'Collaborator not found'); + } + collaborator = existing; + } else { + collaborator = collaboratorRepo.create(); + } + + // Update collaborator fields + Object.assign(collaborator, { + ...data.collaborator, + userId: user.id + }); + + await collaboratorRepo.save(collaborator); + user.collaboratorId = collaborator.id; + await userRepo.save(user); + } + + // Handle experiences + if (data.experiences) { + // Delete existing + await experienceRepo.delete({ userId: user.id }); + + // Create new + for (const expData of data.experiences) { + const experience = experienceRepo.create({ + ...expData, + userId: user.id + }); + await experienceRepo.save(experience); + } + } + + // Handle educations + if (data.educations) { + await educationRepo.delete({ userId: user.id }); + for (const eduData of data.educations) { + const education = educationRepo.create({ + ...eduData, + userId: user.id + }); + await educationRepo.save(education); + } + } + + // Handle skills + if (data.skills) { + await skillRepo.delete({ userId: user.id }); + for (const skillData of data.skills) { + const skill = skillRepo.create({ + ...skillData, + userId: user.id + }); + await skillRepo.save(skill); + } + } + + // Handle languages + if (data.languages) { + await languageRepo.delete({ userId: user.id }); + for (const langData of data.languages) { + const language = languageRepo.create({ + ...langData, + userId: user.id + }); + await languageRepo.save(language); + } + } + + // Handle hobbies + if (data.hobbies) { + await hobbyRepo.delete({ userId: user.id }); + for (const hobbyData of data.hobbies) { + const hobby = hobbyRepo.create({ + ...hobbyData, + userId: user.id + }); + await hobbyRepo.save(hobby); + } + } + + // Handle business domains + if (data.businessDomains) { + await domainRepo.delete({ userId: user.id }); + for (const domainData of data.businessDomains) { + const domain = domainRepo.create({ + ...domainData, + userId: user.id + }); + await domainRepo.save(domain); + } + } + + return this.ok(res, { + id: user.id, + message: isNewUser ? 'User profile created successfully' : 'User profile updated successfully' + }); + }); + } catch (error: any) { + console.error('Error creating/updating user profile:', error); + return this.fail(res, error.message); + } + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/widget.controller.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/widget.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..2d34eed371d8927cb7ba7cec92afb550bd54976a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/controllers/widget.controller.ts @@ -0,0 +1,31 @@ +import { Request, Response } from 'express'; +import { WidgetService } from '../services/widget.service'; +import { BaseController } from './base.controller'; + +export class WidgetController extends BaseController { + private service: WidgetService; + + constructor() { + super(); + this.service = new WidgetService(); + } + + getHomeWidgets = async (req: Request, res: Response) => { + try { + const widgets = await this.service.getHomeWidgets(); + return this.ok(res, widgets); + } catch (error) { + return this.fail(res, error as Error); + } + } + + getProjectWidgets = async (req: Request, res: Response) => { + try { + const widgets = await this.service.getProjectWidgets(req.params.projectId); + return this.ok(res, widgets); + } catch (error) { + return this.fail(res, error as Error); + } + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/business-domains.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/business-domains.json new file mode 100644 index 0000000000000000000000000000000000000000..5c31888e36d1c5515b4cb3924d29e4db4fdef43e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/business-domains.json @@ -0,0 +1,72 @@ +[ + { + "domain": "Logistics", + "level": "Expert", + "yearsOfExperience": 12, + "description": "Supply chain management, warehouse systems, TMS integration, delivery tracking, and last-mile logistics optimization.", + "icon": "Truck" + }, + { + "domain": "Finance", + "level": "Expert", + "yearsOfExperience": 8, + "description": "Asset management, portfolio systems, trading platforms, regulatory compliance, and financial data integration.", + "icon": "TrendingUp" + }, + { + "domain": "Trading", + "level": "Expert", + "yearsOfExperience": 5, + "description": "Algorithmic trading systems, market data processing, order management, and cryptocurrency platforms.", + "icon": "LineChart" + }, + { + "domain": "Banking", + "level": "Advanced", + "yearsOfExperience": 3, + "description": "Core banking systems, payment processing, VoIP infrastructure for banking operations.", + "icon": "Building" + }, + { + "domain": "Asset Management", + "level": "Expert", + "yearsOfExperience": 4, + "description": "Investment portfolio systems, fund management, asset position tracking, and regulatory reporting.", + "icon": "Briefcase" + }, + { + "domain": "Steel Manufacturing", + "level": "Advanced", + "yearsOfExperience": 2, + "description": "Production planning, SAP ERP integration, supply chain for steel industry, and manufacturing logistics.", + "icon": "Factory" + }, + { + "domain": "Retail", + "level": "Expert", + "yearsOfExperience": 10, + "description": "E-commerce platforms, inventory management, supplier systems, and omnichannel retail solutions.", + "icon": "ShoppingCart" + }, + { + "domain": "Entertainment", + "level": "Expert", + "yearsOfExperience": 10, + "description": "Live show control systems, real-time audio/visual synchronization, and interactive performance technology.", + "icon": "Clapperboard" + }, + { + "domain": "Theatre", + "level": "Expert", + "yearsOfExperience": 10, + "description": "Stage automation, lighting control systems, MIDI integration, and 3D stereoscopic visual effects for live performances.", + "icon": "Drama" + }, + { + "domain": "Music Composing", + "level": "Advanced", + "yearsOfExperience": 15, + "description": "Original music composition for performances, electronic music production, and sound design.", + "icon": "Music2" + } +] diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/carousel.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/carousel.json new file mode 100644 index 0000000000000000000000000000000000000000..403d0cf47c8d1d3cead624ab816a1c006cfdafec --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/carousel.json @@ -0,0 +1,62 @@ +[ + { + "title": "Ark.Alliance", + "subtitle": "A New Era for Human Recognition", + "description": "Honoring the journey, not just the destination. Your failures today are tomorrow's algorithms. If AI uses OUR collective knowledge to generate BILLIONS in value, we deserve our fair share.", + "imageUrl": "/Assets/Projects/Ark.Alliance/Ark_Alliance_Hero.png", + "linkUrl": "/projects/ark-alliance", + "linkText": "Discover More", + "order": 0, + "isActive": true + }, + { + "title": "Ark.Alliance.StartupCms.AI", + "subtitle": "AI-Powered CMS for Startups", + "description": "Collaborative profile building with AI assistance, hierarchical team visualization, task tracking with growth-mindset evaluation, and selective public visibility. Built with Next.js 15, Prisma, and multi-provider AI integration.", + "imageUrl": "/Assets/Projects/Ark.Alliance.StartupCms.AI/ArkAllianceStartupCmsAiHero.png", + "linkUrl": "/projects/ark-alliance-startupcms-ai", + "linkText": "View Project", + "order": 1, + "isActive": true + }, + { + "title": "Ark.Alliance.React.Component.UI", + "subtitle": "Enterprise React Component Library", + "description": "40 component categories spanning Finance, Healthcare, Logistics, and more. React 19, TypeScript 5.9, 258 passing tests with 100% coverage, MVVM architecture.", + "imageUrl": "/Assets/Projects/Ark.Alliance.React.Component/components-hero.png", + "linkUrl": "/projects/ark-alliance-react-component-ui", + "linkText": "Explore Components", + "order": 2, + "isActive": true + }, + { + "title": "Ark.Alliance.Trading.Bot", + "subtitle": "Automated Cryptocurrency Trading", + "description": "Real-time algorithmic trading platform with WebSocket integration, multi-exchange support, and advanced analytics dashboard. Built for high-frequency trading.", + "imageUrl": "/Assets/Projects/Ark.Alliance.Trading.Bot/bot-hero.png", + "linkUrl": "/projects/ark-alliance-trading-bot", + "linkText": "Learn More", + "order": 3, + "isActive": true + }, + { + "title": "Ark.Alliance.Trading.Providers.Lib", + "subtitle": "Multi-Exchange Trading SDK", + "description": "TypeScript SDK unifying cryptocurrency trading across Binance Futures, Deribit with Result pattern, WebSocket streams, and 70+ tested scenarios. Published on NPM.", + "imageUrl": "/Assets/Projects/Ark.Alliance.Trading.Providers.Lib/providers-hero.png", + "linkUrl": "/projects/ark-alliance-trading-providers-lib", + "linkText": "View Documentation", + "order": 4, + "isActive": true + }, + { + "title": "Ark.Alliance.Trading.TrendsCalculator", + "subtitle": "Real-Time Crypto Trend Analysis", + "description": "Production-grade microservice for cryptocurrency trend analysis. Combines Hurst Exponent, GARCH, Linear Regression, and EMA with optional Gemini AI. WebSocket streaming for sub-second updates.", + "imageUrl": "/Assets/Projects/Ark.Alliance.TrendsCalculator/trends-hero.png", + "linkUrl": "/projects/ark-alliance-trading-trendscalculator", + "linkText": "Explore Trends", + "order": 5, + "isActive": true + } +] \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/education.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/education.json new file mode 100644 index 0000000000000000000000000000000000000000..a555e5973341d4a6cd0dbcb161af18d7a9c5526c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/education.json @@ -0,0 +1,10 @@ +[ + { + "degree": "Master in Computer Science", + "institution": "ULB (Université Libre de Bruxelles)", + "fieldOfStudy": "Computer Science - Algorithmic and Software Engineering", + "startYear": 1999, + "endYear": 2004, + "description": "Specialization in Algorithmic and Software Engineering. Comprehensive curriculum covering data structures, algorithms, software design patterns, database systems, and artificial intelligence fundamentals." + } +] diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/experience.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/experience.json new file mode 100644 index 0000000000000000000000000000000000000000..686327f94dfc7d2ad90bb60efe2cd568a568a6d3 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/experience.json @@ -0,0 +1,66 @@ +[ + { + "company": "M2H / AI and Technology", + "project": "Ark.Alliance Ecosystem; Mindful AI Initiatives", + "period": "Dec 2022 - Present", + "role": "AI Lead & Principal Solution Architect", + "tech": "C#, React, TypeScript, Python, Three.js, Docker, K8s, SLM, OpenAI, Anthropic, Mistral", + "desc": "Pioneered mindful AI solutions and the Ark.Alliance Ecosystem. Architected prompt libraries and guidelines for consistent AI experiences. Led full-stack teams (C#, React, Python) in startup environments, implementing resilient patterns (Circuit Breaker, Bulkhead) and robust incident response processes. Built scalable, cloud-native systems from scratch using Agile/DevOps." + }, + { + "company": "Ahold Delhaize / Logistics Supply Chain", + "project": "New Logistics Systems Design & Optimization", + "period": "Jan 2021 - Feb 2025", + "role": "Solution & Software Architect", + "tech": "C#, .NET 8, Blazor, Microservices, Azure, SAP, CQRS, GitHub, OpenAI API", + "desc": "Architected and delivered critical logistics systems (TMS integration, delivery tracking). Led technical analysis and acceptance testing. Designed generic integration tools. Coached cross-functional and offshore teams to ensure high-quality delivery during cutover and HyperCare periods." + }, + { + "company": "Candriam / Asset Management", + "project": "Asset Position Integration System", + "period": "Apr 2020 - Dec 2020", + "role": "Software Architect & Full Stack Dev", + "tech": "C#, .NET 5, Docker, MQSeries, DB2, Cobol, Microservices, SQL Server, CQRS", + "desc": "Reverse-engineered legacy Mainframe systems to modern .NET microservices. Designed and rewrote the asset position integration system (shares, funds) for financial account managers. Executed a seamless migration with zero downtime during cutover." + }, + { + "company": "Liberty Steel / Steel Industry", + "project": "Web-Based Application Portfolio & SAP Integration", + "period": "May 2019 - Apr 2020", + "role": "Business Analyst & Full Stack Dev", + "tech": "C#, .NET Core, TypeScript, Python, Azure, SAP ERP, Clean Architecture, Blazor", + "desc": "Analyzed business flows and developed web-based microservices for steel production logistics. Migrated legacy services to WCF and rewrote production planning schedulers. Integrated deeply with SAP and Mainframe systems using Azure cloud services." + }, + { + "company": "Delhaize Group / Retail Logistics", + "project": "Logistics Systems Design & Cloud Transformation", + "period": "Sep 2011 - Apr 2019", + "role": "Full Stack Developer & IT Owner", + "tech": "C#, .NET, Entity Framework, Azure, Java, SAP ERP, WMS, IoT, SQL Server/Oracle", + "desc": "Owned IT delivery for global logistics reporting and supplier management apps. Modernized legacy systems with SAP and WMS integrations (dock scheduling, EDIFACT). Provided L3 support and implemented ITIL processes for resilient 24/7 logistics operations." + }, + { + "company": "Spectacles Charles Kleinberg", + "project": "Live Show Control Systems", + "period": "Jan 2005 - Nov 2015", + "role": "Solution Architect - Developer & Artist", + "tech": "C#, Python, C++, DirectX, Unity, Three.js, Max/Msp, Maya, Real-time Systems", + "desc": "Architected real-time show control systems synchronizing audio, MIDI, lighting, and 3D Stereoscopic visuals. Developed high-performance interactive solutions in Agile environments. Blended technical engineering with artistic direction for immersive live performances." + }, + { + "company": "BNP Paribas Fortis / Banking", + "project": "Voice over IP Systems", + "period": "Nov 2010 - Feb 2011", + "role": "ICT Consultant", + "tech": ".NET (C#/VB), SQL Server, SOAP", + "desc": "Consulted on the architecture and full-stack development of robust Voice over IP systems for banking infrastructure." + }, + { + "company": "Mastercard / Financial Services", + "project": "Credit Card Chip Validation", + "period": "Nov 2004 - Feb 2005", + "role": "Software Engineer", + "tech": "C#, C, SQL Server, Assembly", + "desc": "Developed critical testing and reporting systems for credit card chip compliance. Collaborated on feature launches to enhance global transaction security." + } +] diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/hobbies.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/hobbies.json new file mode 100644 index 0000000000000000000000000000000000000000..e13b4ab37f6de8bdd27c61fcb3ceaba47d77fb3f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/hobbies.json @@ -0,0 +1,27 @@ +[ + { + "name": "Music Production", + "description": "Electronic music composition and production using DAWs, synthesizers, and audio engineering techniques.", + "icon": "Headphones" + }, + { + "name": "3D Graphics & Animation", + "description": "Creating 3D models, animations, and visual effects using Cinema 4D, Maya, and Three.js.", + "icon": "Cuboid" + }, + { + "name": "Gaming", + "description": "Strategy games, simulation, and game development exploration.", + "icon": "Gamepad2" + }, + { + "name": "Photography", + "description": "Digital photography with focus on architectural and landscape subjects.", + "icon": "Camera" + }, + { + "name": "AI Research", + "description": "Exploring cutting-edge AI technologies, prompt engineering, and building AI-powered applications.", + "icon": "Brain" + } +] \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/languages.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/languages.json new file mode 100644 index 0000000000000000000000000000000000000000..1b20b5858612e022b4221038d5ddc149dc7c450e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/languages.json @@ -0,0 +1,26 @@ +[ + { + "language": "French", + "speaking": 5, + "writing": 5, + "presenting": 5 + }, + { + "language": "English", + "speaking": 5, + "writing": 5, + "presenting": 5 + }, + { + "language": "Spanish", + "speaking": 4, + "writing": 3, + "presenting": 3 + }, + { + "language": "Dutch", + "speaking": 2, + "writing": 1, + "presenting": 1 + } +] diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/profile.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/profile.json new file mode 100644 index 0000000000000000000000000000000000000000..689b356a3788c9cd24d8789bd699ec80ae7ca14e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/profile.json @@ -0,0 +1,10 @@ +{ + "firstName": "Armand", + "lastName": "Richelet-Kleinberg", + "title": "AI Principal Solutions Architect - Full Stack Dev - Business & Technical Analyst", + "overview": "Experienced AI Principal Architect with 20+ years in software development and system architecture, focused on the AI industry. Specialized in designing and delivering AI-driven solutions, MLOps pipelines, and cloud-native systems using C#, TypeScript (React), Js, Python. Proven ability to build scalable, event-driven architectures and integrate advanced AI frameworks. Expert in Agile/DevOps environments, translating complex AI business needs into robust, production-grade platforms.", + "email": "arkleinberg@gmail.com", + "githubUrl": "https://github.com/ArmandRicheletKleinberg", + "linkedinUrl": "https://do.linkedin.com/in/arkleinberg/es", + "avatarUrl": "/Assets/Site/Icon.png" +} \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/projects.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/projects.json new file mode 100644 index 0000000000000000000000000000000000000000..6b901996518c50e34399f34fd7c3910b71a3322e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/projects.json @@ -0,0 +1,169 @@ +[ + { + "title": "Ark.Alliance Trading Ecosystem", + "description": "A high-frequency crypto trading bot and ecosystem featuring a cyberpunk dashboard, real-time WebSocket streaming, and AI-driven trend detection. Built on a microservices architecture with a focus on resilience and speed.", + "status": "In Progress", + "isFeatured": true, + "technologies": [ + "C#", + "React", + "TypeScript", + "Python", + "Docker", + "Kubernetes", + "Three.js", + "OpenAI" + ], + "imageUrl": "/assets/Bot10.PNG", + "repoUrl": "https://github.com/ark/alliance", + "demoUrl": "https://ark-alliance.demo", + "startDate": "2022-12-01", + "features": [ + { + "title": "Strategy Command Center", + "description": "The central nerve center of the operation. Visualizes PnL in real-time with sub-millisecond updates via WebSockets. The 'Cyberpunk' aesthetic isn't just for show—it uses high-contrast neon elements to make critical alerts visible instantly.", + "icon": "monitor", + "imageUrl": "/assets/Bot10.PNG" + }, + { + "title": "3D Market Topography", + "description": "Interactive Three.js visualization of the order book. Peaks and valleys represent buy/sell walls. This allows the trader to 'Surf the Wave' by visually identifying support and resistance zones in 3D space.", + "icon": "box", + "imageUrl": "/assets/Bot6.PNG" + }, + { + "title": "AI Logic Analysis", + "description": "Real-time introspection into the 'Gemini' AI Strategy. Shows the calculated 'Sigma' (volatility threshold) and 'Trend Confidence' score. This is where the 'Wait' vs 'Trade' decisions happen.", + "icon": "cpu", + "imageUrl": "/assets/Bot2.PNG" + }, + { + "title": "Logistics Matrix", + "description": "A grid view for managing hundreds of concurrent 'Worker' instances. Each cell represents a Docker container running a specific strategy on a specific pair. Green = Profit, Red = Inversion/Loss.", + "icon": "grid", + "imageUrl": "/assets/Bot13.PNG" + }, + { + "title": "Configuration Console", + "description": "Fine-grained control over the 'Click' parameters. Here you set the 'Take Profit Step' (e.g., 0.05%) and the 'Inversion Sigma'. Changes propagate to active instances immediately.", + "icon": "sliders", + "imageUrl": "/assets/Bot5.PNG" + }, + { + "title": "Execution Logs", + "description": "Live stream of order execution events. Tracks latency statistics (e.g., 'Order to Ack: 45ms') and API weight usage to prevent bans.", + "icon": "activity", + "imageUrl": "/assets/Bot4.PNG" + } + ], + "pages": [ + { + "type": "OVERVIEW", + "title": "Surfing on the Wave", + "content": "### Philosophy: Chaos as Potential\n\nThe **Ark.Alliance** strategy is built on a simple premise: *You cannot predict the ocean, but you can learn to surf.* \n\nInstead of trying to forecast price 10 minutes from now, the bot reacts to *micro-movements* in the present moment. We call this **'Click Strategy'**.\n\n#### The 'Click' Mechanism\n\n1. **Entry (The Paddle)**: The bot enters a position based on **Gemini AI Analysis** of order book imbalance and volatility.\n2. **Surfing (The Ride)**: As price moves in favor, the bot executes 'Clicks'—partial take-profits at fixed intervals (e.g., every +0.1% PnL).\n * **Ratchet Effect**: Each click raises the 'Inversion Threshold' (Stop Loss), locking in gains.\n3. **Inversion (The Bail)**: If the wave breaks (price reverses), the bot hits the 'Inversion Threshold'.\n * It instantly closes the position and flips to the opposite side (2x quantity).\n * **Result**: The surfer catches the *new* wave immediately.\n\n```mermaid\nstateDiagram-v2\n [*] --> Entry\n Entry --> Surfing: Price Moves Up\n Surfing --> Click: +0.1% Gain\n Click --> Surfing: Raise Safety Net\n Surfing --> Inversion: Hit Safety Net\n Inversion --> [*]: Flip Position\n```" + }, + { + "type": "TECHNICAL", + "title": "Engineering Resilience", + "content": "### Speed vs. Reliability\n\nCrypto markets are 24/7 and unforgiving. The bot's architecture prioritizes **Resilience** over raw speed, though it achieves both.\n\n#### 1. Hybrid Push/Pull Architecture\n\nReliance on WebSockets alone is dangerous (silent disconnects). Reliability on Polling alone is too slow. We use **Both**.\n\n* **Fast Path (Push)**: `BinanceUserDataStream` delivers order updates in ~50ms.\n* **Safe Path (Pull)**: A `MonitoringLayout` polls the API every 200ms. If the WebSocket misses an event, the Poller catches it.\n\n#### 2. Fee Management & Pre-Computation\n\nTrading frequently ('Clicking') accumulates fees. Ignorance of fees leads to 'Death by a Thousand Cuts'.\n\n* **Maker vs Taker**: The bot prefers `POST_ONLY` (Maker) orders to gain rebates.\n* **Pre-Computation**: Before placing *any* trade, the `FuturesCost Service` calculates:\n * `OpenFee` + `CloseFee` + `FundingCost`\n * **Rule**: The first 'Click' target is moved *further out* to cover these costs. You are never 'Green' until the fees are paid.\n\n#### 3. Rate Limit Arbitrage\n\nBinance allows 1200 weight/minute. \n* The **RateLimiter** service tracks usage in real-time.\n* If usage > 80%, it switches non-critical polls to 'Lazy Mode', saving bandwidth for critical 'Inversion' orders.\n\n```mermaid\nsequenceDiagram\n participant Bot\n participant RateLimiter\n participant Binance\n\n Bot->>RateLimiter: Can I Order?\n RateLimiter->>RateLimiter: Check Weight (1100/1200)\n alt Critical (Inversion)\n RateLimiter-->>Bot: YES (Emergency Override)\n else Standard (Poll)\n RateLimiter-->>Bot: PAUSE (Save for Trade)\n end\n Bot->>Binance: Execute Order\n```" + }, + { + "type": "FUNCTIONAL", + "title": "Functional Gallery", + "content": "Experience the interface designed for the high-stakes environment of algorithmic trading." + } + ] + }, + { + "title": "Logistics Orchestration Platform", + "description": "A comprehensive logistics supply chain platform for Ahold Delhaize, integrating TMS (Transport Management Systems), SAP, and real-time delivery tracking.", + "status": "Completed", + "isFeatured": true, + "technologies": [ + "C#", + ".NET 8", + "Blazor", + "Azure", + "SAP", + "Microservices", + "CQRS" + ], + "imageUrl": "/assets/Bot13.PNG", + "startDate": "2021-01-01", + "endDate": "2025-02-01", + "features": [ + { + "title": "TMS Integration", + "description": "Seamless connecting with Ortec & Axiodis TMS.", + "icon": "truck" + }, + { + "title": "Global Tracking", + "description": "Real-time visibility into delivery status across Europe.", + "icon": "map" + } + ], + "pages": [ + { + "type": "OVERVIEW", + "title": "Strategic Impact", + "content": "Revitalized logistics core by moving to Domain-Driven Design microservices, achieving 30% cost reduction." + } + ] + }, + { + "title": "Live Show Control System", + "description": "Real-time audiovisual synchronization system for immersive live performances.", + "status": "Completed", + "technologies": [ + "C++", + "Python", + "Unity", + "C#" + ], + "imageUrl": "/assets/Bot6.PNG", + "startDate": "2005-01-01", + "endDate": "2015-11-01", + "features": [ + { + "title": "AV Sync", + "description": "Frame-perfect synchronization.", + "icon": "music" + }, + { + "title": "3D Visuals", + "description": "Real-time stereoscopic 3D content.", + "icon": "box" + } + ], + "pages": [ + { + "type": "OVERVIEW", + "title": "Art Meets Engineering", + "content": "Bridging technical precision and artistic expression for major live spectacles." + } + ] + }, + { + "title": "Asset Position Integration", + "description": "Financial system integration bridging legacy Mainframe data with modern web interfaces.", + "status": "Completed", + "technologies": [ + "C#", + ".NET 5", + "Docker", + "MQSeries", + "Cobol" + ], + "imageUrl": "/assets/Bot12.PNG", + "startDate": "2020-04-01", + "endDate": "2020-12-01", + "features": [ + { + "title": "Legacy Bridge", + "description": "Secure communication with Mainframe.", + "icon": "server" + } + ] + } +] diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/skills.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/skills.json new file mode 100644 index 0000000000000000000000000000000000000000..1c5f85fd3ab6923708216f618b7e79185eddef15 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/skills.json @@ -0,0 +1,49 @@ +{ + "languages": [ + "C#", + "Python", + "Java", + "TypeScript", + "JavaScript", + "C", + "C++", + "T-SQL", + "PL-SQL" + ], + "frameworks": [ + ".NET up to 10", + "Entity Framework", + "Python Ecosystem", + "Unity", + "Adobe Suite", + "Cinema 4D" + ], + "databases": [ + "SQL Server", + "SQLite", + "PostgreSQL", + "MongoDB", + "DB2", + "Oracle 9i", + "Sybase" + ], + "tools": [ + "Docker", + "Kubernetes", + "Azure Services", + "GitHub", + "PyTorch", + "RabbitMQ", + "AWS Services", + "Unreal/Three.js" + ], + "methodologies": [ + "DDD", + "CQRS", + "Event-Driven Architecture", + "Microservices", + "MLOps", + "Agile/Scrum", + "DevOps" + ] +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/technologies.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/technologies.json new file mode 100644 index 0000000000000000000000000000000000000000..f3216137a337352c1a75d5e64c63171fa2c92c7c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/technologies.json @@ -0,0 +1,1285 @@ +{ + "categories": [ + { + "id": "frontend", + "name": "Frontend Frameworks & Libraries", + "description": "Client-side web development technologies", + "order": 1 + }, + { + "id": "languages", + "name": "Programming Languages", + "description": "Core programming languages", + "order": 2 + }, + { + "id": "runtimes", + "name": "Runtimes & Platforms", + "description": "Runtime environments and development platforms", + "order": 3 + }, + { + "id": "backend", + "name": "Backend Frameworks", + "description": "Server-side frameworks and libraries", + "order": 4 + }, + { + "id": "databases", + "name": "Databases", + "description": "Relational and NoSQL database systems", + "order": 5 + }, + { + "id": "cloud", + "name": "Cloud Platforms", + "description": "Cloud computing providers and services", + "order": 6 + }, + { + "id": "devops", + "name": "DevOps & Infrastructure", + "description": "CI/CD, containerization, and infrastructure tools", + "order": 7 + }, + { + "id": "messaging", + "name": "Message Brokers & Queues", + "description": "Event streaming and message queue systems", + "order": 8 + }, + { + "id": "ai", + "name": "AI/ML & Data Science", + "description": "Artificial intelligence and machine learning technologies", + "order": 9 + }, + { + "id": "enterprise", + "name": "Enterprise Systems", + "description": "ERP, CRM, and enterprise software platforms", + "order": 10 + }, + { + "id": "patterns", + "name": "Architecture Patterns", + "description": "Architectural patterns and API specifications", + "order": 11 + }, + { + "id": "apis", + "name": "External APIs & Services", + "description": "Third-party APIs and integration services", + "order": 12 + }, + { + "id": "testing", + "name": "Testing & Quality", + "description": "Testing frameworks and quality assurance tools", + "order": 13 + }, + { + "id": "mobile", + "name": "Mobile Development", + "description": "Mobile app development frameworks", + "order": 14 + }, + { + "id": "styling", + "name": "Styling & Design", + "description": "CSS frameworks and design tools", + "order": 15 + }, + { + "id": "blockchain-platform", + "name": "Blockchain Platforms", + "description": "Layer-1 and layer-2 blockchain networks", + "order": 16 + }, + { + "id": "distributed-ledger", + "name": "Distributed Ledger Technology", + "description": "Blockchain and decentralized technologies", + "order": 17 + }, + { + "id": "consensus-mechanism", + "name": "Consensus Mechanisms", + "description": "Blockchain consensus algorithms", + "order": 18 + }, + { + "id": "blockchain-standard", + "name": "Blockchain Standards", + "description": "Token standards and blockchain protocols", + "order": 19 + }, + { + "id": "mining-platform", + "name": "Mining Platforms", + "description": "Cryptocurrency mining operating systems", + "order": 20 + }, + { + "id": "mining-hardware", + "name": "Mining Hardware", + "description": "ASIC and GPU mining equipment", + "order": 21 + }, + { + "id": "blockchain-layer2", + "name": "Layer-2 Solutions", + "description": "Scaling solutions for blockchain networks", + "order": 22 + }, + { + "id": "leadership", + "name": "Leadership & Management", + "description": "Team management and leadership skills", + "order": 23 + }, + { + "id": "markup-language", + "name": "Markup Languages", + "description": "HTML and other markup technologies", + "order": 24 + }, + { + "id": "styling-language", + "name": "Styling Languages", + "description": "CSS and styling technologies", + "order": 25 + }, + { + "id": "css-preprocessor", + "name": "CSS Preprocessors", + "description": "SASS, LESS, and other CSS preprocessors", + "order": 26 + }, + { + "id": "data-format", + "name": "Data Formats", + "description": "JSON, XML, and other data interchange formats", + "order": 27 + }, + { + "id": "crypto-exchange-api", + "name": "Crypto Exchange APIs", + "description": "Cryptocurrency exchange trading APIs", + "order": 28 + }, + { + "id": "cybersecurity", + "name": "Cybersecurity", + "description": "Security testing and ethical hacking", + "order": 29 + } + ], + "technologies": [ + { + "key": "react", + "name": "React", + "label": "React.js", + "category": "frontend", + "description": "A JavaScript library for building user interfaces with component-based architecture", + "icon": "fab fa-react", + "color": "#61DAFB", + "website": "https://react.dev", + "versions": [ + "16", + "17", + "18", + "19" + ] + }, + { + "key": "angular", + "name": "Angular", + "label": "Angular", + "category": "frontend", + "description": "A TypeScript-based web application framework led by Google", + "icon": "fab fa-angular", + "color": "#DD0031", + "website": "https://angular.io" + }, + { + "key": "vue", + "name": "Vue.js", + "label": "Vue.js", + "category": "frontend", + "description": "A progressive JavaScript framework for building user interfaces", + "icon": "fab fa-vuejs", + "color": "#4FC08D", + "website": "https://vuejs.org" + }, + { + "key": "svelte", + "name": "Svelte", + "label": "Svelte", + "category": "frontend", + "description": "A compiler-based framework that shifts work from runtime to build time", + "icon": "devicon-svelte-plain", + "color": "#FF3E00", + "website": "https://svelte.dev" + }, + { + "key": "nextjs", + "name": "Next.js", + "label": "Next.js", + "category": "frontend", + "description": "A React framework for production with hybrid static & server rendering", + "icon": "devicon-nextjs-original", + "color": "#000000", + "website": "https://nextjs.org" + }, + { + "key": "blazor", + "name": "Blazor", + "label": "Blazor", + "category": "frontend", + "description": "A framework for building interactive web UIs using C# instead of JavaScript", + "icon": "devicon-dotnetcore-plain", + "color": "#512BD4", + "website": "https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor" + }, + { + "key": "threejs", + "name": "Three.js", + "label": "Three.js", + "category": "frontend", + "description": "A cross-browser JavaScript library for creating 3D graphics in web browsers", + "icon": "devicon-threejs-original", + "color": "#000000", + "website": "https://threejs.org" + }, + { + "key": "typescript", + "name": "TypeScript", + "label": "TypeScript", + "category": "languages", + "description": "A strongly typed programming language that builds on JavaScript", + "icon": "devicon-typescript-plain", + "color": "#3178C6", + "website": "https://www.typescriptlang.org" + }, + { + "key": "javascript", + "name": "JavaScript", + "label": "JavaScript", + "category": "languages", + "description": "A dynamic programming language that powers the web", + "icon": "fab fa-js-square", + "color": "#F7DF1E", + "website": "https://developer.mozilla.org/en-US/docs/Web/JavaScript" + }, + { + "key": "csharp", + "name": "C#", + "label": "C#", + "category": "languages", + "description": "A modern, object-oriented programming language developed by Microsoft", + "icon": "devicon-csharp-plain", + "color": "#239120", + "website": "https://docs.microsoft.com/en-us/dotnet/csharp/" + }, + { + "key": "cpp", + "name": "C++", + "label": "C++", + "category": "languages", + "description": "A high-performance programming language for systems and applications", + "icon": "devicon-cplusplus-plain", + "color": "#00599C", + "website": "https://isocpp.org" + }, + { + "key": "java", + "name": "Java", + "label": "Java", + "category": "languages", + "description": "A class-based, object-oriented programming language for enterprise applications", + "icon": "fab fa-java", + "color": "#007396", + "website": "https://www.java.com" + }, + { + "key": "go", + "name": "Go", + "label": "Go (Golang)", + "category": "languages", + "description": "A statically typed, compiled language designed at Google for simplicity and efficiency", + "icon": "devicon-go-original-wordmark", + "color": "#00ADD8", + "website": "https://go.dev" + }, + { + "key": "rust", + "name": "Rust", + "label": "Rust", + "category": "languages", + "description": "A systems programming language focused on safety, speed, and concurrency", + "icon": "devicon-rust-plain", + "color": "#DEA584", + "website": "https://www.rust-lang.org" + }, + { + "key": "cobol", + "name": "Cobol", + "label": "COBOL", + "category": "languages", + "description": "A business-oriented programming language used in mainframe systems", + "icon": "fas fa-code", + "color": "#005CA5", + "website": "https://www.ibm.com/products/cobol-compiler-zos" + }, + { + "key": "nodejs", + "name": "Node.js", + "label": "Node.js", + "category": "runtimes", + "description": "A JavaScript runtime built on Chrome's V8 engine for server-side development", + "icon": "fab fa-node-js", + "color": "#339933", + "website": "https://nodejs.org" + }, + { + "key": "dotnet", + "name": ".NET", + "label": ".NET", + "category": "runtimes", + "description": "A free, cross-platform, open-source developer platform by Microsoft", + "icon": "devicon-dotnetcore-plain", + "color": "#512BD4", + "website": "https://dotnet.microsoft.com", + "versions": [ + "5", + "6", + "7", + "8", + "9", + "10" + ] + }, + { + "key": "unity", + "name": "Unity", + "label": "Unity Engine", + "category": "runtimes", + "description": "A cross-platform game engine for 2D/3D games and interactive experiences", + "icon": "devicon-unity-original", + "color": "#000000", + "website": "https://unity.com" + }, + { + "key": "unreal", + "name": "Unreal Engine", + "label": "Unreal Engine", + "category": "runtimes", + "description": "A powerful 3D game engine for AAA game development and visualization", + "icon": "devicon-unrealengine-original", + "color": "#0E1128", + "website": "https://www.unrealengine.com" + }, + { + "key": "express", + "name": "Express.js", + "label": "Express.js", + "category": "backend", + "description": "A fast, unopinionated, minimalist web framework for Node.js", + "icon": "devicon-express-original", + "color": "#000000", + "website": "https://expressjs.com" + }, + { + "key": "nestjs", + "name": "NestJS", + "label": "NestJS", + "category": "backend", + "description": "A progressive Node.js framework for building efficient server-side applications", + "icon": "devicon-nestjs-plain", + "color": "#E0234E", + "website": "https://nestjs.com" + }, + { + "key": "fastapi", + "name": "FastAPI", + "label": "FastAPI", + "category": "backend", + "description": "A modern, fast Python web framework for building APIs with automatic OpenAPI docs", + "icon": "devicon-fastapi-plain", + "color": "#009688", + "website": "https://fastapi.tiangolo.com" + }, + { + "key": "django", + "name": "Django", + "label": "Django", + "category": "backend", + "description": "A high-level Python web framework for rapid development", + "icon": "devicon-django-plain", + "color": "#092E20", + "website": "https://www.djangoproject.com" + }, + { + "key": "flask", + "name": "Flask", + "label": "Flask", + "category": "backend", + "description": "A lightweight WSGI web application framework in Python", + "icon": "devicon-flask-original", + "color": "#000000", + "website": "https://flask.palletsprojects.com" + }, + { + "key": "aspnetcore", + "name": "ASP.NET Core", + "label": "ASP.NET Core", + "category": "backend", + "description": "A cross-platform, high-performance framework for building modern web apps", + "icon": "devicon-dotnetcore-plain", + "color": "#512BD4", + "website": "https://docs.microsoft.com/en-us/aspnet/core" + }, + { + "key": "spring", + "name": "Spring", + "label": "Spring Boot", + "category": "backend", + "description": "A Java-based framework for building enterprise-grade applications", + "icon": "devicon-spring-plain", + "color": "#6DB33F", + "website": "https://spring.io" + }, + { + "key": "sqlite", + "name": "SQLite", + "label": "SQLite", + "category": "databases", + "description": "A lightweight, serverless, self-contained SQL database engine", + "icon": "devicon-sqlite-plain", + "color": "#003B57", + "website": "https://www.sqlite.org" + }, + { + "key": "postgresql", + "name": "PostgreSQL", + "label": "PostgreSQL", + "category": "databases", + "description": "A powerful, open-source object-relational database system", + "icon": "devicon-postgresql-plain", + "color": "#4169E1", + "website": "https://www.postgresql.org" + }, + { + "key": "mysql", + "name": "MySQL", + "label": "MySQL", + "category": "databases", + "description": "The world's most popular open-source relational database", + "icon": "devicon-mysql-plain", + "color": "#4479A1", + "website": "https://www.mysql.com" + }, + { + "key": "sqlserver", + "name": "SQL Server", + "label": "SQL Server", + "category": "databases", + "description": "Microsoft's enterprise-grade relational database management system", + "icon": "devicon-microsoftsqlserver-plain", + "color": "#CC2927", + "website": "https://www.microsoft.com/sql-server" + }, + { + "key": "mongodb", + "name": "MongoDB", + "label": "MongoDB", + "category": "databases", + "description": "A document-based NoSQL database for modern applications", + "icon": "devicon-mongodb-plain", + "color": "#47A248", + "website": "https://www.mongodb.com" + }, + { + "key": "redis", + "name": "Redis", + "label": "Redis", + "category": "databases", + "description": "An in-memory data structure store used as database, cache, and message broker", + "icon": "devicon-redis-plain", + "color": "#DC382D", + "website": "https://redis.io" + }, + { + "key": "elasticsearch", + "name": "Elasticsearch", + "label": "Elasticsearch", + "category": "databases", + "description": "A distributed search and analytics engine for all types of data", + "icon": "devicon-elasticsearch-original", + "color": "#005571", + "website": "https://www.elastic.co" + }, + { + "key": "oracle", + "name": "Oracle", + "label": "Oracle Database", + "category": "databases", + "description": "A multi-model database management system for enterprise workloads", + "icon": "devicon-oracle-original", + "color": "#F80000", + "website": "https://www.oracle.com/database" + }, + { + "key": "db2", + "name": "DB2", + "label": "IBM DB2", + "category": "databases", + "description": "IBM's family of data management products including database servers", + "icon": "fas fa-database", + "color": "#052FAD", + "website": "https://www.ibm.com/products/db2" + }, + { + "key": "aws", + "name": "AWS", + "label": "Amazon Web Services", + "category": "cloud", + "description": "The world's leading cloud computing platform by Amazon", + "icon": "fab fa-aws", + "color": "#FF9900", + "website": "https://aws.amazon.com" + }, + { + "key": "azure", + "name": "Azure", + "label": "Microsoft Azure", + "category": "cloud", + "description": "Microsoft's cloud computing platform for building, testing, and managing applications", + "icon": "devicon-azure-plain", + "color": "#0078D4", + "website": "https://azure.microsoft.com" + }, + { + "key": "gcp", + "name": "Google Cloud", + "label": "Google Cloud Platform", + "category": "cloud", + "description": "Google's suite of cloud computing services", + "icon": "devicon-googlecloud-plain", + "color": "#4285F4", + "website": "https://cloud.google.com" + }, + { + "key": "digitalocean", + "name": "DigitalOcean", + "label": "DigitalOcean", + "category": "cloud", + "description": "A cloud infrastructure provider focused on simplicity for developers", + "icon": "devicon-digitalocean-plain", + "color": "#0080FF", + "website": "https://www.digitalocean.com" + }, + { + "key": "vercel", + "name": "Vercel", + "label": "Vercel", + "category": "cloud", + "description": "A cloud platform for static sites and serverless functions", + "icon": "devicon-vercel-original", + "color": "#000000", + "website": "https://vercel.com" + }, + { + "key": "docker", + "name": "Docker", + "label": "Docker", + "category": "devops", + "description": "A platform for developing, shipping, and running applications in containers", + "icon": "fab fa-docker", + "color": "#2496ED", + "website": "https://www.docker.com" + }, + { + "key": "kubernetes", + "name": "Kubernetes", + "label": "Kubernetes (K8s)", + "category": "devops", + "description": "An open-source system for automating deployment and management of containerized applications", + "icon": "devicon-kubernetes-plain", + "color": "#326CE5", + "website": "https://kubernetes.io" + }, + { + "key": "terraform", + "name": "Terraform", + "label": "Terraform", + "category": "devops", + "description": "An infrastructure as code tool for building, changing, and versioning infrastructure", + "icon": "devicon-terraform-plain", + "color": "#7B42BC", + "website": "https://www.terraform.io" + }, + { + "key": "github-actions", + "name": "GitHub Actions", + "label": "GitHub Actions", + "category": "devops", + "description": "CI/CD platform integrated with GitHub for automating workflows", + "icon": "fab fa-github", + "color": "#2088FF", + "website": "https://github.com/features/actions" + }, + { + "key": "jenkins", + "name": "Jenkins", + "label": "Jenkins", + "category": "devops", + "description": "An open-source automation server for building, deploying, and automating projects", + "icon": "devicon-jenkins-line", + "color": "#D24939", + "website": "https://www.jenkins.io" + }, + { + "key": "gitlab-ci", + "name": "GitLab CI", + "label": "GitLab CI/CD", + "category": "devops", + "description": "GitLab's built-in continuous integration and deployment tool", + "icon": "fab fa-gitlab", + "color": "#FC6D26", + "website": "https://docs.gitlab.com/ee/ci" + }, + { + "key": "ansible", + "name": "Ansible", + "label": "Ansible", + "category": "devops", + "description": "An open-source automation tool for configuration management and deployment", + "icon": "devicon-ansible-plain", + "color": "#EE0000", + "website": "https://www.ansible.com" + }, + { + "key": "nginx", + "name": "NGINX", + "label": "NGINX", + "category": "devops", + "description": "A high-performance web server, reverse proxy, and load balancer", + "icon": "devicon-nginx-original", + "color": "#009639", + "website": "https://nginx.org" + }, + { + "key": "rabbitmq", + "name": "RabbitMQ", + "label": "RabbitMQ", + "category": "messaging", + "description": "An open-source message broker implementing AMQP protocol", + "icon": "devicon-rabbitmq-original", + "color": "#FF6600", + "website": "https://www.rabbitmq.com" + }, + { + "key": "kafka", + "name": "Kafka", + "label": "Apache Kafka", + "category": "messaging", + "description": "A distributed event streaming platform for high-throughput data pipelines", + "icon": "devicon-apachekafka-original", + "color": "#231F20", + "website": "https://kafka.apache.org" + }, + { + "key": "mqseries", + "name": "MQSeries", + "label": "IBM MQ", + "category": "messaging", + "description": "IBM's enterprise message-oriented middleware for reliable messaging", + "icon": "fas fa-exchange-alt", + "color": "#052FAD", + "website": "https://www.ibm.com/products/mq" + }, + { + "key": "azure-service-bus", + "name": "Azure Service Bus", + "label": "Azure Service Bus", + "category": "messaging", + "description": "A fully managed enterprise message broker with message queues and publish-subscribe topics", + "icon": "devicon-azure-plain", + "color": "#0078D4", + "website": "https://azure.microsoft.com/services/service-bus" + }, + { + "key": "pytorch", + "name": "PyTorch", + "label": "PyTorch", + "category": "ai", + "description": "An open-source machine learning framework based on the Torch library", + "icon": "devicon-pytorch-original", + "color": "#EE4C2C", + "website": "https://pytorch.org" + }, + { + "key": "tensorflow", + "name": "TensorFlow", + "label": "TensorFlow", + "category": "ai", + "description": "An end-to-end open-source platform for machine learning", + "icon": "devicon-tensorflow-original", + "color": "#FF6F00", + "website": "https://www.tensorflow.org" + }, + { + "key": "openai", + "name": "OpenAI", + "label": "OpenAI API", + "category": "ai", + "description": "AI models and APIs including GPT-4, DALL-E, and Whisper", + "icon": "fas fa-robot", + "color": "#412991", + "website": "https://openai.com" + }, + { + "key": "anthropic", + "name": "Anthropic", + "label": "Claude API", + "category": "ai", + "description": "Anthropic's Claude AI assistant and API for safe, helpful AI", + "icon": "fas fa-brain", + "color": "#CC9B7A", + "website": "https://www.anthropic.com" + }, + { + "key": "gemini", + "name": "Gemini", + "label": "Google Gemini", + "category": "ai", + "description": "Google's multimodal AI model for text, images, and code", + "icon": "fas fa-gem", + "color": "#4285F4", + "website": "https://deepmind.google/technologies/gemini" + }, + { + "key": "huggingface", + "name": "Hugging Face", + "label": "Hugging Face", + "category": "ai", + "description": "The AI community platform with models, datasets, and ML applications", + "icon": "fas fa-smile", + "color": "#FFD21E", + "website": "https://huggingface.co" + }, + { + "key": "langchain", + "name": "LangChain", + "label": "LangChain", + "category": "ai", + "description": "A framework for developing applications powered by language models", + "icon": "fas fa-link", + "color": "#1C3C3C", + "website": "https://www.langchain.com" + }, + { + "key": "pandas", + "name": "Pandas", + "label": "Pandas", + "category": "ai", + "description": "A fast, powerful data analysis and manipulation library for Python", + "icon": "devicon-pandas-original", + "color": "#150458", + "website": "https://pandas.pydata.org" + }, + { + "key": "numpy", + "name": "NumPy", + "label": "NumPy", + "category": "ai", + "description": "The fundamental package for scientific computing with Python", + "icon": "devicon-numpy-original", + "color": "#013243", + "website": "https://numpy.org" + }, + { + "key": "sap", + "name": "SAP", + "label": "SAP ERP", + "category": "enterprise", + "description": "Enterprise resource planning software for business operations", + "icon": "fas fa-building", + "color": "#0FAAFF", + "website": "https://www.sap.com" + }, + { + "key": "salesforce", + "name": "Salesforce", + "label": "Salesforce", + "category": "enterprise", + "description": "The world's leading customer relationship management (CRM) platform", + "icon": "devicon-salesforce-plain", + "color": "#00A1E0", + "website": "https://www.salesforce.com" + }, + { + "key": "dynamics365", + "name": "Dynamics 365", + "label": "Microsoft Dynamics 365", + "category": "enterprise", + "description": "Microsoft's portfolio of enterprise resource planning and CRM applications", + "icon": "fab fa-microsoft", + "color": "#002050", + "website": "https://dynamics.microsoft.com" + }, + { + "key": "microservices", + "name": "Microservices", + "label": "Microservices Architecture", + "category": "patterns", + "description": "An architectural style that structures an application as loosely coupled services", + "icon": "fas fa-cubes", + "color": "#6C5CE7" + }, + { + "key": "cqrs", + "name": "CQRS", + "label": "CQRS", + "category": "patterns", + "description": "Command Query Responsibility Segregation - separating read and write operations", + "icon": "fas fa-exchange-alt", + "color": "#00B894" + }, + { + "key": "event-sourcing", + "name": "Event Sourcing", + "label": "Event Sourcing", + "category": "patterns", + "description": "Storing all state changes as a sequence of events", + "icon": "fas fa-history", + "color": "#FDCB6E" + }, + { + "key": "ddd", + "name": "DDD", + "label": "Domain-Driven Design", + "category": "patterns", + "description": "An approach to software development focused on complex business domains", + "icon": "fas fa-project-diagram", + "color": "#E17055" + }, + { + "key": "graphql", + "name": "GraphQL", + "label": "GraphQL", + "category": "patterns", + "description": "A query language for APIs and a runtime for fulfilling those queries", + "icon": "devicon-graphql-plain", + "color": "#E10098", + "website": "https://graphql.org" + }, + { + "key": "rest", + "name": "REST", + "label": "REST API", + "category": "patterns", + "description": "Representational State Transfer - an architectural style for distributed systems", + "icon": "fas fa-plug", + "color": "#0D6EFD" + }, + { + "key": "grpc", + "name": "gRPC", + "label": "gRPC", + "category": "patterns", + "description": "A high-performance, open-source universal RPC framework", + "icon": "fas fa-bolt", + "color": "#244C5A", + "website": "https://grpc.io" + }, + { + "key": "binance", + "name": "Binance API", + "label": "Binance API", + "category": "apis", + "description": "Cryptocurrency exchange API for trading and market data", + "icon": "fas fa-coins", + "color": "#F0B90B", + "website": "https://binance-docs.github.io/apidocs" + }, + { + "key": "deribit", + "name": "Deribit API", + "label": "Deribit API", + "category": "apis", + "description": "Cryptocurrency derivatives exchange API for options and futures trading", + "icon": "fas fa-chart-line", + "color": "#00A859", + "website": "https://docs.deribit.com" + }, + { + "key": "stripe", + "name": "Stripe", + "label": "Stripe", + "category": "apis", + "description": "A suite of payment APIs that powers commerce for businesses of all sizes", + "icon": "fab fa-stripe", + "color": "#635BFF", + "website": "https://stripe.com" + }, + { + "key": "twilio", + "name": "Twilio", + "label": "Twilio", + "category": "apis", + "description": "Cloud communications platform for SMS, voice, and video", + "icon": "fas fa-phone", + "color": "#F22F46", + "website": "https://www.twilio.com" + }, + { + "key": "sendgrid", + "name": "SendGrid", + "label": "SendGrid", + "category": "apis", + "description": "A cloud-based email delivery service for transactional and marketing emails", + "icon": "fas fa-envelope", + "color": "#1A82E2", + "website": "https://sendgrid.com" + }, + { + "key": "jest", + "name": "Jest", + "label": "Jest", + "category": "testing", + "description": "A delightful JavaScript testing framework with a focus on simplicity", + "icon": "devicon-jest-plain", + "color": "#C21325", + "website": "https://jestjs.io" + }, + { + "key": "vitest", + "name": "Vitest", + "label": "Vitest", + "category": "testing", + "description": "A Vite-native unit test framework with a focus on speed", + "icon": "devicon-vitest-plain", + "color": "#6E9F18", + "website": "https://vitest.dev" + }, + { + "key": "cypress", + "name": "Cypress", + "label": "Cypress", + "category": "testing", + "description": "A next-generation front-end testing tool for web applications", + "icon": "devicon-cypressio-original", + "color": "#17202C", + "website": "https://www.cypress.io" + }, + { + "key": "playwright", + "name": "Playwright", + "label": "Playwright", + "category": "testing", + "description": "A framework for reliable end-to-end testing for modern web apps", + "icon": "fas fa-theater-masks", + "color": "#2EAD33", + "website": "https://playwright.dev" + }, + { + "key": "xunit", + "name": "xUnit", + "label": "xUnit.net", + "category": "testing", + "description": "A free, open-source unit testing tool for the .NET Framework", + "icon": "fas fa-vial", + "color": "#512BD4", + "website": "https://xunit.net" + }, + { + "key": "react-native", + "name": "React Native", + "label": "React Native", + "category": "mobile", + "description": "A framework for building native mobile apps using React", + "icon": "fab fa-react", + "color": "#61DAFB", + "website": "https://reactnative.dev" + }, + { + "key": "flutter", + "name": "Flutter", + "label": "Flutter", + "category": "mobile", + "description": "Google's UI toolkit for building natively compiled applications", + "icon": "devicon-flutter-plain", + "color": "#02569B", + "website": "https://flutter.dev" + }, + { + "key": "swift", + "name": "Swift", + "label": "Swift", + "category": "mobile", + "description": "Apple's powerful and intuitive programming language for iOS/macOS", + "icon": "devicon-swift-plain", + "color": "#FA7343", + "website": "https://developer.apple.com/swift" + }, + { + "key": "kotlin", + "name": "Kotlin", + "label": "Kotlin", + "category": "mobile", + "description": "A modern programming language for Android development", + "icon": "devicon-kotlin-plain", + "color": "#7F52FF", + "website": "https://kotlinlang.org" + }, + { + "key": "maui", + "name": ".NET MAUI", + "label": ".NET MAUI", + "category": "mobile", + "description": "Multi-platform App UI for building cross-platform applications with .NET", + "icon": "devicon-dotnetcore-plain", + "color": "#512BD4", + "website": "https://dotnet.microsoft.com/apps/maui" + }, + { + "key": "tailwindcss", + "name": "TailwindCSS", + "label": "Tailwind CSS", + "category": "styling", + "description": "A utility-first CSS framework for rapidly building custom designs", + "icon": "devicon-tailwindcss-plain", + "color": "#06B6D4", + "website": "https://tailwindcss.com" + }, + { + "key": "sass", + "name": "Sass", + "label": "Sass/SCSS", + "category": "styling", + "description": "A mature, stable, and powerful professional grade CSS extension language", + "icon": "devicon-sass-original", + "color": "#CC6699", + "website": "https://sass-lang.com" + }, + { + "key": "bootstrap", + "name": "Bootstrap", + "label": "Bootstrap", + "category": "styling", + "description": "The most popular HTML, CSS, and JS library in the world", + "icon": "devicon-bootstrap-plain", + "color": "#7952B3", + "website": "https://getbootstrap.com" + }, + { + "key": "materialui", + "name": "Material UI", + "label": "Material UI", + "category": "styling", + "description": "React components for faster and easier web development", + "icon": "devicon-materialui-plain", + "color": "#007FFF", + "website": "https://mui.com" + }, + { + "key": "python", + "name": "Python", + "label": "Python", + "category": "programming-language", + "description": "A high-level, general-purpose programming language known for its readability, simplicity, and versatility.", + "abbreviation": null, + "icon": "fab fa-python", + "color": "#3776AB", + "website": "https://www.python.org" + }, + { + "key": "blockchain", + "name": "Blockchain", + "label": "Blockchain Technology", + "category": "distributed-ledger", + "description": "A decentralized, distributed ledger technology that enables secure, transparent, and tamper-resistant record-keeping across cryptocurrencies and dApps.", + "abbreviation": null, + "icon": "fas fa-link", + "color": "#1E88E5", + "website": "https://www.ibm.com/topics/what-is-blockchain" + }, + { + "key": "pow", + "name": "Proof-of-Work", + "label": "Proof-of-Work (PoW)", + "category": "consensus-mechanism", + "description": "A blockchain consensus algorithm requiring computational effort to validate transactions and secure the network against attacks.", + "abbreviation": "PoW", + "icon": "fas fa-puzzle-piece", + "color": "#FF9900", + "website": "https://bitcoin.org/en/glossary/proof-of-work" + }, + { + "key": "hiveos", + "name": "Hive OS", + "label": "Hive OS", + "category": "mining-platform", + "description": "A specialized Linux-based operating system for managing and monitoring large-scale cryptocurrency mining farms (GPU and ASIC rigs).", + "abbreviation": null, + "icon": "fas fa-server", + "color": "#FFA500", + "website": "https://hiveon.com/os/" + }, + { + "key": "antminer", + "name": "Antminer", + "label": "Bitmain Antminer", + "category": "mining-hardware", + "description": "Industry-leading series of ASIC miners for Bitcoin and other SHA-256 cryptocurrencies, widely used in large mining farms.", + "abbreviation": null, + "icon": "fas fa-microchip", + "color": "#000000", + "website": "https://www.bitmain.com/" + }, + { + "key": "erc20", + "name": "ERC-20", + "label": "ERC-20 Token Standard", + "category": "blockchain-standard", + "description": "The most widely used Ethereum token standard for creating fungible tokens, enabling interoperability across wallets, exchanges, and dApps.", + "abbreviation": "ERC-20", + "icon": "fas fa-coins", + "color": "#627EEA", + "website": "https://ethereum.org/en/developers/docs/standards/tokens/erc-20/" + }, + { + "key": "ethereum", + "name": "Ethereum", + "label": "Ethereum (ETH)", + "category": "blockchain-platform", + "description": "A decentralized blockchain platform that enables smart contracts, dApps, and ERC-20 tokens with proof-of-stake consensus.", + "abbreviation": "ETH", + "icon": "fab fa-ethereum", + "color": "#627EEA", + "website": "https://ethereum.org" + }, + { + "key": "solana", + "name": "Solana", + "label": "Solana (SOL)", + "category": "blockchain-platform", + "description": "A high-performance layer-1 blockchain using proof-of-history and proof-of-stake for fast, low-cost transactions and scalable dApps.", + "abbreviation": "SOL", + "icon": "fas fa-bolt", + "color": "#14F195", + "website": "https://solana.com" + }, + { + "key": "polygon", + "name": "Polygon", + "label": "Polygon (POL)", + "category": "blockchain-layer2", + "description": "A layer-2 scaling solution for Ethereum that provides faster and cheaper transactions while maintaining security and compatibility.", + "abbreviation": "POL", + "icon": "fas fa-cubes", + "color": "#8247E5", + "website": "https://polygon.technology" + }, + { + "key": "team-management", + "name": "Team Management", + "label": "Team Leadership & Management", + "category": "leadership", + "description": "Experience in leading, coordinating, and managing hybrid teams (in-person and remote) of up to 7 members, with proven leadership in entrepreneurial environments.", + "abbreviation": null, + "icon": "fas fa-users-cog", + "color": "#4CAF50", + "website": null + }, + { + "key": "html", + "name": "HTML", + "label": "HTML5", + "category": "markup-language", + "description": "The standard markup language for creating and structuring web pages and web applications.", + "abbreviation": "HTML", + "icon": "fab fa-html5", + "color": "#E34F26", + "website": "https://html.spec.whatwg.org/" + }, + { + "key": "css", + "name": "CSS", + "label": "CSS3", + "category": "styling-language", + "description": "A stylesheet language used for describing the presentation and layout of web documents written in HTML.", + "abbreviation": "CSS", + "icon": "fab fa-css3-alt", + "color": "#1572B6", + "website": "https://www.w3.org/Style/CSS/" + }, + { + "key": "scss", + "name": "SCSS", + "label": "SCSS (Sass)", + "category": "css-preprocessor", + "description": "A CSS preprocessor that adds features like variables, nesting, and mixins for more maintainable and powerful stylesheets.", + "abbreviation": "SCSS", + "icon": "fab fa-sass", + "color": "#CC6699", + "website": "https://sass-lang.com" + }, + { + "key": "json", + "name": "JSON", + "label": "JSON", + "category": "data-format", + "description": "A lightweight data-interchange format that is easy for humans to read and write, and easy for machines to parse and generate.", + "abbreviation": "JSON", + "icon": "fas fa-file-code", + "color": "#000000", + "website": "https://www.json.org" + }, + { + "key": "binance-api", + "name": "Binance API", + "label": "Binance API", + "category": "crypto-exchange-api", + "description": "Official REST and WebSocket API provided by Binance for programmatic trading, market data access, and account management on the world's largest cryptocurrency exchange.", + "abbreviation": null, + "icon": "fas fa-exchange-alt", + "color": "#F0B90B", + "website": "https://github.com/binance/binance-spot-api-docs" + }, + { + "key": "ethical-hacking", + "name": "Ethical Hacking", + "label": "Ethical Hacking / Penetration Testing", + "category": "cybersecurity", + "description": "Authorized testing of systems and networks to identify vulnerabilities and improve security (also known as white-hat hacking or penetration testing).", + "abbreviation": null, + "icon": "fas fa-shield-alt", + "color": "#00FF00", + "website": "https://www.eccouncil.org/programs/certified-ethical-hacker-ceh/" + }, + { + "key": "figma", + "name": "Figma", + "label": "Figma", + "category": "styling", + "description": "A collaborative web-based design tool for UI/UX design", + "icon": "devicon-figma-plain", + "color": "#F24E1E", + "website": "https://www.figma.com" + }, + { + "key": "adobe-suite", + "name": "Adobe Suite", + "label": "Adobe Creative Suite", + "category": "styling", + "description": "Professional creative software including Photoshop, Illustrator, and more", + "icon": "devicon-photoshop-plain", + "color": "#FF0000", + "website": "https://www.adobe.com/creativecloud.html" + } + ] +} \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes.json new file mode 100644 index 0000000000000000000000000000000000000000..ab8c86b99aef838a53f5b90f6da585a7e85ad2e2 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes.json @@ -0,0 +1,54 @@ +{ + "themes": [ + { + "name": "Professional", + "slug": "professional", + "description": "Polished dark theme with subtle accents for professional impact", + "previewColor": "#00D4FF", + "icon": "◆", + "isDefault": true, + "order": 0, + "cssFile": "theme.professional.json" + }, + { + "name": "Normal Cyber", + "slug": "normal", + "description": "Balanced neon glow with teal-cyan palette", + "previewColor": "#00d4ff", + "icon": "⚡", + "isDefault": false, + "order": 1, + "cssFile": "theme.normal.json" + }, + { + "name": "Neon Cyber", + "slug": "neon", + "description": "Maximum intensity glow effects", + "previewColor": "#00ffff", + "icon": "✨", + "isDefault": false, + "order": 2, + "cssFile": "theme.neon.json" + }, + { + "name": "Minimal", + "slug": "minimal", + "description": "Reduced effects, clean appearance", + "previewColor": "#0099ff", + "icon": "○", + "isDefault": false, + "order": 3, + "cssFile": "theme.minimal.json" + }, + { + "name": "Glass", + "slug": "glass", + "description": "Ultra glassmorphism with soft glow", + "previewColor": "#7dd3fc", + "icon": "◇", + "isDefault": false, + "order": 4, + "cssFile": "theme.glass.json" + } + ] +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.glass.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.glass.json new file mode 100644 index 0000000000000000000000000000000000000000..ef2ae100620f8ce28f541ee833bf68244cb18920 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.glass.json @@ -0,0 +1,3 @@ +{ + "cssContent": "/* Glass Theme - Ultra glassmorphism with soft glow */\n[data-cyber-theme=\"glass\"] {\n --theme-primary: var(--neon-primary, #00d4ff);\n --theme-glow: var(--glow-soft, 0 0 12px rgba(0, 212, 255, 0.4));\n --theme-border: rgba(255, 255, 255, 0.1);\n --glass-blur: var(--glass-blur-ultra, blur(32px));\n \n /* Enhanced glass surfaces */\n --bg-surface: rgba(15, 20, 30, 0.3);\n --bg-surface-light: rgba(20, 25, 35, 0.2);\n\n /* Accent colors */\n --accent: #7dd3fc;\n --accent-muted: #38bdf8;\n --accent-dark: #0ea5e9;\n\n /* Backgrounds (Glass Specific) */\n --bg-primary: #0a0e17; /* Keep dark base */\n --bg-secondary: rgba(15, 23, 42, 0.4);\n --bg-card: rgba(255, 255, 255, 0.05);\n --bg-card-elevated: rgba(255, 255, 255, 0.08);\n\n /* High contrast text - Light for glass */\n --text-primary: #ffffff;\n --text-secondary: #e0f2fe;\n --text-muted: #bae6fd;\n\n /* Borders */\n --border-color: rgba(255, 255, 255, 0.1);\n --border-color-hover: rgba(255, 255, 255, 0.2);\n\n /* Admin semantic variables */\n --admin-panel-bg: rgba(255, 255, 255, 0.08);\n --admin-panel-border: rgba(255, 255, 255, 0.15);\n --admin-card-bg: rgba(255, 255, 255, 0.08);\n --admin-input-bg: rgba(0, 0, 0, 0.2);\n --admin-text-primary: #ffffff;\n --admin-text-secondary: #cbd5e1;\n}\n\n/* Glass mode panel styling - translucent with heavy blur */\n[data-cyber-theme=\"glass\"] .panel-cyber,\n[data-cyber-theme=\"glass\"] .card-cyber {\n background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.01));\n backdrop-filter: blur(24px);\n -webkit-backdrop-filter: blur(24px);\n border: 1px solid rgba(255, 255, 255, 0.08);\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.37), inset 0 0 0 1px rgba(255, 255, 255, 0.02);\n}\n\n[data-cyber-theme=\"glass\"] .panel-cyber:hover,\n[data-cyber-theme=\"glass\"] .card-cyber:hover {\n background: linear-gradient(135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02));\n border-color: rgba(255, 255, 255, 0.15);\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45), 0 0 20px rgba(125, 211, 252, 0.2);\n transform: translateY(-6px);\n}\n\n/* Glass mode text styling - soft glow */\n[data-cyber-theme=\"glass\"] .text-theme-primary {\n color: #7dd3fc;\n}\n\n[data-cyber-theme=\"glass\"] .text-theme-glow {\n color: #7dd3fc;\n text-shadow: 0 0 20px rgba(125, 211, 252, 0.5);\n}\n\n/* Glass mode button styling - translucent */\n[data-cyber-theme=\"glass\"] .btn-theme {\n background: linear-gradient(135deg, rgba(125, 211, 252, 0.3), rgba(125, 211, 252, 0.1));\n backdrop-filter: blur(12px);\n color: white;\n border: 1px solid rgba(255, 255, 255, 0.1);\n box-shadow: 0 0 20px rgba(125, 211, 252, 0.2);\n}\n\n[data-cyber-theme=\"glass\"] .btn-theme:hover {\n background: linear-gradient(135deg, rgba(125, 211, 252, 0.4), rgba(125, 211, 252, 0.2));\n box-shadow: 0 0 30px rgba(125, 211, 252, 0.4);\n transform: translateY(-2px);\n}\n\n/* Glass mode special - frosted glass effect */\n[data-cyber-theme=\"glass\"] .glass-frost {\n background: rgba(255, 255, 255, 0.05);\n backdrop-filter: blur(20px) saturate(180%);\n -webkit-backdrop-filter: blur(20px) saturate(180%);\n}" +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.minimal.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.minimal.json new file mode 100644 index 0000000000000000000000000000000000000000..b71ba43e2619a47f7cf5352437deacd83034071a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.minimal.json @@ -0,0 +1,3 @@ +{ + "cssContent": "/* Minimal Theme - Reduced effects, clean appearance */\n[data-cyber-theme=\"minimal\"] {\n --theme-primary: var(--neon-blue, #0099ff);\n --theme-glow: var(--glow-soft, 0 0 12px rgba(0, 212, 255, 0.4));\n --theme-border: var(--border-neon, rgba(0, 212, 255, 0.2));\n --glass-blur: var(--glass-blur-light, blur(8px));\n \n /* Disable some effects */\n --glow-layered: none;\n --gradient-shine: none;\n\n /* Accent colors */\n --accent: #0099ff;\n --accent-muted: #3b82f6;\n --accent-dark: #1d4ed8;\n\n /* Backgrounds */\n --bg-primary: #0f172a;\n --bg-secondary: #1e293b;\n --bg-card: rgba(30, 41, 59, 0.5);\n --bg-card-elevated: rgba(51, 65, 85, 0.6);\n\n /* High contrast text */\n --text-primary: #f1f5f9;\n --text-secondary: #cbd5e1;\n --text-muted: #94a3b8;\n\n /* Borders */\n --border-color: rgba(0, 153, 255, 0.2);\n --border-color-hover: rgba(0, 153, 255, 0.4);\n\n /* Admin semantic variables */\n --admin-panel-bg: rgba(255, 255, 255, 0.02);\n --admin-panel-border: rgba(255, 255, 255, 0.1);\n --admin-card-bg: rgba(255, 255, 255, 0.02);\n --admin-input-bg: rgba(0, 0, 0, 0.2);\n --admin-text-primary: #f8fafc;\n --admin-text-secondary: #94a3b8;\n}\n\n/* Minimal mode panel styling - clean, subtle */\n[data-cyber-theme=\"minimal\"] .panel-cyber,\n[data-cyber-theme=\"minimal\"] .card-cyber {\n background: var(--bg-surface-dark, rgba(10, 15, 25, 0.8));\n border: 1px solid rgba(0, 153, 255, 0.2);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);\n}\n\n[data-cyber-theme=\"minimal\"] .panel-cyber:hover,\n[data-cyber-theme=\"minimal\"] .card-cyber:hover {\n border-color: rgba(0, 153, 255, 0.4);\n box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);\n transform: translateY(-4px);\n}\n\n/* Minimal mode text styling - no glow */\n[data-cyber-theme=\"minimal\"] .text-theme-primary {\n color: var(--neon-blue, #0099ff);\n}\n\n[data-cyber-theme=\"minimal\"] .text-theme-glow {\n color: var(--neon-blue, #0099ff);\n text-shadow: none;\n}\n\n/* Minimal mode button styling - flat design */\n[data-cyber-theme=\"minimal\"] .btn-theme {\n background: var(--neon-blue, #0099ff);\n color: white;\n border: none;\n box-shadow: none;\n}\n\n[data-cyber-theme=\"minimal\"] .btn-theme:hover {\n background: #007acc;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n transform: translateY(-2px);\n}\n\n/* Disable animations in minimal mode */\n[data-cyber-theme=\"minimal\"] .shimmer-neon,\n[data-cyber-theme=\"minimal\"] .glow-pulse,\n[data-cyber-theme=\"minimal\"] .border-neon-pulse {\n animation: none;\n}" +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.neon.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.neon.json new file mode 100644 index 0000000000000000000000000000000000000000..cb3fc969e060a7f43e6414771c4c5366b389f920 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.neon.json @@ -0,0 +1,3 @@ +{ + "cssContent": "/* Neon Cyber Theme - Maximum intensity glow */\n[data-cyber-theme=\"neon\"] {\n --theme-primary: var(--neon-cyan, #00ffff);\n --theme-glow: var(--glow-strong, 0 0 24px rgba(0, 212, 255, 0.9));\n --theme-border: var(--border-neon-glow, rgba(0, 212, 255, 0.8));\n --glass-blur: var(--glass-blur-light, blur(8px));\n\n /* Accent colors */\n --accent: #00ffff;\n --accent-muted: #00d4ff;\n --accent-dark: #00a3cc;\n\n /* Backgrounds */\n --bg-primary: #050a10;\n --bg-secondary: #0a0f1e;\n --bg-card: rgba(10, 15, 25, 0.85);\n --bg-card-elevated: rgba(20, 30, 40, 0.9);\n\n /* High contrast text */\n --text-primary: #e0f2fe;\n --text-secondary: #bae6fd;\n --text-muted: #7dd3fc;\n\n /* Borders */\n --border-color: rgba(0, 255, 255, 0.3);\n --border-color-hover: rgba(0, 255, 255, 0.6);\n\n /* Admin semantic variables */\n --admin-panel-bg: rgba(20, 30, 40, 0.85);\n --admin-panel-border: rgba(0, 255, 255, 0.4);\n --admin-card-bg: rgba(20, 30, 40, 0.85);\n --admin-input-bg: rgba(0, 0, 0, 0.6);\n --admin-text-primary: #e0f2fe;\n --admin-text-secondary: #bae6fd;\n}\n\n/* Neon mode panel styling - enhanced glow */\n[data-cyber-theme=\"neon\"] .panel-cyber,\n[data-cyber-theme=\"neon\"] .card-cyber {\n background: var(--bg-surface, rgba(15, 20, 30, 0.65));\n border: 1px solid var(--neon-primary, #00d4ff);\n box-shadow: 0 0 20px rgba(0, 255, 255, 0.5), 0 8px 32px rgba(0, 0, 0, 0.5);\n}\n\n[data-cyber-theme=\"neon\"] .panel-cyber:hover,\n[data-cyber-theme=\"neon\"] .card-cyber:hover {\n border-color: var(--neon-cyan, #00ffff);\n box-shadow: 0 0 30px rgba(0, 255, 255, 0.7), 0 0 60px rgba(0, 212, 255, 0.3), 0 12px 40px rgba(0, 0, 0, 0.6);\n transform: translateY(-8px);\n}\n\n/* Neon mode text styling - strong glow */\n[data-cyber-theme=\"neon\"] .text-theme-primary {\n color: var(--neon-cyan, #00ffff);\n}\n\n[data-cyber-theme=\"neon\"] .text-theme-glow {\n color: var(--neon-cyan, #00ffff);\n text-shadow: 0 0 10px rgba(0, 255, 255, 0.8), 0 0 20px rgba(0, 255, 255, 0.5), 0 0 30px rgba(0, 255, 255, 0.3);\n}\n\n/* Neon mode button styling - intense glow */\n[data-cyber-theme=\"neon\"] .btn-theme {\n background: linear-gradient(135deg, #00ffff 0%, #00d4ff 100%);\n color: #0a0e17;\n border: none;\n box-shadow: 0 0 20px rgba(0, 255, 255, 0.5);\n}\n\n[data-cyber-theme=\"neon\"] .btn-theme:hover {\n box-shadow: 0 0 30px rgba(0, 255, 255, 0.7);\n transform: translateY(-3px);\n}\n\n/* Neon mode special accents */\n[data-cyber-theme=\"neon\"] .accent-glow {\n animation: neon-pulse 2s ease-in-out infinite;\n}\n\n@keyframes neon-pulse {\n 0%, 100% { box-shadow: 0 0 10px rgba(0, 255, 255, 0.5); }\n 50% { box-shadow: 0 0 30px rgba(0, 255, 255, 0.9); }\n}" +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.normal.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.normal.json new file mode 100644 index 0000000000000000000000000000000000000000..6106df18025c9f5d32b11b8f28976ed264dae2ce --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.normal.json @@ -0,0 +1,3 @@ +{ + "cssContent": "/* Normal Cyber Theme - Balanced neon glow */\n[data-cyber-theme=\"normal\"] {\n --theme-primary: var(--neon-primary, #00d4ff);\n --theme-glow: var(--glow-medium, 0 0 16px rgba(0, 212, 255, 0.7));\n --theme-border: var(--border-neon, rgba(0, 212, 255, 0.2));\n --glass-blur: var(--glass-blur-medium, blur(16px));\n \n /* Accent colors */\n --accent: #00d4ff;\n --accent-muted: #00b8e6;\n --accent-dark: #0099cc;\n\n /* Backgrounds */\n --bg-primary: #0f172a;\n --bg-secondary: #1e293b;\n --bg-card: rgba(30, 41, 59, 0.75);\n --bg-card-elevated: rgba(51, 65, 85, 0.8);\n\n /* High contrast text */\n --text-primary: #f8fafc;\n --text-secondary: #cbd5e1;\n --text-muted: #94a3b8;\n\n /* Borders */\n --border-color: rgba(0, 212, 255, 0.2);\n --border-color-hover: rgba(0, 212, 255, 0.4);\n\n /* Admin semantic variables */\n --admin-panel-bg: rgba(30, 41, 59, 0.65);\n --admin-panel-border: rgba(0, 212, 255, 0.2);\n --admin-card-bg: rgba(30, 41, 59, 0.65);\n --admin-input-bg: rgba(10, 15, 25, 0.8);\n --admin-text-primary: #e0e7ff;\n --admin-text-secondary: #94a3b8;\n}\n\n/* Normal mode panel styling */\n[data-cyber-theme=\"normal\"] .panel-cyber,\n[data-cyber-theme=\"normal\"] .card-cyber {\n background: var(--bg-surface, rgba(15, 20, 30, 0.65));\n border: 1px solid var(--border-neon, rgba(0, 212, 255, 0.2));\n box-shadow: var(--shadow-neon-md, 0 4px 16px rgba(0, 0, 0, 0.4), 0 0 12px rgba(0, 212, 255, 0.3));\n}\n\n[data-cyber-theme=\"normal\"] .panel-cyber:hover,\n[data-cyber-theme=\"normal\"] .card-cyber:hover {\n border-color: var(--border-neon-bright, rgba(0, 212, 255, 0.5));\n box-shadow: var(--shadow-neon-lg, 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 20px rgba(0, 212, 255, 0.4));\n}\n\n/* Normal mode text styling */\n[data-cyber-theme=\"normal\"] .text-theme-primary {\n color: var(--neon-primary, #00d4ff);\n}\n\n[data-cyber-theme=\"normal\"] .text-theme-glow {\n color: var(--neon-primary, #00d4ff);\n text-shadow: var(--glow-medium, 0 0 16px rgba(0, 212, 255, 0.7));\n}\n\n/* Normal mode button styling */\n[data-cyber-theme=\"normal\"] .btn-theme {\n background: linear-gradient(135deg, #00d4ff 0%, #00ffff 100%);\n color: #0a0e17;\n border: none;\n box-shadow: 0 0 12px rgba(0, 212, 255, 0.4);\n}\n\n[data-cyber-theme=\"normal\"] .btn-theme:hover {\n box-shadow: 0 0 20px rgba(0, 212, 255, 0.6);\n transform: translateY(-2px);\n}" +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.professional.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.professional.json new file mode 100644 index 0000000000000000000000000000000000000000..aaada658296939dca285119a6ec2d39d0a3cdf61 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/JsonDatas/themes/theme.professional.json @@ -0,0 +1,3 @@ +{ + "cssContent": "/* Professional Theme - Polished dark with teal accents */\n[data-cyber-theme=\"professional\"] {\n /* Primary accent colors */\n --theme-primary: #00D4FF;\n --theme-glow: 0 0 8px rgba(0, 212, 255, 0.3);\n --theme-border: rgba(51, 65, 85, 0.6);\n --glass-blur: blur(12px);\n \n /* Accent colors */\n --accent: #00D4FF;\n --accent-muted: #06B6D4;\n --accent-dark: #0891B2;\n \n /* Backgrounds */\n --bg-primary: #0A0E17;\n --bg-secondary: #0F172A;\n --bg-card: rgba(15, 23, 42, 0.85);\n --bg-card-elevated: rgba(30, 41, 59, 0.9);\n \n /* High contrast text */\n --text-primary: #F1F5F9;\n --text-secondary: #94A3B8;\n --text-muted: #64748B;\n \n /* Borders */\n --border-color: rgba(51, 65, 85, 0.6);\n --border-color-hover: rgba(0, 212, 255, 0.4);\n \n /* Admin variables */\n --admin-panel-bg: rgba(15, 23, 42, 0.9);\n --admin-panel-border: rgba(51, 65, 85, 0.6);\n --admin-card-bg: rgba(30, 41, 59, 0.85);\n --admin-input-bg: rgba(10, 15, 25, 0.9);\n --admin-text-primary: #F1F5F9;\n --admin-text-secondary: #94A3B8;\n}\n\n/* ============================================\n Tab Control Styling\n ============================================ */\n\n[data-cyber-theme=\"professional\"] .tab-control {\n --tab-container-bg: rgba(15, 23, 42, 0.85);\n --tab-container-border: rgba(51, 65, 85, 0.6);\n --tab-bg: transparent;\n --tab-bg-hover: rgba(0, 212, 255, 0.1);\n --tab-bg-active: #00D4FF;\n --tab-text: #94A3B8;\n --tab-text-hover: #F1F5F9;\n --tab-text-active: #0A0E17;\n --tab-shadow-active: 0 2px 8px rgba(0, 212, 255, 0.3);\n}\n\n/* ============================================\n Dashboard Stat Cards\n ============================================ */\n\n[data-cyber-theme=\"professional\"] .stat-card {\n background: rgba(15, 23, 42, 0.85);\n border: 1px solid rgba(51, 65, 85, 0.6);\n border-radius: 1rem;\n padding: 1.5rem;\n}\n\n[data-cyber-theme=\"professional\"] .stat-card:hover {\n border-color: rgba(0, 212, 255, 0.5);\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);\n}\n\n[data-cyber-theme=\"professional\"] .stat-label {\n color: #94A3B8;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n}\n\n[data-cyber-theme=\"professional\"] .stat-value {\n color: #F1F5F9;\n font-size: 2rem;\n font-weight: 700;\n}\n\n/* ============================================\n Timeline / Experience Cards\n ============================================ */\n\n[data-cyber-theme=\"professional\"] .timeline-card {\n background: rgba(15, 23, 42, 0.85);\n border: 1px solid rgba(51, 65, 85, 0.6);\n border-radius: 1rem;\n padding: 2rem;\n}\n\n[data-cyber-theme=\"professional\"] .timeline-card:hover {\n border-color: #00D4FF;\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);\n}\n\n/* Date Badge - Teal filled */\n[data-cyber-theme=\"professional\"] .timeline-card-date {\n background: #00D4FF;\n color: #0A0E17;\n padding: 0.375rem 0.75rem;\n border-radius: 9999px;\n font-size: 0.75rem;\n font-weight: 600;\n}\n\n[data-cyber-theme=\"professional\"] .timeline-card-date[data-current=\"true\"] {\n background: #10B981;\n color: #FFFFFF;\n}\n\n/* Type Badge - Teal outline */\n[data-cyber-theme=\"professional\"] .timeline-type-badge {\n padding: 0.375rem 0.75rem;\n font-size: 0.6875rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n border-radius: 0.375rem;\n background: transparent;\n border: 1px solid;\n}\n\n[data-cyber-theme=\"professional\"] .timeline-type-badge[data-type=\"experience\"] {\n color: #00D4FF;\n border-color: #00D4FF;\n}\n\n[data-cyber-theme=\"professional\"] .timeline-type-badge[data-type=\"education\"] {\n color: #10B981;\n border-color: #10B981;\n}\n\n[data-cyber-theme=\"professional\"] .timeline-type-badge[data-type=\"achievement\"] {\n color: #F59E0B;\n border-color: #F59E0B;\n}\n\n/* Card Content Text */\n[data-cyber-theme=\"professional\"] .timeline-card-title {\n font-size: 1.25rem;\n font-weight: 700;\n color: #FFFFFF;\n}\n\n[data-cyber-theme=\"professional\"] .timeline-card-org {\n font-size: 0.9375rem;\n color: #94A3B8;\n}\n\n[data-cyber-theme=\"professional\"] .timeline-card-org svg {\n color: #64748B;\n}\n\n[data-cyber-theme=\"professional\"] .timeline-card-desc {\n font-size: 0.9375rem;\n color: #CBD5E1;\n line-height: 1.75;\n}\n\n/* Technology Badges */\n[data-cyber-theme=\"professional\"] .timeline-card-skills {\n border-top: 1px solid rgba(51, 65, 85, 0.6);\n padding-top: 1.25rem;\n margin-top: 1.25rem;\n}\n\n[data-cyber-theme=\"professional\"] .timeline-card-skills .tech-badge,\n[data-cyber-theme=\"professional\"] .tech-badge {\n padding: 0.375rem 0.75rem;\n font-size: 0.8125rem;\n font-weight: 500;\n color: #F1F5F9;\n background: rgba(30, 41, 59, 0.8);\n border: 1px solid rgba(148, 163, 184, 0.4);\n border-radius: 0.375rem;\n}\n\n[data-cyber-theme=\"professional\"] .timeline-card-skills .tech-badge:hover,\n[data-cyber-theme=\"professional\"] .tech-badge:hover {\n border-color: #00D4FF;\n background: rgba(0, 212, 255, 0.1);\n color: #FFFFFF;\n}\n\n/* ============================================\n Skill Category Cards\n ============================================ */\n\n[data-cyber-theme=\"professional\"] .resume-skill-category {\n background: rgba(15, 23, 42, 0.85);\n border: 1px solid rgba(51, 65, 85, 0.6);\n border-radius: 1rem;\n}\n\n[data-cyber-theme=\"professional\"] .resume-skill-category:hover {\n border-color: #00D4FF;\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);\n}\n\n[data-cyber-theme=\"professional\"] .resume-skill-category-header {\n border-bottom: 1px solid rgba(51, 65, 85, 0.6);\n padding: 1.25rem;\n}\n\n[data-cyber-theme=\"professional\"] .resume-skill-category-name {\n font-size: 1.125rem;\n font-weight: 700;\n color: #F1F5F9;\n}\n\n[data-cyber-theme=\"professional\"] .resume-skill-item {\n padding: 0.375rem 0.75rem;\n background: rgba(30, 41, 59, 0.8);\n border: 1px solid rgba(148, 163, 184, 0.4);\n border-radius: 0.375rem;\n font-size: 0.8125rem;\n font-weight: 500;\n color: #F1F5F9;\n}\n\n[data-cyber-theme=\"professional\"] .resume-skill-item:hover {\n border-color: #00D4FF;\n background: rgba(0, 212, 255, 0.1);\n}\n\n/* ============================================\n Language Cards\n ============================================ */\n\n[data-cyber-theme=\"professional\"] .resume-language-card {\n background: rgba(15, 23, 42, 0.85);\n border: 1px solid rgba(51, 65, 85, 0.6);\n border-radius: 1rem;\n padding: 1.5rem;\n}\n\n[data-cyber-theme=\"professional\"] .resume-language-card:hover {\n border-color: #00D4FF;\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);\n}\n\n[data-cyber-theme=\"professional\"] .resume-language-card .language-name {\n font-size: 1.25rem;\n font-weight: 700;\n color: #F1F5F9;\n}\n\n[data-cyber-theme=\"professional\"] .resume-language-card .skill-label {\n color: #94A3B8;\n}\n\n/* ============================================\n Domain Cards\n ============================================ */\n\n[data-cyber-theme=\"professional\"] .resume-domain-card {\n background: rgba(15, 23, 42, 0.85);\n border: 1px solid rgba(51, 65, 85, 0.6);\n border-radius: 1rem;\n padding: 1.5rem;\n}\n\n[data-cyber-theme=\"professional\"] .resume-domain-card:hover {\n border-color: #00D4FF;\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);\n}\n\n[data-cyber-theme=\"professional\"] .resume-domain-card .domain-icon {\n width: 48px;\n height: 48px;\n background: rgba(0, 212, 255, 0.15);\n border: 1px solid rgba(0, 212, 255, 0.3);\n border-radius: 0.75rem;\n color: #00D4FF;\n}\n\n[data-cyber-theme=\"professional\"] .resume-domain-card .domain-name {\n font-size: 1.25rem;\n font-weight: 700;\n color: #F1F5F9;\n}\n\n[data-cyber-theme=\"professional\"] .resume-domain-card .domain-years {\n color: #94A3B8;\n}\n\n[data-cyber-theme=\"professional\"] .resume-domain-card .domain-level {\n background: #00D4FF;\n color: #0A0E17;\n padding: 0.375rem 1rem;\n border-radius: 9999px;\n font-size: 0.6875rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n}\n\n[data-cyber-theme=\"professional\"] .resume-domain-card .domain-description {\n color: #94A3B8;\n border-top: 1px solid rgba(51, 65, 85, 0.6);\n padding-top: 0.75rem;\n margin-top: 0.75rem;\n}\n\n/* ============================================\n Hobby Cards\n ============================================ */\n\n[data-cyber-theme=\"professional\"] .resume-hobby-card {\n background: rgba(15, 23, 42, 0.85);\n border: 1px solid rgba(51, 65, 85, 0.6);\n border-radius: 1rem;\n padding: 1.25rem;\n}\n\n[data-cyber-theme=\"professional\"] .resume-hobby-card:hover {\n border-color: #00D4FF;\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);\n}\n\n[data-cyber-theme=\"professional\"] .resume-hobby-card .hobby-icon {\n width: 48px;\n height: 48px;\n background: rgba(0, 212, 255, 0.15);\n border: 1px solid rgba(0, 212, 255, 0.3);\n color: #00D4FF;\n border-radius: 0.75rem;\n}\n\n[data-cyber-theme=\"professional\"] .resume-hobby-card .hobby-name {\n font-size: 1.125rem;\n font-weight: 700;\n color: #F1F5F9;\n}\n\n[data-cyber-theme=\"professional\"] .resume-hobby-card .hobby-description {\n color: #94A3B8;\n}\n\n/* ============================================\n Project Cards\n ============================================ */\n\n[data-cyber-theme=\"professional\"] .project-card {\n background: rgba(15, 23, 42, 0.85);\n border: 1px solid rgba(51, 65, 85, 0.6);\n border-radius: 1rem;\n}\n\n[data-cyber-theme=\"professional\"] .project-card:hover {\n border-color: #00D4FF;\n box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);\n}\n\n[data-cyber-theme=\"professional\"] .project-card-status,\n[data-cyber-theme=\"professional\"] .project-card-footer .tech-label {\n color: #00D4FF;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n}\n\n[data-cyber-theme=\"professional\"] .tech-more {\n color: #00D4FF;\n background: rgba(0, 212, 255, 0.1);\n}\n\n/* ============================================\n Hero Summary Card\n ============================================ */\n\n[data-cyber-theme=\"professional\"] .resume-summary {\n background: rgba(15, 23, 42, 0.85);\n border: 1px solid rgba(51, 65, 85, 0.6);\n border-radius: 1.5rem;\n padding: 2.5rem;\n color: #94A3B8;\n}\n\n/* ============================================\n Buttons\n ============================================ */\n\n[data-cyber-theme=\"professional\"] .btn-theme {\n background: linear-gradient(135deg, #00D4FF 0%, #06B6D4 100%);\n color: #0A0E17;\n border: none;\n box-shadow: 0 2px 8px rgba(0, 212, 255, 0.3);\n font-weight: 600;\n}\n\n[data-cyber-theme=\"professional\"] .btn-theme:hover {\n box-shadow: 0 4px 16px rgba(0, 212, 255, 0.4);\n transform: translateY(-2px);\n}\n\n/* ============================================\n Panels and Cards - Generic\n ============================================ */\n\n[data-cyber-theme=\"professional\"] .panel-cyber,\n[data-cyber-theme=\"professional\"] .card-cyber {\n background: rgba(15, 23, 42, 0.85);\n border: 1px solid rgba(51, 65, 85, 0.6);\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(12px);\n}\n\n[data-cyber-theme=\"professional\"] .panel-cyber:hover,\n[data-cyber-theme=\"professional\"] .card-cyber:hover {\n border-color: #00D4FF;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n}\n\n/* ============================================\n Text Utilities\n ============================================ */\n\n[data-cyber-theme=\"professional\"] .text-theme-primary {\n color: #00D4FF;\n}\n\n[data-cyber-theme=\"professional\"] .text-theme-glow {\n color: #00D4FF;\n text-shadow: 0 0 12px rgba(0, 212, 255, 0.4);\n}\n\n/* ============================================\n Disable aggressive animations\n ============================================ */\n\n[data-cyber-theme=\"professional\"] .shimmer-neon,\n[data-cyber-theme=\"professional\"] .glow-pulse {\n animation: none;\n}\n\n[data-cyber-theme=\"professional\"] .resume-animate-in {\n animation: professionalFadeIn 0.4s ease-out forwards;\n}\n\n@keyframes professionalFadeIn {\n from {\n opacity: 0;\n transform: translateY(12px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}" +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/ark-alliance-startupcms-ia.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/ark-alliance-startupcms-ia.json new file mode 100644 index 0000000000000000000000000000000000000000..6f5c874b7ea56c2e6bbaea88a369cd4739228f0b --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/ark-alliance-startupcms-ia.json @@ -0,0 +1,81 @@ +{ + "title": "Ark.Alliance.StartupCms.AI", + "description": "AI-powered CMS platform designed specifically for startups. Features collaborative profile/resume building with AI assistance, hierarchical team visualization, task tracking with growth-mindset evaluation, partial public visibility of accomplishments, and rich personal sections (experiences, education, skills, hobbies, domains). Built with Next.js 15 App Router, Prisma ORM, TypeScript 5, and Tailwind/Shadcn UI for modern, scalable, human-centric collaboration.", + "status": "In Development", + "imageUrl": "/Assets/Projects/Ark.Alliance.StartupCms.AI/ArkAllianceStartupCmsAiHero.png", + "repoUrl": "https://github.com/M2H-Machine-to-Human-Race/Ark.Alliance.StartupCms.AI", + "demoUrl": "", + "packageUrl": "", + "technologies": [ + "react", + "nextjs", + "typescript", + "prisma", + "postgresql", + "tailwindcss", + "shadcn", + "openai", + "anthropic", + "gemini", + "jest", + "playwright" + ], + "features": [ + { + "title": "AI-Assisted Profile & Resume Generation", + "description": "Transform natural language descriptions into professional profile sections and comprehensive resumes. Multi-provider AI integration (OpenAI, Anthropic, Gemini) with intelligent prompt templates for profile generation, resume enhancement, and content optimization.", + "icon": "brain" + }, + { + "title": "Team Hierarchy Organigram", + "description": "Visual organizational chart displaying team structure with dynamic hierarchical layouts. Supports multiple organizational levels, role-based permissions, and interactive team member profiles with drill-down capabilities.", + "icon": "sitemap" + }, + { + "title": "Task Tracking & Evaluation", + "description": "Growth-mindset task management system with lesson-learned evaluations. Track tasks, document learnings, celebrate progress, and maintain a culture of continuous improvement aligned with human-centric development principles.", + "icon": "tasks" + }, + { + "title": "Partial Public Visibility", + "description": "Selective disclosure of accomplishments and profile sections. Control what's publicly visible vs. team-internal, enabling professional portfolio showcasing while maintaining privacy for sensitive information.", + "icon": "eye" + }, + { + "title": "Rich Personal Sections", + "description": "Comprehensive profile system with experiences, education, skills (with proficiency levels), hobbies, domain expertise, and custom sections. Supports multimedia content, timeline visualization, and structured data rendering.", + "icon": "user-circle" + }, + { + "title": "Technology & Skills Display", + "description": "Technology badges with category-based organization, proficiency indicators, and years of experience tracking. Visual skill matrices and technology stack documentation for team capabilities and project requirements.", + "icon": "code" + } + ], + "pages": [ + { + "title": "Overview", + "type": "OVERVIEW", + "navOrder": 1, + "content": "# Ark.Alliance.StartupCms.AI – Application Overview & Architecture\n\n**Last Updated**: January 07, 2026 \n**Version**: 1.0.0\n\n## Introduction\n\n**Ark.Alliance.StartupCms.AI** is an AI-powered content management system built specifically for startups. It combines collaborative profile building, AI-assisted content generation, hierarchical team management, and growth-mindset task tracking into a unified platform that emphasizes human-centric design and continuous improvement.\n\n### Core Philosophy\n\nBuilt on the conviction that technology should amplify human potential, this platform integrates:\n\n- 🧠 **AI Augmentation**: AI assists but never replaces human creativity and decision-making\n- 🌱 **Growth Mindset**: Continuous learning through task evaluation and lesson documentation\n- 🤝 **Collaborative Excellence**: Team-based profile building with transparent hierarchies\n- 🎯 **Selective Transparency**: Control what's public vs. team-internal for professional portfolios\n\n### Perfect For\n\n- 🚀 **Startup Teams** — Collaborative profile management and team showcasing\n- 💼 **Growing Organizations** — Hierarchical team structures with clear role definitions\n- 🎓 **Portfolio Building** — AI-assisted resume and profile generation\n- 📊 **Team Analytics** — Track skills, expertise, and organizational capabilities\n- 🌍 **Public Engagement** — Selectively showcase team accomplishments and expertise\n\n### Key Statistics\n\n- **Built on**: Next.js 15 App Router\n- **Database**: Prisma ORM with PostgreSQL\n- **Type Safety**: End-to-end TypeScript 5.x\n- **Styling**: Tailwind CSS + Shadcn UI\n- **AI Providers**: OpenAI, Anthropic, Google Gemini\n\n## Table of Contents\n\n1. [Core Functionalities Overview](#core-functionalities-overview)\n2. [AI-Assisted Profile & Resume Generation](#ai-assisted-profile-resume-generation)\n3. [Team Hierarchy Organigram](#team-hierarchy-organigram)\n4. [Task Tracking & Evaluation](#task-tracking-evaluation)\n5. [Partial Public Visibility](#partial-public-visibility)\n6. [Rich Personal Sections Rendering](#rich-personal-sections-rendering)\n7. [Technology & Skills Display](#technology-skills-display)\n8. [Architecture Overview](#architecture-overview)\n9. [Development Lifecycle & Quality Assurance](#development-lifecycle-quality-assurance)\n10. [Best Practices & Philosophy](#best-practices-philosophy)\n\n---\n\n## Core Functionalities Overview\n\n| Functionality | Description | User Benefits |\n|---------------|-------------|---------------|\n| **AI Profile Generation** | Natural language to professional profile | Save time, maintain consistency |\n| **Team Organigram** | Visual hierarchical team structure | Clear reporting lines, role clarity |\n| **Task System** | Growth-mindset task tracking | Continuous improvement culture |\n| **Visibility Control** | Public/private section toggles | Professional portfolios + privacy |\n| **Personal Sections** | Experiences, education, skills, hobbies | Comprehensive team profiles |\n| **Tech Stack Display** | Technology badges and skill matrices | Capability documentation |" + }, + { + "title": "Detailed Functionalities", + "type": "FUNCTIONAL", + "navOrder": 2, + "content": "## AI-Assisted Profile & Resume Generation\n\n### Description\n\nTransform simple text descriptions into polished, professional profile content. The system uses multi-provider AI (OpenAI GPT-4, Anthropic Claude, Google Gemini) with intelligent prompt templates to generate:\n\n- Professional profile summaries from brief descriptions\n- Enhanced resume sections (experience, education, skills)\n- Optimized content for clarity and impact\n- Consistent tone and formatting across profiles\n\n### User Benefits\n\n✅ **Time Savings**: Generate professional content in seconds, not hours \n✅ **Consistency**: Maintain uniform quality across all team profiles \n✅ **AI Flexibility**: Choose the best AI provider for your use case \n✅ **Iterative Refinement**: Generate, review, and refine with AI assistance\n\n### User/Data Flow\n\n```mermaid\nflowchart TD\n Start([User provides text description]) --> Submit[Submit to AI Profile Service]\n Submit --> Select{Select AI Provider}\n Select -->|OpenAI| GPT[GPT-4 Processing]\n Select -->|Anthropic| Claude[Claude 3 Processing]\n Select -->|Gemini| Gemini[Gemini 1.5 Processing]\n \n GPT --> Process[Apply Prompt Template]\n Claude --> Process\n Gemini --> Process\n \n Process --> Generate[Generate Professional Content]\n Generate --> Review{User Reviews}\n Review -->|Approve| Save[(Save to Profile)]\n Review -->|Refine| Submit\n Save --> Render[Render in UI]\n \n style Start fill:#10b981,stroke:#059669,color:#fff\n style Save fill:#3b82f6,stroke:#2563eb,color:#fff\n style Render fill:#10b981,stroke:#059669,color:#fff\n style Review fill:#f59e0b,stroke:#d97706,color:#000\n```\n\n---\n\n## Team Hierarchy Organigram\n\n### Description\n\nVisualize your organization's structure with an interactive hierarchical chart. The organigram displays:\n\n- Multi-level organizational hierarchy (CEO → Managers → Team Members)\n- Role and title visualization\n- Interactive drill-down to team member profiles\n- Dynamic layout based on team composition\n\n### User Benefits\n\n✅ **Clear Structure**: Instantly understand reporting lines and team organization \n✅ **Onboarding Tool**: Help new team members understand the organization \n✅ **Profile Access**: One-click navigation to detailed team member profiles \n✅ **Scalability**: Supports teams from 5 to 500+ members\n\n### User/Data Flow\n\n```mermaid\nflowchart TB\n Load([Load Team Page]) --> Fetch[Fetch Team Structure]\n Fetch --> Parse[Parse Hierarchical Data]\n Parse --> Build[Build Organization Tree]\n Build --> Layout{Calculate Layout}\n Layout --> Render[Render Organigram]\n \n Render --> Interact{User Interaction}\n Interact -->|Click Member| Profile[Navigate to Profile]\n Interact -->|Hover| Tooltip[Show Quick Info]\n Interact -->|Expand| SubTeam[Show Sub-Teams]\n \n Profile --> Display[Display Full Resume]\n Tooltip --> Render\n SubTeam --> Layout\n \n style Load fill:#10b981,stroke:#059669,color:#fff\n style Render fill:#3b82f6,stroke:#2563eb,color:#fff\n style Profile fill:#8b5cf6,stroke:#7c3aed,color:#fff\n```\n\n---\n\n## Task Tracking & Evaluation\n\n### Description\n\nGrowth-mindset task management that emphasizes learning over mere completion. Features:\n\n- Task creation with detailed descriptions and acceptance criteria\n- Status tracking (Todo, In Progress, Done)\n- **Lesson-learned evaluations** upon task completion\n- Growth-oriented retrospectives and continuous improvement documentation\n\n### User Benefits\n\n✅ **Growth Culture**: Foster a learning-first environment \n✅ **Knowledge Retention**: Capture lessons and avoid repeated mistakes \n✅ **Team Learning**: Share insights across the team \n✅ **Progress Tracking**: Visualize individual and team growth\n\n### User/Data Flow\n\n```mermaid\nflowchart TD\n Create([Create Task]) --> Define[Define Task Details]\n Define --> Assign[Assign to Team Member]\n Assign --> InProgress[Start Work]\n InProgress --> Complete{Mark Complete}\n \n Complete --> Evaluate[Evaluate: Lessons Learned]\n Evaluate --> Document[Document Insights]\n Document --> Categories{Categorize Learning}\n \n Categories -->|Technical| TechLesson[(Store Technical Lesson)]\n Categories -->|Process| ProcessLesson[(Store Process Lesson)]\n Categories -->|Collaboration| CollabLesson[(Store Collaboration Lesson)]\n \n TechLesson --> Share[Share with Team]\n ProcessLesson --> Share\n CollabLesson --> Share\n \n Share --> Archive[Archive to Knowledge Base]\n Archive --> End([Task Archived])\n \n style Create fill:#10b981,stroke:#059669,color:#fff\n style Evaluate fill:#f59e0b,stroke:#d97706,color:#000\n style Share fill:#3b82f6,stroke:#2563eb,color:#fff\n style End fill:#8b5cf6,stroke:#7c3aed,color:#fff\n```\n\n---\n\n## Partial Public Visibility\n\n### Description\n\nControl what parts of your profile and accomplishments are publicly visible versus team-internal. Features:\n\n- Section-level visibility toggles (Public, Team Only, Private)\n- Selective accomplishment disclosure\n- Public portfolio generation from selected sections\n- Privacy-first design with explicit opt-in for public sharing\n\n### User Benefits\n\n✅ **Privacy Control**: Share only what you're comfortable with \n✅ **Professional Portfolio**: Public-facing resume from selected sections \n✅ **Team Transparency**: Share work-in-progress with colleagues \n✅ **Compliance**: Meet regulatory requirements for data disclosure\n\n### User/Data Flow\n\n```mermaid\nflowchart LR\n Profile[(User Profile Data)] --> Filter{Visibility Filter}\n Filter -->|Public Tag| Public[Public Portfolio]\n Filter -->|Team Tag| Internal[Team Dashboard]\n Filter -->|Private Tag| Personal[Personal View Only]\n \n Public --> WebPage[Public Web Page]\n Internal --> TeamDash[Team Dashboard]\n Personal --> UserDash[User Admin Panel]\n \n WebPage --> Visitor([External Visitors])\n TeamDash --> Colleagues([Team Members])\n UserDash --> Owner([Profile Owner])\n \n style Profile fill:#3b82f6,stroke:#2563eb,color:#fff\n style Public fill:#10b981,stroke:#059669,color:#fff\n style Internal fill:#f59e0b,stroke:#d97706,color:#000\n style Personal fill:#ef4444,stroke:#dc2626,color:#fff\n```\n\n---\n\n## Rich Personal Sections Rendering\n\n### Description\n\nComprehensive profile sections covering all aspects of professional and personal identity:\n\n- **Experiences**: Work history with detailed descriptions, technologies used, and achievements\n- **Education**: Academic background with degrees, institutions, and graduation dates\n- **Skills**: Proficiency levels, years of experience, and skill categories\n- **Hobbies**: Personal interests and activities\n- **Domains**: Areas of expertise and specialization\n- **Custom Sections**: Flexible schema for additional content\n\n### User Benefits\n\n✅ **Holistic Profiles**: Capture the complete picture of team members \n✅ **Timeline Visualization**: Experience and education timelines \n✅ **Skill Matrices**: Visual representation of team capabilities \n✅ **Multimedia Support**: Images, videos, documents in sections\n\n### User/Data Flow\n\n```mermaid\nflowchart TB\n Input([User Creates Section]) --> Type{Section Type}\n \n Type -->|Experience| ExpForm[Experience Form]\n Type -->|Education| EduForm[Education Form]\n Type -->|Skill| SkillForm[Skill Form]\n Type -->|Hobby| HobbyForm[Hobby Form]\n Type -->|Domain| DomainForm[Domain Form]\n \n ExpForm --> Validate[Validate Data]\n EduForm --> Validate\n SkillForm --> Validate\n HobbyForm --> Validate\n DomainForm --> Validate\n \n Validate --> Save[(Save to Database)]\n Save --> Render[Render in Profile]\n Render --> Timeline[Generate Timeline]\n Timeline --> Display[Display to Viewers]\n \n style Input fill:#10b981,stroke:#059669,color:#fff\n style Validate fill:#f59e0b,stroke:#d97706,color:#000\n style Save fill:#3b82f6,stroke:#2563eb,color:#fff\n style Display fill:#8b5cf6,stroke:#7c3aed,color:#fff\n```\n\n---\n\n## Technology & Skills Display\n\n### Description\n\nTechnology stack and skills visualization with:\n\n- Technology badges organized by category (Frontend, Backend, Database, Cloud, etc.)\n- Proficiency indicators (Beginner, Intermediate, Advanced, Expert)\n- Years of experience tracking\n- Skill matrices showing team capabilities\n- Technology detail modals with descriptions and links\n\n### User Benefits\n\n✅ **Team Capabilities**: Understand collective technical expertise \n✅ **Project Staffing**: Match skills to project requirements \n✅ **Skill Development**: Identify learning opportunities and gaps \n✅ **Client Communication**: Showcase team expertise to stakeholders\n\n### User/Data Flow\n\n```mermaid\nflowchart TD\n AddSkill([Add Technology/Skill]) --> Details[Enter Details]\n Details --> Category{Select Category}\n \n Category -->|Frontend| FE[React, Next.js, etc.]\n Category -->|Backend| BE[Node.js, Python, etc.]\n Category -->|Database| DB[PostgreSQL, MongoDB, etc.]\n Category -->|Cloud| Cloud[AWS, Azure, GCP]\n \n FE --> Proficiency[Set Proficiency Level]\n BE --> Proficiency\n DB --> Proficiency\n Cloud --> Proficiency\n \n Proficiency --> Years[Years of Experience]\n Years --> Badge[Generate Badge]\n Badge --> Matrix[Add to Skill Matrix]\n Matrix --> TeamView[Display in Team View]\n \n style AddSkill fill:#10b981,stroke:#059669,color:#fff\n style Badge fill:#3b82f6,stroke:#2563eb,color:#fff\n style TeamView fill:#8b5cf6,stroke:#7c3aed,color:#fff\n```" + }, + { + "title": "Architecture & Technology", + "type": "ARCHITECTURE", + "navOrder": 3, + "content": "## Architecture Overview\n\n### Technology Stack\n\n**Frontend**\n- **Next.js 15+**: App Router with Server Components, partial prerendering, and advanced caching\n- **React 19**: Latest React features with Suspense and concurrent rendering\n- **TypeScript 5.x**: Strict mode for compile-time type safety\n- **Tailwind CSS**: Utility-first styling framework\n- **Shadcn UI**: Component library built on Radix UI primitives\n\n**Backend**\n- **Next.js API Routes**: Server-side API endpoints with edge runtime support\n- **Prisma ORM**: Type-safe database access with schema-first design\n- **PostgreSQL**: Production-grade relational database\n- **AI Services**:\n - OpenAI GPT-4 for general content generation\n - Anthropic Claude 3 for long-form content\n - Google Gemini 1.5 for multi-modal processing\n\n**Testing & Quality**\n- **Jest**: Unit and integration testing\n- **Playwright**: End-to-end browser testing\n- **TypeScript**: Compile-time type checking\n- **ESLint**: Code quality enforcement\n\n### System Architecture\n\n```mermaid\ngraph TB\n subgraph Client[\"🖥️ Client Layer\"]\n Browser[\"Next.js App Router
Server Components
Client Components
Tailwind + Shadcn UI\"]\n end\n \n subgraph Server[\"⚙️ Server Layer\"]\n API[\"Next.js API Routes
Edge Runtime
Middleware\"]\n Services[\"Business Services
Profile, Team, Task
AI Orchestration\"]\n end\n \n subgraph Data[\"💾 Data Layer\"]\n Prisma[\"Prisma ORM
Type-Safe Queries
Migrations\"]\n DB[(\"PostgreSQL
Relational Data
Transactions\")]\n end\n \n subgraph External[\"🌐 External Services\"]\n OpenAI[\"OpenAI
GPT-4\"]\n Anthropic[\"Anthropic
Claude 3\"]\n Gemini[\"Google
Gemini 1.5\"]\n end\n \n Browser -->|\"SSR/RSC\"| API\n API --> Services\n Services --> Prisma\n Prisma --> DB\n Services -.->|\"AI Requests\"| OpenAI\n Services -.->|\"AI Requests\"| Anthropic\n Services -.->|\"AI Requests\"| Gemini\n \n style Client fill:#1e293b,stroke:#3b82f6,stroke-width:3px,color:#fff\n style Server fill:#1e293b,stroke:#10b981,stroke-width:3px,color:#fff\n style Data fill:#1e293b,stroke:#8b5cf6,stroke-width:3px,color:#fff\n style External fill:#1e293b,stroke:#f59e0b,stroke-width:2px,color:#fff\n```\n\n### Core Entities\n\n```mermaid\nerDiagram\n USER ||--o{ COLLABORATOR : \"has profile\"\n USER {\n uuid id PK\n string username UK\n string password\n string email\n datetime createdAt\n }\n \n COLLABORATOR ||--|{ RESUME_SECTION : \"contains\"\n COLLABORATOR ||--o{ TASK : \"assigned\"\n COLLABORATOR {\n uuid id PK\n uuid userId FK\n string firstName\n string lastName\n string title\n string bio\n string avatarUrl\n boolean isPublic\n }\n \n RESUME_SECTION ||--o{ EXPERIENCE : \"includes\"\n RESUME_SECTION ||--o{ EDUCATION : \"includes\"\n RESUME_SECTION ||--o{ SKILL : \"includes\"\n RESUME_SECTION {\n uuid id PK\n uuid collaboratorId FK\n string sectionType\n boolean isPublic\n json data\n }\n \n EXPERIENCE {\n uuid id PK\n string company\n string role\n date startDate\n date endDate\n text description\n json technologies\n }\n \n EDUCATION {\n uuid id PK\n string institution\n string degree\n string field\n date graduationDate\n }\n \n SKILL ||--|| SKILL_CATEGORY : \"belongs to\"\n SKILL {\n uuid id PK\n string name\n int proficiency\n int yearsOfExperience\n }\n \n SKILL_CATEGORY {\n uuid id PK\n string name UK\n string description\n }\n \n TASK {\n uuid id PK\n uuid collaboratorId FK\n string title\n text description\n string status\n text lessonLearned\n datetime completedAt\n }\n \n PROMPT_TEMPLATE {\n uuid id PK\n string name UK\n string purpose\n text template\n json parameters\n }\n```\n\n---\n\n## Reliability, Solidity, Scalability\n\n### Reliability\n\n**Type Safety**\n- End-to-end TypeScript with strict mode enabled\n- Prisma-generated types ensure database schema matches code\n- Zod schema validation for runtime type checking\n- No `any` types in production code\n\n**Error Handling**\n- Comprehensive try-catch blocks in all async operations\n- Prisma transaction rollback on failures\n- User-friendly error messages with detailed logging\n- Graceful degradation when AI services are unavailable\n\n**Data Integrity**\n- Prisma transactions for multi-step database operations\n- Foreign key constraints enforce referential integrity\n- Soft deletes for critical data (users, profiles)\n- Automated database backups\n\n### Solidity\n\n**Clean Architecture**\n- **Presentation Layer**: Next.js components (Server + Client)\n- **Business Logic Layer**: Service classes isolated from framework\n- **Data Access Layer**: Prisma repositories with abstraction\n- **Shared Layer**: DTOs, types, and utilities\n\n**Design Principles**\n- **Single Responsibility**: Each service handles one domain\n- **Dependency Inversion**: Services depend on interfaces, not implementations\n- **DRY (Don't Repeat Yourself)**: Shared utilities and components\n- **KISS (Keep It Simple)**: Avoid over-engineering\n\n**Testing Strategy**\n- Unit tests for business logic (Jest)\n- Integration tests for API routes\n- E2E tests for critical user flows (Playwright)\n- Continuous testing in CI/CD pipeline\n\n### Scalability\n\n**Next.js 15 Features**\n- **Server Components**: Reduced client-side JavaScript bundle\n- **Partial Prerendering (PPR)**: Static shell with dynamic content\n- **Advanced Caching**: Aggressive caching with revalidation strategies\n- **Edge Runtime**: Deploy API routes to edge for low latency\n\n**Database Optimization**\n- Indexed foreign keys for fast joins\n- Query optimization with Prisma `include` and `select`\n- Connection pooling for concurrent requests\n- Read replicas for scaling read-heavy workloads\n\n**Performance**\n- Image optimization with Next.js `Image` component\n- Code splitting and lazy loading\n- Suspense boundaries for progressive rendering\n- CDN integration for static assets" + }, + { + "title": "Development & Quality Assurance", + "type": "TECHNICAL", + "navOrder": 4, + "content": "## Development Lifecycle & Quality Assurance\n\n### Development Cycle\n\nOur development follows an iterative, quality-first approach:\n\n```mermaid\nflowchart LR\n Plan([Feature Planning]) --> Design[System Design]\n Design --> Implement[Implementation]\n Implement --> Test[Testing]\n Test --> Review{Code Review}\n Review -->|Changes Needed| Implement\n Review -->|Approved| Deploy[Deployment]\n Deploy --> Monitor[Monitoring]\n Monitor --> Feedback[Gather Feedback]\n Feedback --> Plan\n \n style Plan fill:#10b981,stroke:#059669,color:#fff\n style Test fill:#f59e0b,stroke:#d97706,color:#000\n style Deploy fill:#3b82f6,stroke:#2563eb,color:#fff\n style Monitor fill:#8b5cf6,stroke:#7c3aed,color:#fff\n```\n\n**Phase 1: Feature Planning**\n- Collaborate with stakeholders to define requirements\n- Create user stories and acceptance criteria\n- Estimate effort and prioritize in backlog\n- Write technical design document if needed\n\n**Phase 2: System Design**\n- Design database schema changes (Prisma migrations)\n- Plan API contracts and DTOs\n- Sketch UI wireframes and component hierarchy\n- Identify dependencies and integration points\n\n**Phase 3: Implementation**\n- Create feature branch from `develop`\n- Write code following style guidelines (ESLint)\n- Implement business logic in services\n- Build UI components with Tailwind + Shadcn\n- Add inline documentation and JSDoc comments\n\n**Phase 4: Testing**\n- Write unit tests for business logic (Jest)\n- Create integration tests for API routes\n- Add E2E tests for user flows (Playwright)\n- Manual testing in development environment\n- Accessibility testing (keyboard navigation, screen readers)\n\n**Phase 5: Code Review**\n- Open pull request with detailed description\n- Automated CI checks (build, tests, linting)\n- Peer review for code quality and design\n- Address feedback and iterate\n\n**Phase 6: Deployment**\n- Merge to `develop` branch\n- Deploy to staging environment\n- Run smoke tests and regression tests\n- Merge to `main` and deploy to production\n\n**Phase 7: Monitoring**\n- Monitor error rates and performance metrics\n- Collect user feedback and usage analytics\n- Identify bugs and improvement opportunities\n- Plan next iteration\n\n---\n\n## Testing Strategy\n\n### Unit Testing (Jest)\n\n**Business Logic**\n```typescript\n// Example: Profile Service unit test\ndescribe('ProfileService', () => {\n it('should generate profile from AI description', async () => {\n const service = new ProfileService(mockPrisma, mockAIService);\n const result = await service.generateFromDescription(\n 'Experienced full-stack developer with 5 years in React'\n );\n expect(result).toHaveProperty('title');\n expect(result).toHaveProperty('bio');\n });\n});\n```\n\n**Model Validation**\n```typescript\n// Example: Zod schema validation test\ndescribe('CollaboratorSchema', () => {\n it('should validate valid collaborator data', () => {\n const data = { firstName: 'John', lastName: 'Doe', ... };\n expect(() => CollaboratorSchema.parse(data)).not.toThrow();\n });\n});\n```\n\n### Integration Testing\n\n**API Routes**\n```typescript\n// Example: API route integration test\ndescribe('POST /api/profile', () => {\n it('should create new collaborator profile', async () => {\n const response = await fetch('/api/profile', {\n method: 'POST',\n body: JSON.stringify({ firstName: 'Jane', ... })\n });\n expect(response.status).toBe(201);\n });\n});\n```\n\n**Database Interactions**\n- Prisma client tests with in-memory SQLite\n- Transaction rollback tests\n- Constraint validation tests\n\n### End-to-End Testing (Playwright)\n\n**Critical User Flows**\n```typescript\n// Example: E2E test for profile creation\ntest('User can create profile via AI generation', async ({ page }) => {\n await page.goto('/profile/new');\n await page.fill('[name=\"description\"]', 'Senior engineer...');\n await page.click('button:has-text(\"Generate with AI\")');\n await expect(page.locator('.profile-preview')).toBeVisible();\n});\n```\n\n**Test Coverage**\n- Profile creation and editing\n- Team organigram navigation\n- Task creation with lesson-learned evaluation\n- AI content generation workflows\n- Public/private visibility toggling\n\n### CI/CD Pipeline\n\n**GitHub Actions Workflow**\n\n```yaml\nname: CI/CD Pipeline\non:\n push:\n branches: [main, develop]\n pull_request:\n branches: [main, develop]\n\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: '20'\n - run: npm ci\n - run: npm run build\n - run: npm test\n - run: npm run test:e2e\n```\n\n**Build Steps**:\n1. ✅ Checkout code\n2. ✅ Install dependencies\n3. ✅ Run TypeScript compiler\n4. ✅ Run ESLint\n5. ✅ Execute unit tests (Jest)\n6. ✅ Execute E2E tests (Playwright)\n7. ✅ Build production bundle\n\n**Quality Gates**:\n- All tests must pass (100% pass rate required)\n- No TypeScript errors\n- No ESLint errors (warnings allowed)\n- Build must complete successfully\n\n---\n\n## Best Practices & Philosophy\n\n### Growth Mindset Integration\n\nThe platform is built on **Carol Dweck's Growth Mindset** principles:\n\n> **Fixed Mindset**: \"I can't do this.\" \n> **Growth Mindset**: \"I can't do this *yet*. What can I learn?\"\n\n**Task Evaluation System**\n- Every completed task prompts: \"What did you learn?\"\n- Lessons are categorized: Technical, Process, Collaboration\n- Team knowledge base grows with each task\n- Celebrate effort and learning, not just outcomes\n\n**Human-Centric Design**\n- AI assists but never replaces human decision-making\n- Transparent AI usage (always show when AI is involved)\n- User control over AI suggestions (approve/reject/refine)\n- Inclusive design with accessibility as a priority\n\n### Development Philosophy\n\n**Reliability First**\n- Type safety prevents entire classes of bugs\n- Comprehensive error handling ensures graceful failures\n- Database transactions maintain data integrity\n\n**Solidity Through Architecture**\n- Clean separation of concerns enables maintainability\n- Service layer isolation allows independent testing\n- Shared contracts ensure consistency across layers\n\n**Scalability by Design**\n- Next.js 15 features optimize for performance from day one\n- Database indexing and query optimization built-in\n- Caching strategies reduce server load\n\n**Continuous Improvement**\n- Regular retrospectives to refine processes\n- Automated testing catches regressions early\n- Monitoring and feedback loops drive iteration\n\n---\n\n## Getting Started\n\n### Prerequisites\n- Node.js >= 20.0.0\n- PostgreSQL >= 14\n- npm >= 10.0.0\n\n### Installation\n\n```bash\n# Clone repository\ngit clone https://github.com/M2H-Machine-to-Human-Race/Ark.Alliance.StartupCms.AI.git\ncd Ark.Alliance.StartupCms.AI\n\n# Install dependencies\nnpm install\n\n# Set up environment variables\ncp .env.example .env\n# Edit .env with your database URL and AI API keys\n\n# Run database migrations\nnpx prisma migrate dev\n\n# Seed database (optional)\nnpx prisma db seed\n\n# Start development server\nnpm run dev\n```\n\n### Environment Variables\n\n```env\nDATABASE_URL=\"postgresql://user:password@localhost:5432/startupcms\"\nNEXTAUTH_SECRET=\"your-secret-key\"\nNEXTAUTH_URL=\"http://localhost:3000\"\n\n# AI Provider API Keys\nOPENAI_API_KEY=\"sk-...\"\nANTHROPIC_API_KEY=\"sk-ant-...\"\nGOOGLE_AI_API_KEY=\"...\"\n```\n\n### Running Tests\n\n```bash\n# Unit and integration tests\nnpm test\n\n# E2E tests\nnpm run test:e2e\n\n# Coverage report\nnpm run test:coverage\n```\n\n---\n\n**Built with ❤️ by the Ark Alliance Team** \n**© 2026 M2H.IO - Machine-to-Human Excellence**" + } + ] +} \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/ark-alliance.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/ark-alliance.json new file mode 100644 index 0000000000000000000000000000000000000000..212b919254b4c980c559cb98fa637ccbbfc4f9b9 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/ark-alliance.json @@ -0,0 +1,74 @@ +{ + "title": "Ark.Alliance", + "description": "A philosophical movement and technological platform for authentic human recognition. History celebrates winners, yet every success rests on an invisible mountain of failures and forgotten contributions. Ark.Alliance flips the paradigm: value lies in the authenticity of effort, not the glory of outcome. If AI uses OUR collective knowledge to generate BILLIONS in value, we deserve our fair share. This is the moment humanity sends its invoice to AI.", + "status": "Featured", + "imageUrl": "/Assets/Projects/Ark.Alliance/Ark_Alliance_Hero.png", + "repoUrl": "", + "demoUrl": "", + "packageUrl": "", + "technologies": [ + "blockchain", + "typescript", + "react", + "nodejs", + "ai" + ], + "features": [ + { + "title": "Universal Recognition", + "description": "Every human is recognized as co-owner of the collective cognitive heritage. From childhood to life's twilight, every authentic action contributes to our collective future. Your effort counts.", + "icon": "globe" + }, + { + "title": "Authenticity Blockchain", + "description": "Track contributions via a decentralized authenticity blockchain. Every documented attempt, every sincere effort, is permanently recorded and valued. Authenticity pays better than cheating.", + "icon": "link" + }, + { + "title": "Smart Contract Redistribution", + "description": "Automatically redistribute AI-generated benefits through smart contracts. If Facebook can track every click to sell ads, we can track every contribution to distribute value. Fair retribution for all.", + "icon": "code" + }, + { + "title": "ARK Token Ecosystem", + "description": "Every documented attempt or effort earns ARK tokens. Share an idea, submit an effort, receive your fair share automatically. The Spotify of contribution meets the Instagram of authenticity.", + "icon": "coins" + }, + { + "title": "DAO Governance", + "description": "Decentralized, transparent governance ensures fair reward based on real contribution. Full DAO governance by 2029, with community-driven evolution for a freer, fairer world.", + "icon": "users" + }, + { + "title": "AI in Service of Humanity", + "description": "AI is the ultimate tool for authentic recognition, not our master. It honors every person at every stage of life and preserves challenge and the boundless space of imagination.", + "icon": "brain" + } + ], + "pages": [ + { + "title": "Overview", + "type": "OVERVIEW", + "navOrder": 1, + "content": "## Ark.Alliance: A New Era for Human Recognition\n\n> *\"Honoring the journey, not just the destination. Your failures today are tomorrow's algorithms.\"*\n\n---\n\n### The Problem We're Solving\n\nHistory has always celebrated winners, etching their names into the marble of collective memory. Yet every success rests on an invisible mountain of failures, courageous attempts, and forgotten contributions. These efforts — often dismissed or ignored — are the essential seeds of all human progress.\n\n**Three undeniable truths:**\n\n| Truth | Reality |\n|-------|--------|\n| AI uses OUR collective knowledge | Fact |\n| It generates BILLIONS in value | Fact |\n| We get NOTHING today | Problem |\n\n---\n\n### The Ark.Alliance Solution\n\nArk.Alliance is born from this fundamental truth: **value lies in the authenticity of effort, not the glory of outcome.**\n\nArtificial intelligence gives us a historic opportunity to flip this paradigm. Just as AI learns more from its errors than its successes, we can create a world where:\n\n- ✨ **Authentic failure** is worth more than artificial success\n- 🎯 **Sincere effort** outweighs hollow victory\n- 🌍 **Every human contribution** is recognized and valued\n- 💰 **Shared cognitive heritage** generates fair retribution for all\n- 🔓 **Authenticity** becomes the most rewarding path\n\n> 💡 **\"This is the moment humanity sends its invoice to AI.\"**\n\n---\n\n### What Makes It Different\n\nArk.Alliance is not just technology — it's a philosophical movement that puts humans at the center of progress.\n\nJust as Wikipedia democratized knowledge, Ark.Alliance democratizes recognition:\n\n| Platform | Analogy |\n|----------|--------|\n| 📚 **The Wikipedia of effort** | Every contribution, no matter how modest, enriches a universal, authentic registry |\n| 🎵 **The Spotify of contribution** | Share an idea or effort = automatically receive your fair share |\n| 📸 **The Instagram of authenticity** | No more empty likes: every genuine interaction creates real value |\n\n> 🎭 *\"Imagine a world where your imposter syndrome becomes your retirement fund.\"*\n\n---\n\n### The Promise\n\nImagine a world where:\n\n- ✨ **Every effort counts** — from childhood to life's twilight, every authentic action contributes to our collective future\n- 🌍 **Authenticity is rewarded** — being yourself becomes the most fulfilling path\n- ⚖️ **Justice prevails** — a decentralized, transparent system ensures fair reward based on real contribution\n- 🔓 **Freedom flourishes** — freed from material constraints and prejudice, people explore the infinite potential of their imagination" + }, + { + "title": "Manifesto", + "type": "FUNCTIONAL", + "navOrder": 2, + "content": "## The Ark.Alliance Manifesto\n\n---\n\n### Our Vision: Honoring the Journey\n\nAI teaches us a profound lesson: **it advances more through failed iterations than immediate successes.** Every negative prompt, every error, enriches its understanding. This technological reality invites us to transform how we view humanity.\n\nWe believe:\n\n- Every human deserves recognition for their authentic effort\n- Failure is not the opposite of success — it's part of the path\n- Our collective knowledge belongs to all of us\n- AI-generated value should benefit its true creators: humanity\n\n---\n\n### How Ark.Alliance Changes the Game\n\nThe solution unfolds in **3 acts**:\n\n```mermaid\nflowchart LR\n subgraph Act1[\"🌍 Act 1: Recognition\"]\n A1[\"Recognize every human as\\nco-owner of collective\\ncognitive heritage\"]\n end\n \n subgraph Act2[\"🔗 Act 2: Tracking\"]\n A2[\"Track contributions via\\nauthenticity blockchain\"]\n end\n \n subgraph Act3[\"💰 Act 3: Redistribution\"]\n A3[\"Automatically redistribute\\nbenefits through\\nsmart contracts\"]\n end\n \n Act1 --> Act2 --> Act3\n \n style Act1 fill:#1e3a5f,stroke:#3b82f6,color:#fff\n style Act2 fill:#1e3a5f,stroke:#10b981,color:#fff\n style Act3 fill:#1e3a5f,stroke:#f59e0b,color:#fff\n```\n\n> 💡 *\"If Facebook can track every click to sell ads, we can track every contribution to distribute value.\"*\n\n---\n\n### Our Philosophy: AI in Service of Humanity\n\nAI will never be our \"God\" or master. It is the ultimate tool for authentic recognition that:\n\n| Principle | Commitment |\n|-----------|------------|\n| 🙏 **Honor** | Honors every person at every stage of life |\n| 🌈 **Foundation** | Lays the foundations for a freer, fairer, more human world |\n| 🎯 **Preservation** | Preserves challenge and the boundless space of imagination |\n| 🔓 **Expression** | Allows each of us to express our deepest nature without submission |\n\n---\n\n### The Universal Heritage Principle\n\nWe are all inheritors and contributors to human knowledge. Every:\n\n- 💭 **Thought shared** enriches the collective\n- 📝 **Attempt documented** becomes training data\n- 🎨 **Creation made** inspires future works\n- ❌ **Failure learned from** prevents others' missteps\n\nThis heritage generates **billions in AI value today**. Ark.Alliance ensures this value returns to its rightful creators: **all of us**." + }, + { + "title": "Applications & FAQ", + "type": "TECHNICAL", + "navOrder": 3, + "content": "## Applications & How It Works\n\n---\n\n### Who Is Ark.Alliance For?\n\n| Audience | What It Means |\n|----------|---------------|\n| 🖥️ **For geeks** | It's Git, but for all humanity |\n| 🎨 **For artists** | Your drafts finally have value |\n| 🤔 **For philosophers** | The free market of authenticity |\n| 🧐 **For skeptics** | Worst case, it doesn't work. Best case, we revolutionize the world |\n| 👤 **For everyone** | You exist = you contribute = you deserve your share |\n\n---\n\n### The \"First 1000\" Pilot Program\n\nThe first 1,000 contributors become **founding members** of the movement:\n\n```mermaid\nflowchart TB\n subgraph Pilot[\"🚀 First 1000 Founders\"]\n P1[\"📄 Document your efforts\"] --> P2[\"🪙 Earn 1 ARK per contribution\"]\n P2 --> P3[\"⏰ First redistribution at 6 months\"]\n P3 --> P4[\"👑 Become founding member\"]\n end\n \n style Pilot fill:#1a1a2e,stroke:#8b5cf6,color:#fff\n```\n\n**Come as you are, with your failures and dreams. That's exactly what we need.**\n\n---\n\n## Common Objections (and Our Responses)\n\nWe know the idea sounds bold. Here are the most frequent objections… and why they make us smile:\n\n### ❓ \"But who will pay?\"\n\nThe same entities already paying: companies that monetize AI. Except this time, part of the profits is redistributed fairly to the humans whose data trained their systems.\n\n---\n\n### ❓ \"This is communism!\"\n\nNo — it's **fair cognitive capitalism**. Everyone remains owner of their own data and efforts. We're not redistributing existing wealth; we're claiming our fair share of *newly generated* AI value.\n\n---\n\n### ❓ \"It's impossible to measure!\"\n\nWe already track your TikTok screen time to the millisecond. We know exactly which videos you watch, skip, and rewatch. **Measuring authentic contribution? Entirely within reach.**\n\n---\n\n### ❓ \"People will cheat!\"\n\nAs if no one cheats today? At Ark.Alliance:\n\n- 🎯 **Authenticity pays better** than cheating\n- 🔗 **Decentralized mechanisms** limit abuse\n- 👥 **Community validation** filters bad actors\n- 📊 **Effort history** builds reputation over time\n\n---\n\n## Join the \"Finally!\" Revolution\n\n> *\"We don't promise to make you rich. We promise to give you justice. And honestly, that's already a lot.\"*\n\n**Ark.Alliance — Because your failures today are tomorrow's algorithms.**" + }, + { + "title": "Roadmap", + "type": "ROADMAP", + "navOrder": 4, + "content": "## Ark.Alliance Roadmap\n\nArk.Alliance advances through concrete, transparent, community-driven stages.\n\n---\n\n```mermaid\ntimeline\n title Ark.Alliance Development Timeline\n \n section Phase 0 - Pilot Launch (Jan-Jun 2026)\n January 2026 : Recruitment of \"First 1000\" founders\n March 2026 : Documentation and ARK token allocation\n June 2026 : Symbolic first redistribution\n \n section Phase 1 - Technical Foundations (2026-2027)\n Q3 2026 : Blockchain protocol development\n Q4 2026 : Decentralized contribution registry\n 2027 : Public beta launch\n \n section Phase 2 - Recognition & Retribution (2027-2028)\n 2027 : Automatic redistributions activated\n 2028 : Mobile app and universal wallet\n 2028 : Large-scale community validation\n \n section Phase 3 - Global Adoption (2028-2030)\n 2028 : AI platform interoperability\n 2029 : Full DAO governance\n 2030 : Education, art, research extension\n \n section Phase 4 - Mature Ecosystem (2030+)\n 2030+ : Universal retribution standard\n Future : Public policy integration\n Ongoing : Community-driven evolution\n```\n\n---\n\n### Phase Details\n\n#### 🚀 Phase 0: Pilot Launch (January–June 2026)\n\n| Milestone | Description |\n|-----------|-------------|\n| **Founder Recruitment** | Recruit the \"First 1000\" founding members |\n| **Token Allocation** | Document and allocate initial ARK tokens for authentic efforts |\n| **First Redistribution** | Symbolic first value redistribution at 6 months |\n| **Community Building** | Collect feedback and build core community |\n\n---\n\n#### 🔧 Phase 1: Technical Foundations (2026–2027)\n\n| Milestone | Description |\n|-----------|-------------|\n| **Blockchain Protocol** | Develop the core blockchain and decentralized contribution registry |\n| **Smart Contracts** | Integrate authentic tracking mechanisms and smart contracts |\n| **Partnerships** | Partner with open-source communities and AI platforms |\n| **Public Beta** | Launch public beta for wider testing |\n\n---\n\n#### 💰 Phase 2: Recognition & Retribution (2027–2028)\n\n| Milestone | Description |\n|-----------|-------------|\n| **Auto-Redistribution** | Activate automatic value redistributions |\n| **Community Validation** | Large-scale program with community-based validation |\n| **Mobile & Wallet** | Launch mobile app and universal wallet |\n\n---\n\n#### 🌍 Phase 3: Global Adoption (2028–2030)\n\n| Milestone | Description |\n|-----------|-------------|\n| **AI Interoperability** | Massive opening and interoperability with major AI platforms |\n| **DAO Governance** | Full decentralized autonomous organization governance |\n| **Sector Extension** | Extension to education, art, and research domains |\n\n---\n\n#### 🏛️ Phase 4: Mature Ecosystem (2030+)\n\n| Milestone | Description |\n|-----------|-------------|\n| **Universal Standard** | Universal retribution as global standard |\n| **Public Policy** | Integration into public policy and responsible companies |\n| **Continuous Evolution** | Ongoing evolution guided by the community |\n\n---\n\n> ⚠️ *This roadmap is iterative and will adapt based on feedback from the first contributors.*" + } + ] +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/ark-portfolio.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/ark-portfolio.json new file mode 100644 index 0000000000000000000000000000000000000000..853f4d0198f2d41013101ee88d7fe87d313064d1 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/ark-portfolio.json @@ -0,0 +1,82 @@ +{ + "title": "Ark.Alliance.Portfolio", + "description": "Production-grade portfolio CMS and static site generator with AI-powered content creation. Features 20 TypeORM entities, multi-provider AI integration (OpenAI, Anthropic, Gemini), one-click static export, MVVM architecture, 235+ passing tests, and comprehensive admin dashboard. Built with React 18, TypeScript 5, Express, and TypeORM with end-to-end type safety.", + "status": "Featured", + "imageUrl": "/Assets/Projects/Ark.Alliance.StartupCms.Ai/portfolio-hero.png", + "repoUrl": "https://github.com/M2H-Machine-to-Human-Race/Ark.Alliance.Portfolio", + "demoUrl": "", + "packageUrl": "", + "technologies": [ + "react", + "typescript", + "nodejs", + "express", + "typeorm", + "sqlite", + "vite", + "jest", + "rest", + "openai", + "anthropic", + "gemini" + ], + "features": [ + { + "title": "20-Entity Content Management System", + "description": "Comprehensive CMS with TypeORM entities for Profile, Projects (with multi-page documentation), Experiences, Education, Skills, Media Assets, Carousel, Widgets, and more. Full CRUD operations through an intuitive admin dashboard.", + "icon": "database" + }, + { + "title": "Multi-Provider AI Integration", + "description": "Native integration with OpenAI GPT-4, Anthropic Claude 3, and Google Gemini. AES-256 encrypted API key storage, intelligent content generation, summarization, and professional writing enhancement for all CMS entities.", + "icon": "brain" + }, + { + "title": "One-Click Static Site Export", + "description": "Transform your portfolio CMS into a deployable static website with a single click. Generates optimized HTML/CSS/JS bundles ready for GitHub Pages, Netlify, Vercel, or any CDN with zero server costs.", + "icon": "download" + }, + { + "title": "Clean Three-Tier Architecture", + "description": "Enterprise-grade separation with Presentation (React MVVM), Shared Contracts (TypeScript DTOs), and Application (Express + TypeORM) layers. Repository pattern, type-safe APIs, and comprehensive error handling throughout.", + "icon": "sitemap" + }, + { + "title": "Production-Grade Security", + "description": "JWT authentication with bcrypt password hashing (12 rounds), Helmet security headers, CORS protection, rate limiting, and AES-256 encryption for sensitive credentials. Built for deployment from day one.", + "icon": "shield-alt" + }, + { + "title": "Dynamic Theme System", + "description": "Runtime theme switching with no page reload. Built-in Architectural and Aloe Vera themes, plus JSON-based custom theme creation. CSS variable injection enables instant visual transformations.", + "icon": "palette" + } + ], + "pages": [ + { + "title": "Overview", + "type": "OVERVIEW", + "navOrder": 1, + "content": "## Ark.Alliance.Portfolio\n\n**Production-Grade Portfolio CMS & Static Site Generator**\n\nA comprehensive portfolio platform that goes beyond simple showcases. It combines enterprise-grade software practices with AI-powered content generation, full content management capabilities, and one-click static site deployment.\n\n### Perfect For\n\n- 💼 **Professionals** — Showcase work experience and projects with dynamic CMS\n- 🎓 **Academics** — Document research, publications, and teaching\n- 🎨 **Creatives** — Present portfolio work with rich media management\n- 💻 **Engineers** — Demonstrate technical projects with multi-page documentation\n- 🏢 **Agencies** — Manage team portfolios with collaborative features\n\n### Core Capabilities\n\n| Capability | Description |\n|------------|-------------|\n| **Content Management** | 20 TypeORM entities covering all portfolio aspects |\n| **AI Content Creation** | Multi-provider AI (OpenAI, Anthropic, Gemini) |\n| **Static Site Export** | One-click deployment to any static host |\n| **Multi-Page Projects** | Rich documentation with Markdown + Mermaid |\n| **Dynamic Theming** | Runtime theme switching, no reload required |\n| **Media Management** | Upload, organize, and optimize assets |\n| **Type Safety** | End-to-end TypeScript with shared DTOs |\n| **Security** | JWT auth, bcrypt, AES-256, Helmet headers |\n\n### Technology Stack\n\n**Frontend**\n- React 18 with TypeScript 5.x\n- Vite build tool\n- MVVM component architecture\n- React Router for navigation\n- Axios for API communication\n\n**Backend**\n- Node.js 18+ with Express\n- TypeORM for data access\n- SQLite (dev) / PostgreSQL (prod)\n- 16 business services\n- 9 API controllers\n\n**Shared**\n- TypeScript DTOs\n- Shared enumerations\n- Type-safe contracts\n\n**AI Integration**\n- OpenAI GPT-4 & GPT-4 Turbo\n- Anthropic Claude 3 (Opus, Sonnet, Haiku)\n- Google Gemini 1.5 (Pro, Flash)\n\n### Key Statistics\n\n- **20** TypeORM Entities\n- **16** Backend Services \n- **9** API Controllers\n- **235+** Test Cases (100% pass rate)\n- **3** Architecture Layers\n- **3** AI Providers\n\n### Quick Start\n\n```bash\n# Install shared library\ncd Ark.Alliance.StartupCms.Ai.Share\nnpm install && npm run build\n\n# Start backend (port 3085)\ncd ../Ark.Alliance.StartupCms.Ai.Backend\nnpm install && npm run dev\n\n# Start frontend (port 3080)\ncd ../Ark.Alliance.StartupCms.Ai.UI\nnpm install && npm run dev\n```\n\n**Default admin credentials**: `admin` / `admin123`" + }, + { + "title": "Architecture & System Design", + "type": "ARCHITECTURE", + "navOrder": 2, + "content": "## System Architecture\n\n### Three-Tier Architecture\n\n```mermaid\ngraph TB\n subgraph Client[\"🖥️ Presentation Layer\"]\n UI[\"Ark.Alliance.StartupCms.Ai.UI
React 18 + TypeScript
MVVM Architecture
Vite Build Tool\"]\n end\n \n subgraph Contracts[\"📦 Shared Contracts\"]\n Share[\"Ark.Alliance.StartupCms.Ai.Share
DTOs + Enums
Type Definitions
End-to-End Type Safety\"]\n end\n \n subgraph Server[\"⚙️ Application Layer\"]\n Backend[\"Ark.Alliance.StartupCms.Ai.Backend
Express REST API
16 Services + 9 Controllers
TypeORM Data Access\"]\n end\n \n subgraph Data[\"💾 Data Layer\"]\n DB[(\"SQLite (Dev)
PostgreSQL (Prod)
20 Entities\")]\n end\n \n subgraph External[\"🌐 External Services\"]\n AI1[\"OpenAI
GPT-4\"]\n AI2[\"Anthropic
Claude 3\"]\n AI3[\"Google
Gemini 1.5\"]\n end\n \n UI -->|\"REST API
(Axios)\"| Backend\n UI -.->|\"Import Types\"| Share\n Backend -.->|\"Import Types\"| Share\n Backend -->|\"TypeORM ORM\"| DB\n Backend -->|\"Encrypted API\"| AI1\n Backend -->|\"Encrypted API\"| AI2\n Backend -->|\"Encrypted API\"| AI3\n \n style Client fill:#1e293b,stroke:#3b82f6,stroke-width:3px,color:#fff\n style Contracts fill:#1e293b,stroke:#10b981,stroke-width:3px,color:#fff\n style Server fill:#1e293b,stroke:#f59e0b,stroke-width:3px,color:#fff\n style Data fill:#1e293b,stroke:#8b5cf6,stroke-width:2px,color:#fff\n style External fill:#1e293b,stroke:#ef4444,stroke-width:2px,color:#fff\n```\n\n### Entity Relationship Model\n\n```mermaid\nerDiagram\n PROFILE ||--o{ USER : \"manages\"\n PROFILE {\n uuid id PK\n string name\n string title\n string summary\n string email\n string phone\n string location\n }\n \n USER ||--o{ PROJECT : \"creates\"\n USER {\n uuid id PK\n string username UK\n string password\n string role\n }\n \n PROJECT ||--|{ PROJECT_PAGE : \"contains\"\n PROJECT ||--|{ PROJECT_FEATURE : \"has\"\n PROJECT ||--|{ PROJECT_TECHNOLOGY : \"uses\"\n PROJECT {\n uuid id PK\n string title\n string description\n string status\n string imageUrl\n string repoUrl\n string demoUrl\n }\n \n PROJECT_PAGE {\n uuid id PK\n string title\n string type\n text content\n int navOrder\n }\n \n PROJECT_FEATURE {\n uuid id PK\n string title\n text description\n string icon\n }\n \n TECHNOLOGY ||--|{ PROJECT_TECHNOLOGY : \"links\"\n TECHNOLOGY {\n uuid id PK\n string name UK\n string category\n string icon\n }\n \n EXPERIENCE ||--|{ SKILL : \"requires\"\n EXPERIENCE {\n uuid id PK\n string company\n string role\n string description\n date startDate\n date endDate\n }\n \n EDUCATION {\n uuid id PK\n string institution\n string degree\n string field\n date graduationDate\n }\n \n SKILL ||--|| SKILL_CATEGORY : \"belongsTo\"\n SKILL {\n uuid id PK\n string name\n int proficiency\n int yearsOfExperience\n }\n \n SKILL_CATEGORY {\n uuid id PK\n string name UK\n string description\n int displayOrder\n }\n \n MEDIA {\n uuid id PK\n string filename\n string mimetype\n string url\n string category\n int fileSize\n }\n \n CAROUSEL_ITEM {\n uuid id PK\n string title\n string subtitle\n string imageUrl\n string linkUrl\n int order\n boolean isActive\n }\n \n AI_SETTINGS {\n uuid id PK\n string provider\n string model\n string encryptedApiKey\n json config\n }\n \n STYLE_CONFIG {\n uuid id PK\n string themeName UK\n json colorPalette\n json typography\n boolean isActive\n }\n```\n\n### Service Architecture\n\n**Backend Services (16 total)**\n\n| Service | Responsibility | Lines of Code |\n|---------|----------------|---------------|\n| `ProfileService` | Profile CRUD operations | ~180 |\n| `ProjectService` | Project management | ~320 |\n| `ExperienceService` | Work history | ~200 |\n| `EducationService` | Academic background | ~180 |\n| `SkillService` | Skill management | ~220 |\n| `MediaService` | Asset upload & management | ~280 |\n| `AiService` | Multi-provider AI integration | ~450 |\n| `ExportService` | Static site generation | ~650 |\n| `AuthService` | JWT authentication | ~240 |\n| `CarouselService` | Homepage carousel | ~160 |\n| `StyleConfigService` | Theme management | ~190 |\n| ...and 5 more | Additional features | ~800 |\n\n**API Controllers (9 total)**\n- Public: Profile, Projects, Resume, Carousel\n- Admin: All entity CRUD, Media Upload, Static Export, AI Generation\n\n### Admin Dashboard\n\n![Admin Dashboard Overview](/Assets/Projects/Ark.Alliance.StartupCms.Ai/Admin_Dashobard.PNG)\n\n*Comprehensive admin interface for managing all portfolio content*" + }, + { + "title": "Features & Use Cases", + "type": "FUNCTIONAL", + "navOrder": 3, + "content": "## Functional Use Cases\n\n### 1. Portfolio Content Management\n\n**Scenario**: Professional wants to maintain a dynamic portfolio with CMS\n\n**Workflow**:\n1. Admin logs in with JWT authentication\n2. Navigate to Admin Dashboard\n3. Create/update profile information (name, title, summary, contact)\n4. Add work experiences with:\n - Company, role, dates\n - Detailed descriptions\n - Technology tags\n5. Create projects with:\n - Title, description, status\n - Repository and demo URLs\n - Hero images\n6. Add multi-page project documentation\n7. Upload media assets (images, PDFs, videos)\n8. Configure homepage carousel\n9. Publish changes (live update)\n\n**Benefits**:\n- ✅ Centralized content management\n- ✅ Version control through database\n- ✅ Type-safe data structures\n- ✅ Real-time preview\n- ✅ No code deployments needed\n\n![Projects Management](/Assets/Projects/Ark.Alliance.StartupCms.Ai/Admin_Projects.PNG)\n\n*Manage projects with full CRUD operations and multi-page documentation*\n\n---\n\n### 2. AI-Assisted Content Generation\n\n**Scenario**: User needs professional content for project descriptions\n\n**Workflow**:\n1. Navigate to **AI Settings** in admin panel\n2. Configure AI provider:\n - Choose: OpenAI, Anthropic, or Gemini\n - Select model (GPT-4, Claude 3 Opus, Gemini 1.5 Pro)\n - Enter API key (encrypted with AES-256)\n3. Select content entity (Project, Experience, Skill, etc.)\n4. Write prompt or select template:\n - \"Generate a professional project summary\"\n - \"Create compelling feature descriptions\"\n - \"Write technical documentation\"\n5. Review AI-generated content\n6. Edit and refine as needed\n7. Save to entity\n\n**Supported Operations**:\n- 📝 Content generation from prompts\n- 📄 Text summarization\n- ✨ Professional writing enhancement\n- 📚 Technical documentation assistance\n- 🎯 SEO optimization suggestions\n\n**AI Provider Comparison**:\n\n| Provider | Best For | Models Available |\n|----------|----------|------------------|\n| OpenAI | General purpose, code | GPT-4, GPT-4 Turbo |\n| Anthropic | Long-form content, analysis | Claude 3 Opus, Sonnet, Haiku |\n| Google Gemini | Multi-modal, structured data | Gemini 1.5 Pro, Flash |\n\n---\n\n### 3. Static Website Deployment\n\n**Scenario**: Deploy portfolio to static hosting (zero server costs)\n\n**Workflow**:\n1. Populate all CMS content (profile, projects, experiences)\n2. Configure visual theme (Architectural or Aloe Vera)\n3. Navigate to **Export** → **Static Site** in admin\n4. Click **\"Generate Static Website\"**\n5. System processes:\n - Renders all React components to HTML\n - Bundles CSS with theme variables\n - Optimizes JavaScript bundle\n - Copies media assets\n - Generates sitemap.xml\n - Creates SEO meta tags\n6. Download ZIP file (typically 5-15 MB)\n7. Extract and deploy to:\n - **GitHub Pages**: Push to `gh-pages` branch\n - **Netlify**: Drag-drop ZIP or connect repo\n - **Vercel**: Import from GitHub\n - **AWS S3 + CloudFront**: Upload to S3 bucket\n\n**Advantages**:\n- 💰 Zero server costs\n- 🚀 Infinite scalability via CDN\n- ⚡ Fast loading times (static assets)\n- 🔒 Maximum security (no backend to attack)\n- 🌍 Global CDN distribution\n- 📱 Mobile-optimized responsive design\n\n**Generated Structure**:\n```\nstatic-export.zip\n├── index.html\n├── projects.html\n├── resume.html\n├── assets/\n│ ├── images/\n│ ├── css/\n│ └── js/\n├── sitemap.xml\n└── robots.txt\n```\n\n---\n\n### 4. Dynamic Theming System\n\n**Scenario**: Customize portfolio appearance without code changes\n\n**Workflow**:\n1. Access **StyleConfig** in admin panel\n2. Choose from built-in themes:\n - **Architectural**: Clean, minimalist, structural aesthetic\n - **Aloe Vera**: Organic, nature-inspired, warm tones\n3. Or create custom theme with JSON:\n ```json\n {\n \"colorPalette\": {\n \"primary\": \"#3B82F6\",\n \"secondary\": \"#8B5CF6\",\n \"accent\": \"#F59E0B\"\n },\n \"typography\": {\n \"fontFamily\": \"Inter, sans-serif\",\n \"headingWeight\": 700\n }\n }\n ```\n4. Click **\"Apply Theme\"** (instant, no reload)\n5. Preview changes across all pages\n6. Save configuration to database\n7. Theme persists across user sessions\n\n**Configuration Options**:\n- 🎨 Color palettes (primary, secondary, accent, backgrounds)\n- 🔤 Typography (fonts, sizes, weights, line heights)\n- 📏 Spacing and padding\n- 🖼️ Border styles and radius\n- 🌈 Gradient definitions\n- 🎭 Component-specific overrides\n\n![Hero Carousel Management](/Assets/Projects/Ark.Alliance.StartupCms.Ai/Admin_Hero_Carrousel.PNG)\n\n*Configure homepage carousel with dynamic theming*\n\n---\n\n### 5. Multi-Page Project Documentation\n\n**Scenario**: Create comprehensive technical documentation for projects\n\n**Workflow**:\n1. Create or edit **Project** entity\n2. Navigate to **Project Pages** tab\n3. Add pages with specific types:\n - **OVERVIEW**: Project summary, quick start\n - **FUNCTIONAL**: Feature specifications, use cases\n - **TECHNICAL**: Implementation details, API docs\n - **ARCHITECTURE**: System diagrams (Mermaid support)\n - **ROADMAP**: Future plans, milestones\n4. Write content in Markdown:\n - Headers, lists, tables\n - Code blocks with syntax highlighting\n - **Mermaid diagrams** for architecture\n - Links and images\n5. Set navigation order (drag-and-drop)\n6. Preview rendered output\n7. Publish (available immediately)\n\n**Benefits**:\n- 📖 Professional documentation structure\n- 🔄 Version controlled in database\n- 🔍 Searchable content\n- 🎨 Rich formatting with Markdown\n- 📊 Visual diagrams with Mermaid\n- 🔗 Deep linking to specific sections\n\n![Resume/Experience Management](/Assets/Projects/Ark.Alliance.StartupCms.Ai/Admin_ResumeManager.PNG)\n\n*Manage professional experience with timeline visualization*\n\n---\n\n### 6. Media Asset Management\n\n**Scenario**: Organize and optimize portfolio media\n\n**Workflow**:\n1. Navigate to **Media** in admin panel\n2. Upload files:\n - Images: PNG, JPG, WebP (auto-optimization)\n - Documents: PDF, DOCX\n - Videos: MP4, WebM\n3. Organize with:\n - Categories (Projects, Experiences, General)\n - Tags for searchability\n - Descriptions and alt text\n4. System processes:\n - Thumbnail generation\n - Image optimization\n - CDN-ready URLs\n5. Use media in projects, experiences, or carousel\n6. Track usage and file sizes\n\n**Supported Operations**:\n- 📤 Upload (single or batch)\n- 🗂️ Categorize and tag\n- 🔍 Search and filter\n- 🖼️ Preview and edit metadata\n- 🗑️ Delete with orphan detection\n- 📊 Storage analytics" + }, + { + "title": "Deployment & Integration", + "type": "TECHNICAL", + "navOrder": 4, + "content": "## Deployment\n\n### Prerequisites\n\n- Node.js >= 18.0.0\n- npm >= 9.0.0\n- Git for version control\n\n### Development Setup\n\n```bash\n# 1. Clone repository\ngit clone https://github.com/M2H-Machine-to-Human-Race/Ark.Alliance.Portfolio.git\ncd Ark.Alliance.Portfolio\n\n# 2. Install and build Shared Library (REQUIRED FIRST)\ncd Ark.Alliance.StartupCms.Ai.Share\nnpm install\nnpm run build\n\n# 3. Install and start Backend\ncd ../Ark.Alliance.StartupCms.Ai.Backend\nnpm install\nnpm run dev\n# Backend runs on https://localhost:3085\n\n# 4. In new terminal - Install and start Frontend\ncd Ark.Alliance.StartupCms.Ai.UI\nnpm install\nnpm run dev\n# Frontend runs on http://localhost:3080\n```\n\n### Production Build\n\n```bash\n# Build all layers\ncd Ark.Alliance.StartupCms.Ai.Share && npm run build\ncd ../Ark.Alliance.StartupCms.Ai.Backend && npm run build\ncd ../Ark.Alliance.StartupCms.Ai.UI && npm run build\n```\n\n### Environment Variables\n\n**Backend** (`Ark.Alliance.StartupCms.Ai.Backend/.env`):\n```env\n# Server Configuration\nPORT=3085\nNODE_ENV=development\n\n# Database\nDATABASE_TYPE=sqlite\nDATABASE_NAME=portfolio.db\n\n# Authentication\nJWT_SECRET=your-super-secret-jwt-key\nJWT_EXPIRES_IN=24h\n\n# AI Services (encrypted in production)\nOPENAI_API_KEY=sk-...\nANTHROPIC_API_KEY=sk-ant-...\nGOOGLE_AI_API_KEY=...\n\n# CORS\nCORS_ORIGIN=http://localhost:3080\n```\n\n**Frontend** (`Ark.Alliance.StartupCms.Ai.UI/.env`):\n```env\n# API Configuration\nVITE_API_URL=https://localhost:3085/api\nVITE_USE_MOCK_DATA=false\n\n# Feature Flags\nVITE_ENABLE_AI_FEATURES=true\nVITE_ENABLE_STATIC_EXPORT=true\n```\n\n---\n\n## Testing\n\n### Running Tests\n\n```bash\ncd Ark.Alliance.StartupCms.Ai.Tests\nnpm install\n\n# Run all tests\nnpm test\n\n# With coverage report\nnpm run test:coverage\n```\n\n**Test Statistics**: 235+ tests with 100% pass rate using Jest and React Testing Library.\n\n---\n\n## CI/CD Pipeline\n\n### GitHub Actions Workflow\n\nTriggered on:\n- **Push** to `main` or `develop` branches\n- **Pull Requests** targeting `main` or `develop`\n\n**Workflow Steps**:\n1. ✅ Build Shared Library\n2. ✅ Build Backend\n3. ✅ Build Frontend\n4. ✅ Run Test Suite (235+ tests)\n\n**Branch Protection**:\n\n| Branch | Protection Rules |\n|--------|-----------------|\n| `main` | Requires PR approval, passing CI, no force push |\n| `develop` | Requires passing CI |\n\n---\n\n## Security\n\n### Security Features\n\n- 🔐 **JWT Authentication**: Token-based auth with bcrypt hashing\n- 🛡️ **Helmet Headers**: Security headers for all responses\n- 🔑 **Encrypted Storage**: AES-256 encryption for AI API keys\n- 🚫 **CORS Protection**: Configurable allowed origins\n- 🔒 **Input Validation**: Class-validator and sanitization\n- 📊 **Rate Limiting**: Prevent abuse and DoS attacks\n\n### Reporting Vulnerabilities\n\nFor security concerns, review [SECURITY.md](https://github.com/M2H-Machine-to-Human-Race/Ark.Alliance.Portfolio/blob/main/SECURITY.md) or email: security@ark-alliance.io\n\n---\n\n## Static Site Export\n\n### Export Process\n\n1. Log in to Admin Dashboard\n2. Navigate to **Export** → **Static Site**\n3. Click **Generate Static Website**\n4. Download ZIP file (5-15 MB)\n5. Deploy to:\n - **GitHub Pages**: Push to `gh-pages` branch\n - **Netlify**: Drag-drop or connect repo\n - **Vercel**: Import from GitHub\n - **AWS S3**: Upload to bucket\n\n### Output Structure\n\n```\nstatic-export.zip\n├── index.html # Homepage\n├── projects.html # Projects catalog\n├── resume.html # Professional resume\n├── assets/\n│ ├── images/ # Optimized images\n│ ├── css/ # Bundled styles\n│ └── js/ # Minified JavaScript\n├── sitemap.xml # SEO sitemap\n└── robots.txt # Crawler instructions\n```\n\n---\n\n## Contributing\n\nWe welcome contributions! Please read [CONTRIBUTING.md](https://github.com/M2H-Machine-to-Human-Race/Ark.Alliance.Portfolio/blob/main/CONTRIBUTING.md) for guidelines.\n\n### Quick Guide\n\n1. Fork the repository\n2. Create feature branch (`git checkout -b feature/amazing-feature`)\n3. Commit changes (`git commit -m 'Add amazing feature'`)\n4. Push to branch (`git push origin feature/amazing-feature`)\n5. Open Pull Request\n\n---\n\n## License\n\nThis project is licensed under the **MIT License** - see [LICENSE.md](https://github.com/M2H-Machine-to-Human-Race/Ark.Alliance.Portfolio/blob/main/LICENSE.md) for details.\n\n**Built with ❤️ by Armand Richelet-Kleinberg | © M2H.IO - Ark Alliance Ecosystem**" + } + ] +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/carousel-ark-portfolio.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/carousel-ark-portfolio.json new file mode 100644 index 0000000000000000000000000000000000000000..93275f96ba762aa00d323ecfad60156dfa659010 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/carousel-ark-portfolio.json @@ -0,0 +1,48 @@ +[ + { + "title": "Ark.Alliance.Portfolio", + "subtitle": "AI-Powered Portfolio CMS & Static Site Generator", + "description": "Production-grade content management system for portfolios with multi-provider AI integration, one-click static export, and comprehensive admin dashboard.", + "imageUrl": "/Assets/Projects/Ark.Alliance.StartupCms.Ai/portfolio-hero.png", + "linkUrl": "/projects/ark-alliance-portfolio", + "order": 1, + "isActive": true + }, + { + "title": "Admin Dashboard", + "subtitle": "Comprehensive Content Management Interface", + "description": "Manage all portfolio content through an intuitive admin interface with real-time preview and AI-assisted content creation.", + "imageUrl": "/Assets/Projects/Ark.Alliance.StartupCms.Ai/Admin_Dashobard.PNG", + "linkUrl": "/admin/dashboard", + "order": 2, + "isActive": true + }, + { + "title": "Project Management", + "subtitle": "Multi-Page Documentation System", + "description": "Create rich project documentation with Markdown support, Mermaid diagrams, and comprehensive feature descriptions.", + "imageUrl": "/Assets/Projects/Ark.Alliance.StartupCms.Ai/Admin_Projects.PNG", + "linkUrl": "/admin/projects", + "order": 3, + "isActive": true + }, + { + "title": "Homepage Carousel", + "subtitle": "Dynamic Homepage Content", + "description": "Configure eye-catching carousel items with custom images, titles, and links to highlight your best work.", + "imageUrl": "/Assets/Projects/Ark.Alliance.StartupCms.Ai/Admin_Hero_Carrousel.PNG", + "linkUrl": "/admin/carousel", + "order": 4, + "isActive": true + }, + { + "title": "Resume Builder", + "subtitle": "Professional Experience Timeline", + "description": "Build your professional story with rich experience entries, education history, and skill management.", + "imageUrl": "/Assets/Projects/Ark.Alliance.StartupCms.Ai/Admin_ResumeManager.PNG", + "linkUrl": "/admin/resume", + "order": 5, + "isActive": true + } +] + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/carousel-react-component.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/carousel-react-component.json new file mode 100644 index 0000000000000000000000000000000000000000..7e4337a7a904dccabd0de92ece6b103f93c13d9a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/carousel-react-component.json @@ -0,0 +1,11 @@ +[ + { + "title": "Ark.Alliance.React.Component.UI", + "subtitle": "Enterprise MVVM Component Library", + "description": "Production-grade React components with strict MVVM architecture, Zod validation, and 100% test coverage. 40 categories for Finance, Healthcare, E-Commerce, and more.", + "imageUrl": "/Assets/Projects/Ark.Alliance.React.Component/components-hero.png", + "linkUrl": "/projects/ark-alliance-react-component-ui", + "order": 1, + "isActive": true + } +] diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/projects.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/projects.json new file mode 100644 index 0000000000000000000000000000000000000000..429d2d26e29cf2a8856f9bb08d34179838c30383 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/projects.json @@ -0,0 +1,248 @@ +[ + { + "title": "Ark.Alliance.Portfolio", + "description": "Production-grade portfolio CMS and static site generator with AI-powered content creation. Features 20 TypeORM entities, multi-provider AI integration (OpenAI, Anthropic, Gemini), one-click static export, MVVM architecture, 235+ passing tests, and comprehensive admin dashboard. Built with React 18, TypeScript, Express, and TypeORM with end-to-end type safety.", + "status": "Featured", + "imageUrl": "/Assets/Projects/Ark.Alliance.StartupCms.Ai/portfolio-hero.png", + "repoUrl": "https://github.com/M2H-Machine-to-Human-Race/Ark.Alliance.Portfolio", + "demoUrl": "", + "packageUrl": "", + "technologies": [ + "react", + "typescript", + "nodejs", + "express", + "typeorm", + "sqlite", + "vite", + "jest", + "rest", + "openai", + "anthropic", + "gemini" + ], + "features": [ + { + "title": "20-Entity Content Management System", + "description": "Comprehensive CMS with TypeORM entities for Profile, Projects (with multi-page documentation), Experiences, Education, Skills, Media Assets, Carousel, Widgets, and more. Full CRUD operations through an intuitive admin dashboard.", + "icon": "database" + }, + { + "title": "Multi-Provider AI Integration", + "description": "Native integration with OpenAI GPT-4, Anthropic Claude 3, and Google Gemini. AES-256 encrypted API key storage, intelligent content generation, summarization, and professional writing enhancement for all CMS entities.", + "icon": "brain" + }, + { + "title": "One-Click Static Site Export", + "description": "Transform your portfolio CMS into a deployable static website with a single click. Generates optimized HTML/CSS/JS bundles ready for GitHub Pages, Netlify, Vercel, or any CDN with zero server costs.", + "icon": "download" + }, + { + "title": "Clean Three-Tier Architecture", + "description": "Enterprise-grade separation with Presentation (React MVVM), Shared Contracts (TypeScript DTOs), and Application (Express + TypeORM) layers. Repository pattern, type-safe APIs, and comprehensive error handling throughout.", + "icon": "sitemap" + }, + { + "title": "Production-Grade Security", + "description": "JWT authentication with bcrypt password hashing (12 rounds), Helmet security headers, CORS protection, rate limiting, and AES-256 encryption for sensitive credentials. Built for deployment from day one.", + "icon": "shield-alt" + }, + { + "title": "Dynamic Theme System", + "description": "Runtime theme switching with no page reload. Built-in Architectural and Aloe Vera themes, plus JSON-based custom theme creation. CSS variable injection enables instant visual transformations.", + "icon": "palette" + } + ], + "pages": [ + { + "title": "Overview", + "type": "OVERVIEW", + "navOrder": 1, + "content": "## Ark.Alliance.Portfolio\n\n**Production-Grade Portfolio CMS & Static Site Generator**\n\nA comprehensive portfolio platform that goes beyond simple showcases. It combines enterprise-grade software practices with AI-powered content generation, full content management capabilities, and one-click static site deployment.\n\n### Perfect For\n\n- 💼 **Professionals** — Showcase work experience and projects with dynamic CMS\n- 🎓 **Academics** — Document research, publications, and teaching\n- 🎨 **Creatives** — Present portfolio work with rich media management\n- 💻 **Engineers** — Demonstrate technical projects with multi-page documentation\n- 🏢 **Agencies** — Manage team portfolios with collaborative features\n\n### Core Capabilities\n\n| Capability | Description |\n|------------|-------------|\n| **Content Management** | 20 TypeORM entities covering all portfolio aspects |\n| **AI Content Creation** | Multi-provider AI (OpenAI, Anthropic, Gemini) |\n| **Static Site Export** | One-click deployment to any static host |\n| **Multi-Page Projects** | Rich documentation with Markdown + Mermaid |\n| **Dynamic Theming** | Runtime theme switching, no reload required |\n| **Media Management** | Upload, organize, and optimize assets |\ +| **Type Safety** | End-to-end TypeScript with shared DTOs |\n| **Security** | JWT auth, bcrypt, AES-256, Helmet headers |\n\n### Technology Stack\n\n**Frontend**\n- React 18 with TypeScript 5.x\n- Vite build tool\n- MVVM component architecture\n- React Router for navigation\n- Axios for API communication\n\n**Backend**\n- Node.js 18+ with Express\n- TypeORM for data access\n- SQLite (dev) / PostgreSQL (prod)\n- 16 business services\n- 9 API controllers\n\n**Shared**\n- TypeScript DTOs\n- Shared enumerations\n- Type-safe contracts\n\n**AI Integration**\n- OpenAI GPT-4 & GPT-4 Turbo\n- Anthropic Claude 3 (Opus, Sonnet, Haiku)\n- Google Gemini 1.5 (Pro, Flash)\n\n### Key Statistics\n\n- **20** TypeORM Entities\n- **16** Backend Services \n- **9** API Controllers\n- **235+** Test Cases (100% pass rate)\n- **3** Architecture Layers\n- **3** AI Providers\n\n### Quick Start\n\n```bash\n# Install shared library\ncd Ark.Alliance.StartupCms.Ai.Share\nnpm install && npm run build\n\n# Start backend (port 3085)\ncd ../Ark.Alliance.StartupCms.Ai.Backend\nnpm install && npm run dev\n\n# Start frontend (port 3080)\ncd ../Ark.Alliance.StartupCms.Ai.UI\nnpm install && npm run dev\n```\n\nDefault admin credentials: `admin` / `admin123`" + }, + { + "title": "Architecture & System Design", + "type": "ARCHITECTURE", + "navOrder": 2, + "content": "## System Architecture\n\n### Three-Tier Architecture\n\n```mermaid\ngraph TB\n subgraph Client[\"🖥️ Presentation Layer\"]\n UI[\"Ark.Alliance.StartupCms.Ai.UI
React 18 + TypeScript
MVVM Architecture
Vite Build Tool\"]\n end\n \n subgraph Contracts[\"📦 Shared Contracts\"]\n Share[\"Ark.Alliance.StartupCms.Ai.Share
DTOs + Enums
Type Definitions
End-to-End Type Safety\"]\n end\n \n subgraph Server[\"⚙️ Application Layer\"]\n Backend[\"Ark.Alliance.StartupCms.Ai.Backend
Express REST API
16 Services + 9 Controllers
TypeORM Data Access\"]\n end\n \n subgraph Data[\"💾 Data Layer\"]\n DB[(\"SQLite (Dev)
PostgreSQL (Prod)
20 Entities\")]\n end\n \n subgraph External[\"🌐 External Services\"]\ + AI1[\"OpenAI
GPT-4\"]\ + AI2[\"Anthropic
Claude 3\"]\ + AI3[\"Google
Gemini 1.5\"]\ + end\ + \ + UI -->|\"REST API
(Axios)\"| Backend\ + UI -.->|\"Import Types\"| Share\ + Backend -.->|\"Import Types\"| Share\ + Backend -->|\"TypeORM ORM\"| DB\ + Backend -->|\"Encrypted API\"| AI1\ + Backend -->|\"Encrypted API\"| AI2\ + Backend -->|\"Encrypted API\"| AI3\ + \ + style Client fill:#2d3748,stroke:#4299e1,stroke-width: 2px,color:#fff\ + style Contracts fill:#2d3748,stroke:#48bb78,stroke-width: 2px,color:#fff\ + style Server fill:#2d3748,stroke:#ed8936,stroke-width: 2px,color:#fff\ + style Data fill:#2d3748,stroke:#9f7aea,stroke-width: 2px,color:#fff\ + style External fill:#2d3748,stroke:#f56565,stroke-width: 2px,color:#fff\ +```\n\n### Entity Relationship Model\n\n```mermaid\nerDiagram\n PROFILE ||--o{ USER : \"manages\"\n PROFILE {\n uuid id PK\n string name\n string title\n string summary\n string email\n string phone\n string location\n }\n \n USER ||--o{ PROJECT : \"creates\"\n USER {\n uuid id PK\n string username UK\n string password\n string role\n }\n \n PROJECT ||--|{ PROJECT_PAGE : \"contains\"\n PROJECT ||--|{ PROJECT_FEATURE : \"has\"\n PROJECT ||--|{ PROJECT_TECHNOLOGY : \"uses\"\n PROJECT {\n uuid id PK\n string title\n string description\n string status\n string imageUrl\n string repoUrl\n string demoUrl\n }\n \n PROJECT_PAGE {\n uuid id PK\n string title\n string type\n text content\n int navOrder\n }\n \n PROJECT_FEATURE {\n uuid id PK\n string title\n text description\n string icon\n }\n \n TECHNOLOGY ||--|{ PROJECT_TECHNOLOGY : \"links\"\n TECHNOLOGY {\n uuid id PK\n string name UK\n string category\n string icon\n }\n \n EXPERIENCE ||--|{ SKILL : \"requires\"\n EXPERIENCE {\n uuid id PK\n string company\n string role\n string description\n date startDate\n date endDate\n }\n \n EDUCATION {\n uuid id PK\n string institution\n string degree\n string field\n date graduationDate\n }\n \n SKILL ||--|| SKILL_CATEGORY : \"belongsTo\"\n SKILL {\n uuid id PK\n string name\n int proficiency\n int yearsOfExperience\n }\n \n SKILL_CATEGORY {\n uuid id PK\n string name UK\n string description\n int displayOrder\n }\n \n MEDIA {\n uuid id PK\n string filename\n string mimetype\n string url\n string category\n int fileSize\n }\n \n CAROUSEL_ITEM {\n uuid id PK\n string title\n string subtitle\n string imageUrl\n string linkUrl\n int order\n boolean isActive\n }\n \n AI_SETTINGS {\n uuid id PK\n string provider\n string model\n string encryptedApiKey\n json config\n }\n \n STYLE_CONFIG {\n uuid id PK\n string themeName UK\n json colorPalette\n json typography\n boolean isActive\n }\n```\n\n### Service Architecture\n\n**Backend Services (16 total)**\n\n| Service | Responsibility | Lines of Code |\n|---------|----------------|---------------|\n| `ProfileService` | Profile CRUD operations | ~180 |\n| `ProjectService` | Project management | ~320 |\n| `ExperienceService` | Work history | ~200 |\n| `EducationService` | Academic background | ~180 |\n| `SkillService` | Skill management | ~220 |\n| `MediaService` | Asset upload & management | ~280 |\n| `AiService` | Multi-provider AI integration | ~450 |\n| `ExportService` | Static site generation | ~650 |\n| `AuthService` | JWT authentication | ~240 |\n| `CarouselService` | Homepage carousel | ~160 |\n| `StyleConfigService` | Theme management | ~190 |\n| ...and 5 more | Additional features | ~800 |\n\n**API Controllers (9 total)**\n- Public: Profile, Projects, Resume, Carousel\n- Admin: All entity CRUD, Media Upload, Static Export, AI Generation\n\n### Admin Dashboard\n\n![Admin Dashboard Overview](/Assets/Projects/Ark.Alliance.StartupCms.Ai/Admin_Dashobard.PNG)\n\n*Comprehensive admin interface for managing all portfolio content*" + }, + { + "title": "Features & Use Cases", + "type": "FUNCTIONAL", + "navOrder": 3, + "content": "## Functional Use Cases\n\n### 1. Portfolio Content Management\n\n**Scenario**: Professional wants to maintain a dynamic portfolio with CMS\n\n**Workflow**:\n1. Admin logs in with JWT authentication\n2. Navigate to Admin Dashboard\n3. Create/update profile information (name, title, summary, contact)\n4. Add work experiences with:\n - Company, role, dates\n - Detailed descriptions\n - Technology tags\n5. Create projects with:\n - Title, description, status\n - Repository and demo URLs\n - Hero images\n6. Add multi-page project documentation\n7. Upload media assets (images, PDFs, videos)\n8. Configure homepage carousel\n9. Publish changes (live update)\n\n**Benefits**:\n- ✅ Centralized content management\n- ✅ Version control through database\n- ✅ Type-safe data structures\n- ✅ Real-time preview\n- ✅ No code deployments needed\n\n![Projects Management](/Assets/Projects/Ark.Alliance.StartupCms.Ai/Admin_Projects.PNG)\n\n*Manage projects with full CRUD operations and multi-page documentation*\n\n---\n\n### 2. AI-Assisted Content Generation\n\n**Scenario**: User needs professional content for project descriptions\n\n**Workflow**:\n1. Navigate to **AI Settings** in admin panel\n2. Configure AI provider:\n - Choose: OpenAI, Anthropic, or Gemini\n - Select model (GPT-4, Claude 3 Opus, Gemini 1.5 Pro)\n - Enter API key (encrypted with AES-256)\n3. Select content entity (Project, Experience, Skill, etc.)\n4. Write prompt or select template:\n - \"Generate a professional project summary\"\n - \"Create compelling feature descriptions\"\n - \"Write technical documentation\"\n5. Review AI-generated content\n6. Edit and refine as needed\n7. Save to entity\n\n**Supported Operations**:\n- 📝 Content generation from prompts\n- 📄 Text summarization\n- ✨ Professional writing enhancement\n- 📚 Technical documentation assistance\n- 🎯 SEO optimization suggestions\n\n**AI Provider Comparison**:\n\n| Provider | Best For | Models Available |\n|----------|----------|------------------|\n| OpenAI | General purpose, code | GPT-4, GPT-4 Turbo |\n| Anthropic | Long-form content, analysis | Claude 3 Opus, Sonnet, Haiku |\n| Google Gemini | Multi-modal, structured data | Gemini 1.5 Pro, Flash |\n\n---\n\n### 3. Static Website Deployment\n\n**Scenario**: Deploy portfolio to static hosting (zero server costs)\n\n**Workflow**:\n1. Populate all CMS content (profile, projects, experiences)\n2. Configure visual theme (Architectural or Aloe Vera)\n3. Navigate to **Export** → **Static Site** in admin\n4. Click **\"Generate Static Website\"**\n5. System processes:\n - Renders all React components to HTML\n - Bundles CSS with theme variables\n - Optimizes JavaScript bundle\n - Copies media assets\n - Generates sitemap.xml\n - Creates SEO meta tags\n6. Download ZIP file (typically 5-15 MB)\n7. Extract and deploy to:\n - **GitHub Pages**: Push to `gh-pages` branch\n - **Netlify**: Drag-drop ZIP or connect repo\n - **Vercel**: Import from GitHub\n - **AWS S3 + CloudFront**: Upload to S3 bucket\n\n**Advantages**:\n- 💰 Zero server costs\n- 🚀 Infinite scalability via CDN\n- ⚡ Fast loading times (static assets)\n- 🔒 Maximum security (no backend to attack)\n- 🌍 Global CDN distribution\n- 📱 Mobile-optimized responsive design\n\n**Generated Structure**:\n```\nstatic-export.zip\n├── index.html\n├── projects.html\n├── resume.html\n├── assets/\n│ ├── images/\n│ ├── css/\n│ └── js/\n├── sitemap.xml\n└── robots.txt\n```\n\n---\n\n### 4. Dynamic Theming System\n\n**Scenario**: Customize portfolio appearance without code changes\n\n**Workflow**:\n1. Access **StyleConfig** in admin panel\n2. Choose from built-in themes:\n - **Architectural**: Clean, minimalist, structural aesthetic\n - **Aloe Vera**: Organic, nature-inspired, warm tones\n3. Or create custom theme with JSON:\n ```json\n {\n \"colorPalette\": {\n \"primary\": \"#3B82F6\",\n \"secondary\": \"#8B5CF6\",\n \"accent\": \"#F59E0B\"\n },\n \"typography\": {\n \"fontFamily\": \"Inter, sans-serif\",\n \"headingWeight\": 700\n }\n }\n ```\n4. Click **\"Apply Theme\"** (instant, no reload)\n5. Preview changes across all pages\n6. Save configuration to database\n7. Theme persists across user sessions\n\n**Configuration Options**:\n- 🎨 Color palettes (primary, secondary, accent, backgrounds)\n- 🔤 Typography (fonts, sizes, weights, line heights)\n- 📏 Spacing and padding\n- 🖼️ Border styles and radius\n- 🌈 Gradient definitions\n- 🎭 Component-specific overrides\n\n![Hero Carousel Management](/Assets/Projects/Ark.Alliance.StartupCms.Ai/Admin_Hero_Carrousel.PNG)\n\n*Configure homepage carousel with dynamic theming*\n\n---\n\n### 5. Multi-Page Project Documentation\n\n**Scenario**: Create comprehensive technical documentation for projects\n\n**Workflow**:\n1. Create or edit **Project** entity\n2. Navigate to **Project Pages** tab\n3. Add pages with specific types:\n - **OVERVIEW**: Project summary, quick start\n - **FUNCTIONAL**: Feature specifications, use cases\n - **TECHNICAL**: Implementation details, API docs\n - **ARCHITECTURE**: System diagrams (Mermaid support)\n - **ROADMAP**: Future plans, milestones\n4. Write content in Markdown:\n - Headers, lists, tables\n - Code blocks with syntax highlighting\n - **Mermaid diagrams** for architecture\n - Links and images\n5. Set navigation order (drag-and-drop)\n6. Preview rendered output\n7. Publish (available immediately)\n\n**Benefits**:\n- 📖 Professional documentation structure\n- 🔄 Version controlled in database\n- 🔍 Searchable content\n- 🎨 Rich formatting with Markdown\n- 📊 Visual diagrams with Mermaid\n- 🔗 Deep linking to specific sections\n\n![Resume/Experience Management](/Assets/Projects/Ark.Alliance.StartupCms.Ai/Admin_ResumeManager.PNG)\n\n*Manage professional experience with timeline visualization*\n\n---\n\n### 6. Media Asset Management\n\n**Scenario**: Organize and optimize portfolio media\n\n**Workflow**:\n1. Navigate to **Media** in admin panel\n2. Upload files:\n - Images: PNG, JPG, WebP (auto-optimization)\n - Documents: PDF, DOCX\n - Videos: MP4, WebM\n3. Organize with:\n - Categories (Projects, Experiences, General)\n - Tags for searchability\n - Descriptions and alt text\n4. System processes:\n - Thumbnail generation\n - Image optimization\n - CDN-ready URLs\n5. Use media in projects, experiences, or carousel\n6. Track usage and file sizes\n\n**Supported Operations**:\n- 📤 Upload (single or batch)\n- 🗂️ Categorize and tag\n- 🔍 Search and filter\n- 🖼️ Preview and edit metadata\n- 🗑️ Delete with orphan detection\n- 📊 Storage analytics" + }, + { + "title": "Deployment & Integration", + "type": "TECHNICAL", + "imageUrl": "/Assets/Projects/Ark.Alliance.Trading.Providers.Lib/trading-hero.png", + "repoUrl": "https://github.com/ArmandRicheletKleinberg/Ark.Alliance.Trading.Providers.Lib", + "demoUrl": "", + "packageUrl": "https://www.npmjs.com/package/ark-alliance-trading-providers-lib", + "technologies": [ + "typescript", + "nodejs", + "binance", + "deribit", + "rest", + "jest", + "docker" + ], + "features": [ + { + "title": "Multi-Provider Abstraction", + "description": "Unified interface for Binance Futures and Deribit exchanges with IProviderClient pattern. Write once, trade on any supported exchange without code changes. Extensible architecture for adding new providers.", + "icon": "plug" + }, + { + "title": "Result Pattern Error Handling", + "description": "Type-safe Result pattern for functional error handling. No exceptions in normal flow, explicit success/failure states, chainable operations (map, flatMap), and comprehensive error details with ResultStatus enum.", + "icon": "check-circle" + }, + { + "title": "Real-Time WebSocket Streams", + "description": "Low-latency market data via WebSocket connections. Subscribe to klines, book ticker, user data streams, and order events. Event-driven architecture with typed event emitters for order fills and position updates.", + "icon": "wifi" + }, + { + "title": "Secure Authentication", + "description": "HMAC-SHA256 signature generation for Binance, Ed25519 signature generation for Deribit, automatic token refresh mechanisms, and secure credential storage with IAuthStrategy interface.", + "icon": "lock" + }, + { + "title": "Comprehensive Testing", + "description": "70+ test scenarios with ReflectionTestEngine, scenario-driven JSON test definitions, dynamic parameter resolution ($DYNAMIC_LIMIT_BUY), tested against live testnets, and 100% pass rate across all categories.", + "icon": "vial" + }, + { + "title": "Clean Architecture", + "description": "Domain-driven design with clear layer separation: Domain (entities, interfaces), Application (use cases, mappers), and Infrastructure (clients, auth). TypeScript-first with full IntelliSense support.", + "icon": "sitemap" + } + ], + "pages": [ + { + "title": "Overview", + "type": "OVERVIEW", + "navOrder": 1, + "content": "## Ark Alliance Trading Providers Library\n\n**Production-Ready Multi-Provider Cryptocurrency Trading SDK**\n\nA TypeScript SDK that unifies cryptocurrency trading across multiple exchanges with a single, elegant API. Stop writing exchange-specific code—write once, trade everywhere.\n\n### Perfect For\n\n- 🤖 Algorithmic trading bots\n- 📊 Market data aggregators\n- 💼 Portfolio management systems\n- 📈 Trading analytics platforms\n\n### Key Features\n\n| Feature | Description |\n|---------|-------------|\n| **Multi-Provider** | Unified interface for Binance Futures and Deribit |\n| **Order Management** | Place, modify, cancel, and track orders |\n| **Position Tracking** | Real-time position monitoring with P&L |\n| **WebSocket Streams** | Low-latency market data and user events |\n| **Event-Driven** | Async event architecture for fills, updates |\n| **Result Pattern** | Type-safe error handling|\n| **Secure Auth** | HMAC-SHA256 (Binance) and Ed25519 (Deribit) |\n| **100% Tested** | 70+ scenarios, 100% pass rate |\n| **TypeScript-First** | Full type definitions with IntelliSense |\n| **Testnet Support** | Built-in testnet URLs for development |\n\n### Installation\n\n```bash\nnpm install ark-alliance-trading-providers-lib\n```\n\n### Quick Start\n\n```typescript\nimport { BinanceRestClient } from 'ark-alliance-trading-providers-lib/Binance';\n\n// Initialize client\nconst client = new BinanceRestClient(apiKey, secret, { testnet: true });\n\n// Place order with type-safe Result pattern\nconst orderResult = await client.placeOrder({\n symbol: 'BTCUSDT',\n side: 'BUY',\n type: 'MARKET',\n quantity: 0.001\n});\n\nif (orderResult.success) {\n console.log(`Order placed! ID: ${orderResult.data.orderId}`);\n} else {\n console.error(`Error: ${orderResult.error.message}`);\n}\n```" + }, + { + "title": "Architecture", + "type": "ARCHITECTURE", + "navOrder": 2, + "content": "## Clean Architecture\n\n### Layer Structure\n\n```\nDomain Layer\n├── Domain Entities (Order, Position, Account)\n└── Interfaces (IProviderClient, IAuthStrategy)\n ↓\nApplication Layer\n├── Use Cases (Place Order, Get Position)\n└── Data Mappers (API ↔ Domain)\n ↓\nInfrastructure Layer\n├── API Clients (REST, WebSocket)\n└── Authentication (HMAC, Ed25519)\n ↓\nExternal Systems\n└── Exchange APIs (Binance, Deribit)\n```\n\n### Provider Abstraction\n\n**Your Application**\n\n↓ Uses\n\n**IProviderClient Interface**\n- connect() / disconnect()\n- isConnected() / getProviderName()\n\n↓ Implemented by\n\n**Provider Implementations**\n- Binance Provider (REST + WebSocket)\n- Deribit Provider (JSON-RPC)\n\n↓ Uses\n\n**IAuthStrategy Interface**\n- generateSignature()\n- authenticate()\n\n↓ Connects to\n\n**External APIs**\n- Binance Futures API (HTTPS + WSS)\n- Deribit API (WebSocket JSON-RPC)" + }, + { + "title": "Use Cases & Examples", + "type": "FUNCTIONAL", + "navOrder": 3, + "content": "## Functional Use Cases\n\n### 1. Algorithmic Trading Bot\n\n**Scenario**: Automated trading strategy execution\n\n**Workflow**:\n```typescript\nimport { BinanceRestClient, BinanceWebSocketClient } from 'ark-alliance-trading-providers-lib';\n\n// Initialize clients\nconst rest = new BinanceRestClient(apiKey, secret, { testnet: true });\nconst ws = new BinanceWebSocketClient({ testnet: true });\n\n// Subscribe to market data\nws.subscribeBookTicker('BTCUSDT');\nws.on('bookTicker', async (ticker) => {\n const signal = analyzeMarket(ticker);\n \n if (signal === 'BUY') {\n const result = await rest.placeOrder({\n symbol: 'BTCUSDT',\n side: 'BUY',\n type: 'MARKET',\n quantity: 0.001\n });\n \n if (result.success) {\n console.log(`Buy order placed: ${result.data.orderId}`);\n }\n }\n});\n```\n\n### 2. Portfolio Management\n\n```typescript\nclass PortfolioManager {\n async getTotalBalance(): Promise {\n const binancePositions = await this.binanceClient.getPositionRisk();\n const deribitPositions = await this.deribitClient.getPositions();\n \n return this.calculateTotalValue([\n ...binancePositions.data || [],\n ...deribitPositions.data || []\n ]);\n }\n}\n```" + }, + { + "title": "Testing & Quality", + "type": "TECHNICAL", + "navOrder": 4, + "content": "## Test Suite\n\n### Test Coverage\n\n**Total**: 70+ scenarios \n**Pass Rate**: ✅ 100% \n**Framework**: ReflectionTestEngine (custom) \n**Execution**: Against live Binance Testnet\n\n| Scenario File | Category | Scenarios | Pass Rate |\n|---------------|----------|-----------|----------|\n| account.scenarios.json | Account | 8 | ✅ 100% |\n| market-data.scenarios.json | Market Data | 8 | ✅ 100% |\n| orders.scenarios.json | Orders | 12 | ✅ 92% (2 disabled) |\n| positions.scenarios.json | Positions | 14 | ✅ 100% |\n| gtx-orders.scenarios.json | Post-Only (GTX) | 13 | ✅ 100% |\n| market-orders.scenarios.json | Market Orders | 8 | ✅ 100% |\n| algo-orders.scenarios.json | Algo Orders | 10 | ✅ 100% |\n| mixed-orders.scenarios.json | Mixed Workflows | 10 | ✅ 100% |\n\n### Running Tests\n\n```bash\ncd Ark.Alliance.Trading.Providers.Lib.Test\n\n# Run all scenarios\nnpm run test:execute\n\n# Run specific category\nnpm run test:execute -- --filter=account\n\n# Generate report\nnpm run test:report\n```" + } + ] + }, + { + "title": "Ark.Alliance.Trading.TrendsCalculator", + "description": "Production-ready real-time cryptocurrency trend analysis microservice combining advanced statistical indicators (Hurst Exponent, GARCH, Linear Regression, EMA) with optional Google Gemini AI integration. Features WebSocket streaming, configurable thresholds, walk-forward validation, and seamless integration with distributed trading systems. Built with TypeScript, React 19, Express, and Socket.IO.", + "status": "Featured", + "imageUrl": "/Assets/Projects/Ark.Alliance.TrendsCalculator/trends-hero.png", + "repoUrl": "https://github.com/ArmandRicheletKleinberg/Ark.Alliance.Trading.TrendsCalculator", + "demoUrl": "", + "packageUrl": "", + "technologies": [ + "react", + "typescript", + "nodejs", + "express", + "socketio", + "vite", + "zod", + "binance", + "gemini" + ], + "features": [ + { + "title": "Real-Time Trend Analysis", + "description": "Sub-second WebSocket streaming of trend calculations with composite scoring from multiple mathematical indicators. Maintains rolling buffer of 200 price points per symbol for accurate statistical analysis.", + "icon": "chart-line" + }, + { + "title": "Multi-Indicator Engine", + "description": "Combines Hurst Exponent (market persistence), GARCH (volatility forecasting), Linear Regression (trend slope/R²), and EMA crossovers (50/200) into a unified composite score for LONG/SHORT/WAIT decisions.", + "icon": "calculator" + }, + { + "title": "AI-Augmented Insights", + "description": "Optional Google Gemini integration provides contextual analysis and confidence scoring. Mathematical indicators ground AI recommendations in quantitative data for informed trading decisions.", + "icon": "brain" + }, + { + "title": "Distributed Trading Integration", + "description": "REST API and WebSocket events designed for seamless integration with trading bots, portfolio managers, and analytics platforms. Event-driven architecture enables real-time signal consumption.", + "icon": "network-wired" + }, + { + "title": "Walk-Forward Validation", + "description": "Training mode for backtesting and parameter optimization. Evaluate strategy performance across historical data with accuracy metrics per direction (LONG/SHORT/WAIT).", + "icon": "graduation-cap" + }, + { + "title": "MVVM Architecture", + "description": "Clean separation of concerns with Model-View-ViewModel pattern in React frontend. Zod-validated DTOs ensure type safety across the entire stack from backend to UI.", + "icon": "layer-group" + } + ], + "pages": [ + { + "title": "Overview", + "type": "OVERVIEW", + "navOrder": 1, + "content": "## Ark.Alliance.Trading.TrendsCalculator\n\n**Real-Time Cryptocurrency Trend Analysis Microservice**\n\nA production-grade service that transforms raw market data into actionable trend intelligence. It answers one critical question for trading systems: **Should I go LONG, SHORT, or WAIT?**\n\n### Core Capabilities\n\n| Capability | Description |\n|------------|-------------|\n| **Real-Time Analysis** | Sub-second trend updates via WebSocket |\n| **Multi-Indicator** | Hurst, GARCH, Linear Regression, EMA |\n| **AI Integration** | Optional Gemini for contextual insights |\n| **Training Mode** | Walk-forward validation for optimization |\n| **Type Safety** | Zod-validated DTOs across stack |\n\n### Perfect For\n\n- 🤖 **Algorithmic Trading Bots** — Entry/exit signal generation\n- 📊 **Analytics Platforms** — Multi-symbol trend monitoring\n- 💼 **Portfolio Managers** — Risk assessment and allocation\n- 🔬 **Quant Research** — Strategy backtesting and validation\n\n### Technology Stack\n\n- **Frontend**: React 19, TypeScript, Vite, Socket.IO Client\n- **Backend**: Node.js 18+, Express, Socket.IO Server\n- **Math Engine**: Hurst, GARCH, Linear Regression, EMA\n- **External**: Binance Futures API, Google Gemini AI\n- **Shared**: Zod validation, TypeScript DTOs" + }, + { + "title": "Architecture & Data Model", + "type": "ARCHITECTURE", + "navOrder": 2, + "content": "## System Architecture\n\n### Three-Layer Structure\n\n```\nPresentation Layer (TrendsCalculator.Ui)\n├── React 19 + TypeScript\n├── MVVM Component Pattern\n├── Socket.IO Real-Time\n└── Vite Build Tool\n ↓\nShared Layer (TrendsCalculator.Share)\n├── 23 Data Transfer Objects\n├── 12 Enumerations\n└── Zod Validation Schemas\n ↓\nApplication Layer (TrendsCalculator.Backend)\n├── Express REST API\n├── Socket.IO WebSocket\n├── 5 Core Services\n└── Mathematical Engine\n ↓\nExternal Systems\n├── Binance Futures (Market Data)\n└── Google Gemini (AI Analysis)\n```\n\n### Core Services\n\n| Service | Lines | Responsibility |\n|---------|-------|----------------|\n| `TrendCalculatorService` | 751 | Trend calculation engine |\n| `BinanceStreamService` | 290 | Market data ingestion |\n| `SymbolTrackingService` | 220 | Symbol lifecycle |\n| `StreamingAnalysisService` | 330 | Real-time processing |\n| `MarketDataService` | 180 | Data aggregation |\n\n### Mathematical Indicators\n\n**Hurst Exponent** — Market memory analysis\n- H < 0.5: Mean-reverting (range trading)\n- H ≈ 0.5: Random walk (neutral)\n- H > 0.5: Trending (momentum)\n\n**GARCH** — Volatility forecasting\n- Predicts next-period variance\n- Identifies volatility clustering\n\n**Linear Regression** — Trend direction\n- Slope: direction and steepness\n- R²: trend reliability (0-1)\n\n**EMA Crossover** — MA bias\n- EMA 50 > EMA 200: Bullish\n- EMA 50 < EMA 200: Bearish" + }, + { + "title": "Features & Use Cases", + "type": "FUNCTIONAL", + "navOrder": 3, + "content": "## Functional Use Cases\n\n### 1. Algorithmic Trading Bot Integration\n\n**Scenario**: Trading bot needs trend signals for entry timing\n\n**Workflow**:\n1. Bot connects via WebSocket\n2. Subscribes to symbol (e.g., BTCUSDT)\n3. Receives `buffer:progress` events during warmup\n4. Receives `trend:update` events with direction + strength\n5. Executes trades based on composite score threshold\n\n**Benefits**:\n- Sub-second latency\n- Confidence scoring\n- Multiple indicator confirmation\n\n### 2. Multi-Symbol Portfolio Screening\n\n**Scenario**: Analyst monitors 20 symbols for strong trends\n\n**Workflow**:\n1. Track multiple symbols via REST API\n2. Receive parallel WebSocket updates\n3. Filter by strength > 0.7\n4. Request AI analysis for top candidates\n5. Make informed allocation decisions\n\n**Benefits**:\n- Parallel processing\n- Unified trend view\n- AI-augmented insights\n\n### 3. Walk-Forward Strategy Validation\n\n**Scenario**: Quant validates indicator parameters\n\n**Workflow**:\n1. Start training session with historical data\n2. System splits into train/test periods\n3. Parameters optimized on training data\n4. Validated against test data\n5. Accuracy metrics reported per direction\n\n**Benefits**:\n- Avoid overfitting\n- Direction-specific accuracy\n- Parameter optimization\n\n### 4. Real-Time Dashboard Monitoring\n\n**UI Pages**:\n- **Overview**: Summary metrics and active symbols\n- **Symbols**: Add/remove tracking with buffer progress\n- **Visualization**: Live price charts with trend overlays\n- **Configuration**: AI settings and parameters\n- **Training**: Validation metrics and history" + }, + { + "title": "Deployment & Integration", + "type": "TECHNICAL", + "navOrder": 4, + "content": "## Deployment\n\n### Prerequisites\n\n- Node.js >= 18.0.0\n- npm >= 8.0.0\n- Binance account (optional, for live data)\n- Google Gemini API key (optional, for AI)\n\n### Quick Start\n\n```bash\n# 1. Install Share layer\ncd Ark.Alliance.TrendsCalculator.Share\nnpm install && npm run build\n\n# 2. Start Backend\ncd ../Ark.Alliance.TrendsCalculator.Backend\nnpm install\ncp .env.example .env\nnpm run dev\n\n# 3. Start Frontend\ncd ../Ark.Alliance.TrendsCalculator.Ui\nnpm install\nnpm run dev\n```\n\n### Environment Variables\n\n```env\n# Server\nPORT=3001\nNODE_ENV=development\n\n# Binance\nBINANCE_API_KEY=your_key\nBINANCE_SECRET_KEY=your_secret\nBINANCE_USE_TESTNET=true\n\n# AI (optional)\nGEMINI_API_KEY=your_gemini_key\n\n# Calculation\nDEFAULT_BUFFER_SIZE=200\nMIN_BUFFER_SIZE=50\n```\n\n## API Integration\n\n### REST Endpoints\n\n| Endpoint | Method | Purpose |\n|----------|--------|---------|\n| `/api/health` | GET | Health check |\n| `/api/symbol/track` | POST | Start tracking |\n| `/api/symbol/:symbol/track` | DELETE | Stop tracking |\n| `/api/symbol/:symbol/status` | GET | Buffer status |\n| `/api/trend/:symbol/analyze` | GET | Current trend |\n| `/api/trend/:symbol/history` | GET | Trend history |\n\n### WebSocket Events\n\n**Server → Client**:\n- `trend:update` — Trend analysis result\n- `buffer:progress` — Buffer filling status\n- `symbol:added` — Symbol tracking started\n- `ai:analysis` — AI analysis result\n\n**Client → Server**:\n- `subscribe:symbol` — Subscribe to updates\n- `unsubscribe:symbol` — Unsubscribe\n\n### Example Consumer\n\n```typescript\nimport { io } from 'socket.io-client';\n\nconst socket = io('wss://trends-api.example.com');\n\nsocket.emit('subscribe:symbol', 'BTCUSDT');\n\nsocket.on('trend:update', (data) => {\n console.log(`${data.symbol}: ${data.direction}`);\n console.log(`Strength: ${data.strength}`);\n console.log(`Composite: ${data.compositeScore}`);\n});\n```" + } + ] + } + ] diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/react-component-ui.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/react-component-ui.json new file mode 100644 index 0000000000000000000000000000000000000000..0a721301d8e9af1a5f895b8d467c2f9a6e857824 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/react-component-ui.json @@ -0,0 +1,78 @@ +{ + "title": "Ark.Alliance.React.Component.UI", + "description": "Enterprise-grade React component library implementing strict MVVM architecture with Zod runtime validation. Features 40 component categories serving multiple industry verticals (Finance/Trading, Healthcare, Logistics, E-Commerce, AI/ML). Built with React 19, TypeScript 5.9, and Tailwind CSS 4. Includes 258 passing tests (100% coverage), premium visual modes (neon, minimal, glass), Three.js 3D charts, and interactive showcase dashboard. Published on NPM as ark-alliance-react-ui.", + "status": "Featured", + "imageUrl": "/Assets/Projects/Ark.Alliance.React.Component/components-hero.png", + "repoUrl": "https://github.com/ArmandRicheletKleinberg/Ark.Alliance.React.Component.UI", + "demoUrl": "", + "packageUrl": "https://www.npmjs.com/package/ark-alliance-react-ui", + "technologies": [ + "react", + "typescript", + "tailwindcss", + "zod", + "threejs", + "vite", + "vitest", + "npm", + "docker" + ], + "features": [ + { + "title": "Strict MVVM Architecture", + "description": "Model-View-ViewModel pattern with Zod schema Models, React Hook ViewModels (useState, useCallback), and pure presentation Views (forwardRef, memo). Ensures clear separation of concerns, testability, and maintainability with complete type inference.", + "icon": "layer-group" + }, + { + "title": "40 Component Categories", + "description": "Comprehensive library spanning Buttons, Cards, 5 Gauge types, 6 Input variants, Three.js 3D Charts, DataGrids, Modals, Timelines, Markdown renderers, and 28 more categories. Designed for multi-domain enterprise applications across Finance, Healthcare, Logistics, and beyond.", + "icon": "th" + }, + { + "title": "Multi-Industry Support", + "description": "Architected for Finance/Trading (primary), Healthcare, Logistics, E-Commerce, AI/ML, Music, Video, Social Media, and Payments. Premium neon aesthetics for trading dashboards, professional modes for healthcare, and glassmorphism for modern UIs.", + "icon": "industry" + }, + { + "title": "Zod Runtime Validation", + "description": "All models use Zod 4 schemas for runtime type checking with clear error messages. TypeScript types inferred from schemas ensure compile-time AND runtime safety. Pattern-based input restrictions and configurable length limits.", + "icon": "check-circle" + }, + { + "title": "Premium Visual Modes", + "description": "Four visual modes with instant switching: Normal (standard), Neon (glowing borders/gradients for trading), Minimal (reduced weight), Glass (glassmorphism with backdrop blur). Mode switching via simple prop—no code changes required.", + "icon": "palette" + }, + { + "title": "100% Test Coverage", + "description": "258 tests with Vitest 2.1.8 + React Testing Library 16.1.0. Component tests (35 BaseInput, 28 ProgressBar, 25 GenericPanel, 24 Tooltip, 23 TradingGridCard), ViewModel tests, Model validation tests. Scenario-driven architecture with ComponentTestEngine.", + "icon": "vial" + } + ], + "pages": [ + { + "title": "Overview", + "type": "OVERVIEW", + "navOrder": 1, + "content": "## Ark.Alliance.React.Component.UI\n\n**Enterprise-Grade MVVM React Component Library**\n\nA production-ready UI component library built with strict Model-View-ViewModel architecture, comprehensive Zod validation, and premium visual aesthetics. Designed for multi-domain enterprise applications with focus on Finance/Trading, Healthcare, Logistics, E-Commerce, and other industry verticals.\n\n### Perfect For\n\n- 🏦 **Financial Platforms** — Trading dashboards, portfolio management, market data visualization\n- 🏥 **Healthcare Applications** — Patient portals, clinical dashboards, health monitoring\n- 🚚 **Logistics Systems** — Shipping tracking, inventory management, warehouse operations\n- 🛒 **E-Commerce Platforms** — Shopping carts, product catalogs, checkout flows\n- 🤖 **AI/ML Dashboards** — Model monitoring, prediction visualizations, analytics\n- 📊 **Data Visualization** — 2D/3D charts, flow diagrams, real-time analytics\n\n### Core Capabilities\n\n| Capability | Description |\n|------------|-------------|\n| **MVVM Architecture** | Strict Model-View-ViewModel with Zod + React Hooks |\n| **40 Component Categories** | Buttons to 3D charts with Three.js |\n| **Runtime Validation** | Zod 4 schemas with TypeScript inference |\n| **Visual Modes** | Normal, Neon, Minimal, Glass |\n| **100% Test Coverage** | 258 tests, scenario-based testing |\n| **NPM Published** | ark-alliance-react-ui v1.1.2 |\n| **Type Safety** | End-to-end TypeScript 5.9 strict mode |\n| **Accessibility** | ARIA labels, keyboard navigation, focus management |\n\n### Technology Stack\n\n**Core**\n- React 19 with hooks (useState, useCallback, useRef, forwardRef)\n- TypeScript 5.9 strict mode\n- Tailwind CSS 4 for utility-first styling\n- Zod 4 for schema validation and runtime safety\n\n**Build & Test**\n- Vite 7 for fast builds and dev server (port 5090)\n- Vitest 2.1.8 for unit testing\n- React Testing Library 16.1.0 for component tests\n- GitHub Actions CI/CD (Node 18.x, 20.x, 22.x)\n\n**Advanced**\n- Three.js 0.182 for 3D chart components\n- @react-three/fiber 9.4 and drei 10.7\n- React Markdown 10.1 for document rendering\n- Mermaid 11.12 for diagram rendering\n- FontAwesome 7.1 for icons\n\n### Key Statistics\n\n- **40** Component Categories\n- **258** Tests (100% pass rate)\n- **6** Input Variants\n- **5** Gauge Types\n- **4** Visual Modes\n- **10+** Industry Domains\n- **v1.1.2** Current Version\n\n### NPM Installation\n\n```bash\nnpm install ark-alliance-react-ui\n```\n\n### Quick Start\n\n```typescript\nimport { NeonButton, GlowCard, CircularGauge } from 'ark-alliance-react-ui';\nimport 'ark-alliance-react-ui/styles';\n\nfunction App() {\n return (\n
\n console.log('Clicked!')}\n >\n Execute Trade\n \n \n \n

5 open positions

\n

Total P&L: +$1,234.56

\n \n \n \n
\n );\n}\n```\n\n### Interactive Showcase\n\nThe component explorer runs at **localhost:5090**:\n\n```bash\ncd Ark.Alliance.React.Component.UI\nnpm install\nnpm run dev\n```\n\n**Features**: Sidebar navigation, live preview, property editor, style presets, code export, responsive testing." + }, + { + "title": "MVVM Architecture & Design", + "type": "ARCHITECTURE", + "navOrder": 2, + "content": "## MVVM Architecture Pattern\n\n### Architecture Flow\n\n```mermaid\nflowchart TB\n subgraph View[\"👁️ View Layer (*.tsx)\"]\n UI[\"React Component
• forwardRef for ref forwarding
• memo for optimization
• Pure presentation logic
• Renders based on ViewModel state\"]\n end\n \n subgraph ViewModel[\"🔄 ViewModel Layer (*.viewmodel.ts)\"]\n Hook[\"Custom React Hook
• useState for state management
• useCallback for event handlers
• Business logic and validation
• Returns {model, state, handlers}\"]\n end\n \n subgraph Model[\"📋 Model Layer (*.model.ts)\"]\n Schema[\"Zod Schema
• Runtime type validation
• TypeScript type inference
• Default value factories
• Extends BaseComponentModel\"]\n end\n \n subgraph Core[\"🏗️ Core Infrastructure\"]\n BaseVM[\"BaseViewModel
Lifecycle management\"]\n BaseModel[\"BaseComponentModel
Common props\"]\n EventBus[\"Event Bus
Cross-component communication\"]\n Validators[\"Input Validators
Pattern matching, constraints\"]\n end\n \n UI -->|\"Uses Hook\"| Hook\n Hook -->|\"Extends Schema\"| Schema\n UI -.->|\"Type-Safe Props\"| Schema\n \n Hook -->|\"State Updates\"| UI\n UI -->|\"User Events\"| Hook\n Schema -->|\"Validated Data\"| Hook\n \n BaseVM -.-> Hook\n BaseModel -.-> Schema\n EventBus -.-> Hook\n Validators -.-> Hook\n \n style View fill:#1e293b,stroke:#3b82f6,stroke-width:3px,color:#fff\n style ViewModel fill:#1e293b,stroke:#10b981,stroke-width:3px,color:#fff\n style Model fill:#1e293b,stroke:#f59e0b,stroke-width:3px,color:#fff\n style Core fill:#1e293b,stroke:#8b5cf6,stroke-width:2px,color:#fff\n```\n\n### Component Hierarchy\n\n```mermaid\ngraph TD\n Base[\"BaseComponentModel
#128467;id, disabled, loading,
className, testId, ariaLabel\"]\n \n Base --> FormInput[\"FormInputModel
#128467;value, onChange, validation,
pattern, minLength, maxLength\"]\n Base --> Display[\"Display Components
#128467;visual modes, status, variants\"]\n Base --> Layout[\"Layout Components
#128467;slots, children, positioning\"]\n Base --> Advanced[\"Advanced Components
#128467;3D, data processing, complex UI\"]\n \n FormInput --> Input[\"Input
#128467;text, email, password, tel, url, number\"]\n FormInput --> Select[\"Select
#128467;options, multi-select, searchable\"]\n FormInput --> TextArea[\"TextArea
#128467;multi-line text with auto-resize\"]\n FormInput --> Slider[\"Slider
#128467;range input with step control\"]\n FormInput --> Numeric[\"NumericInput
#128467;number formatting, locale support\"]\n FormInput --> FileUpload[\"FileUpload
#128467;drag-n-drop, multi-file\"]\n \n Display --> Button[\"NeonButton
#128467;4 variants, 3 sizes, visual modes\"]\n Display --> Card[\"GlowCard
#128467;status-based styling, hover effects\"]\n Display --> Gauge[\"Gauges
#128467;CircularGauge, SpeedometerGauge
DigitalGauge, BatteryGauge
SignalBarsGauge\"]\n Display --> Progress[\"ProgressBar
#128467;linear, animated, color variants\"]\n Display --> Badge[\"StatusBadge
#128467;pulse animations, color coding\"]\n \n Layout --> Panel[\"Panel
#128467;container with header/footer slots\"]\n Layout --> GenericPanel[\"GenericPanel
#128467;glassmorphism, gradients, overlays\"]\n Layout --> Header[\"Header
#128467;search, icons, backgrounds\"]\n Layout --> Footer[\"Footer
#128467;paging controls, slots\"]\n Layout --> Sidebar[\"SideBarMenu
#128467;category navigation, collapsible\"]\n \n Advanced --> Chart3D[\"Chart3D
#128467;Cuboid, Cylinder, Bubble, Candle
Three.js powered\"]\n Advanced --> DataGrid[\"DataGrid
#128467;sortable, filterable, paginated\"]\n Advanced --> TradingCard[\"TradingGridCard
#128467;real-time data, status indicators\"]\n Advanced --> Modal[\"Modal
#128467;portal, focus trap, escape handling\"]\n Advanced --> Timeline[\"Timeline
#128467;event visualization, customizable\"]\n Advanced --> Markdown[\"MarkdownRenderer
#128467;syntax highlighting, Mermaid support\"]\n \n style Base fill:#f59e0b,stroke:#d97706,stroke-width:4px,color:#000\n style FormInput fill:#3b82f6,stroke:#2563eb,stroke-width:3px,color:#fff\n style Display fill:#10b981,stroke:#059669,stroke-width:3px,color:#fff\n style Layout fill:#8b5cf6,stroke:#7c3aed,stroke-width:3px,color:#fff\n style Advanced fill:#ef4444,stroke:#dc2626,stroke-width:3px,color:#fff\n```\n\n### Core Infrastructure\n\n**BaseComponentModel** (`src/core/base/BaseComponentModel.ts`):\n```typescript\nexport const BaseComponentModelSchema = z.object({\n id: z.string().optional(),\n disabled: z.boolean().default(false),\n loading: z.boolean().default(false),\n className: z.string().optional(),\n testId: z.string().optional(),\n ariaLabel: z.string().optional()\n});\n\nexport type BaseComponentModel = z.infer;\n```\n\n**FormInputModel** (`src/core/base/FormInputModel.ts`):\n```typescript\nexport const FormInputModelSchema = BaseComponentModelSchema.extend({\n value: z.string().default(''),\n onChange: z.function().args(z.string()).returns(z.void()).optional(),\n pattern: z.string().optional(),\n minLength: z.number().optional(),\n maxLength: z.number().optional(),\n required: z.boolean().default(false)\n});\n```\n\n**ViewModel Hook Pattern**:\n```typescript\nexport function useButtonViewModel(props: ButtonModel) {\n const [isHovered, setIsHovered] = useState(false);\n const [isPressed, setIsPressed] = useState(false);\n \n const handleClick = useCallback((e: React.MouseEvent) => {\n if (props.disabled || props.loading) return;\n props.onClick?.(e);\n }, [props.disabled, props.loading, props.onClick]);\n \n return {\n model: props,\n state: { isHovered, isPressed },\n handlers: {\n onClick: handleClick,\n onMouseEnter: () => setIsHovered(true),\n onMouseLeave: () => setIsHovered(false)\n }\n };\n}\n```\n\n**View Component Pattern**:\n```typescript\nexport const NeonButton = forwardRef(\n (props, ref) => {\n const { model, state, handlers } = useButtonViewModel(props);\n \n return (\n \n {model.loading ? : model.children}\n \n );\n }\n);\n```\n\n### Input Validation System\n\nThe library includes comprehensive validators in `src/Helpers/Validators/`:\n\n- **Email Validation**: `validateEmail(value)` with RFC 5322 compliance\n- **Text Validation**: `validateText(value, options)` for length and pattern constraints\n- **Pattern Restrictions**: Numeric, alphanumeric, custom regex via `useFormInputRestrictions`\n\n### Benefits\n\n- ✅ **Testability**: Test ViewModel logic independently from UI\n- ✅ **Type Safety**: Zod runtime validation + TypeScript compile-time checks\n- ✅ **Maintainability**: Clear separation of concerns\n- ✅ **Reusability**: Share ViewModels across multiple View implementations\n- ✅ **Scalability**: Easy to add new components following established pattern" + }, + { + "title": "Component Catalog & Usage", + "type": "FUNCTIONAL", + "navOrder": 3, + "content": "## Component Library\n\n### Primitive Components\n\n#### Buttons\n\n**NeonButton** - Premium button with glow effects\n```typescript\nimport { NeonButton } from 'ark-alliance-react-ui';\n\n alert('Clicked!')}\n>\n Execute Trade\n\n```\n\n**Variants**: primary (blue), secondary (gray), success (green), danger (red) \n**Sizes**: sm (32px), md (40px), lg (48px) \n**Visual Modes**: normal, neon, minimal, glass\n\n---\n\n#### Cards\n\n**GlowCard** - Status-based card with hover effects\n```typescript\nimport { GlowCard } from 'ark-alliance-react-ui';\n\n\n

5 open positions

\n

Total P&L: +$1,234.56

\n\n```\n\n**TradingGridCard** - Real-time trading data display\n```typescript\nimport { TradingGridCard } from 'ark-alliance-react-ui';\n\n navigateToDetails('BTC')}\n/>\n```\n\n---\n\n#### Gauges (5 Types)\n\n**1. CircularGauge** - Circular progress with gradient\n```typescript\n\n```\n\n**2. SpeedometerGauge** - Speedometer-style gauge with needle \n**3. DigitalGauge** - Digital LED-style numeric display \n**4. BatteryGauge** - Battery level indicator with charge animation \n**5. SignalBarsGauge** - Signal strength bars (1-5 levels)\n\n---\n\n#### Input Components (6 Variants)\n\n**BaseInput** - Foundation for all inputs\n```typescript\nimport { Input } from 'ark-alliance-react-ui';\n\n setValue(val)}\n size=\"medium\" // small, medium, large\n placeholder=\"Enter text\"\n required\n minLength={3}\n maxLength={50}\n pattern=\"^[a-zA-Z]+$\"\n error={errorMessage}\n disabled={false}\n readOnly={false}\n fullWidth\n/>\n```\n\n**Other Input Types**:\n- **Select**: Dropdown with single/multi-select\n- **TextArea**: Multi-line text with auto-resize\n- **Slider**: Range input with step control\n- **NumericInput**: Number formatting with locale support\n- **FileUpload**: Drag-and-drop with multi-file support\n\n---\n\n### Advanced Components\n\n#### Chart3D - Three.js 3D Visualization\n```typescript\nimport { Chart3D } from 'ark-alliance-react-ui';\n\n\n```\n\n**Types**: Cuboid (bar chart), Cylinder (volume), Bubble (scatter), Candle (OHLC candlesticks)\n\n---\n\n#### DataGrid - Sortable, Filterable Table\n```typescript\nimport { DataGrid } from 'ark-alliance-react-ui';\n\n sortData(field, direction)}\n onRowClick={(row) => viewDetails(row)}\n pageSize={20}\n enablePagination\n/>\n```\n\n---\n\n#### MarkdownRenderer - Document Rendering\n```typescript\nimport { MarkdownRenderer } from 'ark-alliance-react-ui';\n\n\n```\n\n---\n\n### Visual Modes\n\nAll components support 4 visual modes via the `visualMode` prop:\n\n#### 1. Normal Mode (Default)\nStandard appearance for general-purpose applications.\n\n#### 2. Neon Mode\n```typescript\nGlowing Button\n```\nGlowing borders and gradient effects—perfect for trading dashboards and gaming UIs.\n\n#### 3. Minimal Mode\n```typescript\nClean Card\n```\nReduced visual weight for clean, minimalist interfaces.\n\n#### 4. Glass Mode\n```typescript\nPremium Panel\n```\nGlassmorphism with backdrop blur for modern, premium UIs.\n\n---\n\n### Multi-Domain Components\n\n| Domain | Components | Description |\n|--------|------------|-------------|\n| Finance/Trading | TradingGridCard, Chart3D (Candle), CircularGauge | Real-time market data, P&L tracking |\n| Healthcare | Medical category (planned) | Patient portals, health monitoring |\n| Logistics | Logistic category (planned) | Tracking, inventory, warehouse |\n| E-Commerce | Basket, Catalogue, PaymentsForm (planned) | Shopping carts, catalogs, checkout |\n| AI/ML | Ia category (planned) | Model monitoring, predictions |\n| Social | Chat, SocialMedia (planned) | Messaging, feeds, communities |\n\n---\n\n### Component Status\n\n✅ **Implemented** (20 categories): Buttons, Cards, Gauges, Input, Charts, Chart3D, Grids, Modal, Panel, ProgressBar, Header, Footer, SideBar, Documents, TimeLines, Tooltip, Icon, Label, Page, Slides\n\n🔄 **Planned** (20 categories): Basket, Calendars, Catalogue, Chat, DatePicker, Diagram, Finance, FlowChart, Ia, Login, Logistic, Medical, Menu, Music, PaymentsForm, SocialMedia, Sound, Video, Viewers, and more" + }, + { + "title": "Testing & Integration", + "type": "TECHNICAL", + "navOrder": 4, + "content": "## Test Suite\n\n### Test Statistics\n\n**Total**: 258 tests ✅ 100% pass rate \n**Framework**: Vitest 2.1.8 \n**Library**: @testing-library/react 16.1.0 \n**DOM Environment**: jsdom 25.0.1 \n**Duration**: 10.86s (average) \n**Test Files**: 14\n\n### Test Breakdown by Component\n\n| Component Family | Tests | Coverage Areas |\n|------------------|-------|----------------|\n| BaseInput | 35 | Input types (text, email, password, number, tel, url), validation, focus/blur, size variants, disabled/loading/error states, HTML5 validation |\n| ProgressBar | 28 | Percentage calculations (0%, 50%, 100%), value clamping, color schemes (blue, green, red, cyan,yellow, purple), display modes (showValue, showPercentage), animation triggers |\n| GenericPanel | 25 | Panel layouts, visual modes (normal/neon/glass/minimal), header/footer slots, glassmorphism, gradients, grid overlays, glow effects, customization options |\n| Tooltip | 24 | Positioning logic (top/bottom/left/right), delay timing (300ms, 500ms, 1000ms), HOC pattern (withTooltip), ref forwarding, null ref handling, content variants |\n| TradingGridCard | 23 | Status variants (idle, success, warning, error, info), Zod model validation, schema parsing defaults, theme support (dark/light), click interactions, Enter/Space key support |\n| FormInputModel | 21 | Base schema validation, defaults, Zod parsing, type inference |\n| Button | 20 | Click handling, event propagation, variants (primary/secondary/danger/success), sizes (sm/md/lg), states (disabled/loading), ARIA attributes, keyboard navigation |\n| FAIcon | 16 | FontAwesome icon rendering, size variants, color customization |\n| useFormInputRestrictions | 13 | Pattern matching (numeric, alphanumeric), length limits (min/max), character filtering |\n| Input | 12 | Value changes, onChange events, event object structure |\n| Page | 12 | Page component rendering, layout slots |\n| Card | 11 | Card variants, status-based styling |\n| BaseComponentModel | 10 | Base model schema, common props validation |\n| Panel | 8 | Container layouts, header/footer integration |\n\n### Running Tests\n\n```bash\n# Navigate to test project\ncd Ark.Alliance.React.Component.UI.Tests\n\n# Install dependencies\nnpm install\n\n# Run all tests (watch mode)\nnpm test\n\n# Run tests once (CI mode)\nnpm test -- --run\n\n# Run specific test file\nnpm test -- components/Button/Button.test.tsx\n\n# Run with coverage\nnpm test -- --coverage\n```\n\n### Test Architecture: ComponentTestEngine\n\nThe test suite uses a **scenario-driven architecture** powered by `ComponentTestEngine`:\n\n```typescript\nimport { ComponentTestEngine } from '../fixtures/ComponentTestEngine';\n\nconst engine = new ComponentTestEngine();\nengine.registerComponent('MyComponent', MyComponent);\n\nconst scenario = {\n name: 'Should handle click',\n component: 'MyComponent',\n props: { onClick: vi.fn() },\n actions: [{ type: 'click' }],\n assertions: [{ callback: 'onClick', callCount: 1 }]\n};\n\nengine.executeScenario(scenario);\n```\n\n**Engine Features**: Component registration, mock function creation (vi.fn()), user action simulation (click, type, hover, focus, blur), result validation, consistent reusable patterns.\n\n---\n\n## NPM Integration\n\n### Installation\n\n```bash\nnpm install ark-alliance-react-ui\n```\n\n**Package Details**:\n- **Name**: ark-alliance-react-ui\n- **Version**: 1.1.2\n- **Main**: ./dist/index.js (CommonJS)\n- **Module**: ./dist/index.mjs (ES Module)\n- **Types**: ./dist/index.d.ts (TypeScript declarations)\n- **Styles**: ark-alliance-react-ui/styles\n\n### Basic Setup\n\n**1. Import Styles** (in your main `App.tsx` or `index.tsx`):\n```typescript\nimport 'ark-alliance-react-ui/styles';\n```\n\n**2. Import Components**:\n```typescript\nimport { \n NeonButton, \n GlowCard, \n CircularGauge,\n DataGrid,\n Modal,\n TradingGridCard\n} from 'ark-alliance-react-ui';\n```\n\n**3. Use Components**:\n```typescript\nfunction App() {\n return (\n
\n \n Get Started\n \n \n
\n );\n}\n```\n\n---\n\n### Framework Integration\n\n#### Next.js (App Router)\n\n**app/layout.tsx**:\n```typescript\nimport 'ark-alliance-react-ui/styles';\n\nexport default function RootLayout({ children }) {\n return (\n \n {children}\n \n );\n}\n```\n\n#### Vite + React\n\n**src/main.tsx**:\n```typescript\nimport React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App';\nimport 'ark-alliance-react-ui/styles';\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n \n \n \n);\n```\n\n#### Create React App\n\n**src/index.tsx**:\n```typescript\nimport React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App';\nimport 'ark-alliance-react-ui/styles';\n\nconst root = ReactDOM.createRoot(\n document.getElementById('root') as HTMLElement\n);\nroot.render(\n \n \n \n);\n```\n\n---\n\n### Best Practices\n\n#### 1. Tree Shaking\n\nImport only what you need to reduce bundle size:\n```typescript\n// ✅ Good - imports only NeonButton\nimport { NeonButton } from 'ark-alliance-react-ui';\n\n// ❌ Avoid - imports entire library\nimport * as ArkUI from 'ark-alliance-react-ui';\n```\n\n#### 2. Type Safety\n\nLeverage TypeScript for compile-time safety:\n```typescript\nimport type { ButtonModel, GaugeModel } from 'ark-alliance-react-ui';\n\nconst buttonProps: ButtonModel = {\n variant: 'primary', // TypeScript validates this\n size: 'lg',\n onClick: () => {}\n};\n```\n\n#### 3. Accessibility\n\nAlways provide ARIA labels:\n```typescript\n\n Buy BTC\n\n```\n\n#### 4. Visual Consistency\n\nUse visual modes consistently across your app:\n```typescript\n// Define app-wide visual mode\nconst VISUAL_MODE = 'neon'; // or 'normal', 'minimal', 'glass'\n\nButton\nCard\n```\n\n---\n\n## CI/CD Pipeline\n\n### GitHub Actions\n\nThe project uses GitHub Actions for continuous integration:\n\n- **Multi-version Testing**: Node.js 18.x, 20.x, 22.x\n- **Automated Builds**: Triggered on push/PR to master\n- **Dependency Caching**: npm cache for faster builds\n- **Build Verification**: Production builds validated\n- **Test Execution**: All 258 tests run on every commit\n\n### Build Commands\n\n```bash\n# Development server (localhost:5090)\nnpm run dev\n\n# Production build (application)\nnpm run build\n\n# Library build (publishable package)\nnpm run build:lib\n\n# Lint code\nnpm run lint\n```" + } + ] +} \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/trading-providers-lib.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/trading-providers-lib.json new file mode 100644 index 0000000000000000000000000000000000000000..44e32e90a3eb30b11520b8753daa6f85b8593afe --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/trading-providers-lib.json @@ -0,0 +1,84 @@ +{ + "title": "Ark.Alliance.Trading.Providers.Lib", + "description": "Production-ready TypeScript SDK (v1.1.0) unifying cryptocurrency trading across Binance Futures, Deribit, and Kraken Futures with a single, elegant API. Features Result pattern for type-safe error handling, event-driven async architecture, WebSocket streams for low-latency market data, HMAC-SHA256 and Ed25519 authentication, 100+ test scenarios with 100% pass rate, and comprehensive documentation. Published on NPM.", + "status": "Featured", + "imageUrl": "/Assets/Projects/Ark.Alliance.Trading.Providers.Lib/trading-hero.png", + "repoUrl": "https://github.com/ArmandRicheletKleinberg/Ark.Alliance.Trading.Providers.Lib", + "demoUrl": "", + "packageUrl": "https://www.npmjs.com/package/ark-alliance-trading-providers-lib", + "technologies": [ + "typescript", + "nodejs", + "binance", + "deribit", + "kraken", + "rest", + "websocket", + "jest", + "docker" + ], + "features": [ + { + "title": "Multi-Provider Abstraction", + "description": "Unified interface for Binance Futures, Deribit, and Kraken Futures exchanges with IProviderClient pattern. Write once, trade on any supported exchange. Extensible architecture for adding new providers following the same abstraction layer.", + "icon": "plug" + }, + { + "title": "Result Pattern Error Handling", + "description": "Type-safe Result pattern for functional error handling. No exceptions in normal flow, explicit success/failure states, chainable operations (map, flatMap), and comprehensive error details with ResultStatus enum across all providers.", + "icon": "check-circle" + }, + { + "title": "Real-Time WebSocket Streams", + "description": "Low-latency market data via WebSocket connections for all providers. Subscribe to klines, book ticker, user data streams, and order events. Event-driven architecture with typed event emitters for order fills and position updates.", + "icon": "wifi" + }, + { + "title": "Secure Authentication", + "description": "HMAC-SHA256 signature generation for Binance and Kraken, Ed25519 signature generation for Deribit, automatic token refresh mechanisms, and secure credential storage with IAuthStrategy interface.", + "icon": "lock" + }, + { + "title": "Comprehensive Testing", + "description": "100+ test scenarios across 3 providers with Jest. Scenario-driven JSON test definitions, dynamic parameter resolution, tested against live testnets (Binance, Deribit, Kraken), and 100% pass rate across all categories.", + "icon": "vial" + }, + { + "title": "Clean Architecture", + "description": "Domain-driven design with clear layer separation: Domain (entities, interfaces), Application (use cases, mappers), and Infrastructure (clients, auth). TypeScript-first with full IntelliSense support and strict type checking.", + "icon": "sitemap" + } + ], + "pages": [ + { + "title": "Overview", + "type": "OVERVIEW", + "navOrder": 1, + "content": "## Ark Alliance Trading Providers Library\n\n**Production-Ready Multi-Provider Cryptocurrency Trading SDK** | v1.1.0\n\nA TypeScript SDK that unifies cryptocurrency trading across multiple exchanges with a single, elegant API. Stop writing exchange-specific code—write once, trade everywhere.\n\n### Perfect For\n\n- 🤖 **Algorithmic trading bots**\n- 📊 **Market data aggregators**\n- 💼 **Portfolio management systems**\n- 📈 **Trading analytics platforms**\n\n### Supported Providers\n\n| Provider | REST | WebSocket | Auth | Status |\n|:---------|:----:|:---------:|:----:|:------:|\n| **Binance Futures** | ✅ | ✅ | HMAC-SHA256 | ✅ Complete |\n| **Deribit** | ✅ | ✅ | Ed25519 | ✅ Complete |\n| **Kraken Futures** | ✅ | ✅ | HMAC-SHA256 | ✅ Complete |\n\n### Key Features\n\n| Feature | Description |\n|:--------|:------------|\n| **Multi-Provider** | Unified interface for 3 major exchanges |\n| **Order Management** | Place, modify, cancel, and track orders |\n| **Position Tracking** | Real-time position monitoring with P&L |\n| **WebSocket Streams** | Low-latency market data and user events |\n| **Event-Driven** | Async event architecture for fills, updates |\n| **Result Pattern** | Type-safe error handling |\n| **Secure Auth** | HMAC-SHA256 + Ed25519 signatures |\n| **100+ Tests** | Scenario-driven, 100% pass rate |\n| **TypeScript-First** | Full type definitions with IntelliSense |\n| **Testnet Support** | Built-in testnet URLs for all providers |\n\n### Installation\n\n```bash\nnpm install ark-alliance-trading-providers-lib\n```\n\n### Quick Start\n\n```typescript\nimport { BinanceRestClient } from 'ark-alliance-trading-providers-lib/Binance';\nimport { DeribitTradingService } from 'ark-alliance-trading-providers-lib/Deribit';\nimport { KrakenRestClient } from 'ark-alliance-trading-providers-lib/Kraken';\n\n// Binance Example\nconst binance = new BinanceRestClient(apiKey, secret, { testnet: true });\nconst order = await binance.placeOrder({\n symbol: 'BTCUSDT',\n side: 'BUY',\n type: 'MARKET',\n quantity: 0.001\n});\n\nif (order.success) {\n console.log(`Order placed: ${order.data.orderId}`);\n}\n```" + }, + { + "title": "Architecture", + "type": "ARCHITECTURE", + "navOrder": 2, + "content": "## Clean Architecture\n\n### Layer Structure\n\n```mermaid\ngraph TB\n subgraph DL[\"Domain Layer\"]\n E[\"Domain Entities
(Order, Position, Account)\"]\n I[\"Interfaces
(IProviderClient, ITradingService)\"]\n end\n \n subgraph AL[\"Application Layer\"]\n UC[\"Use Cases
(Place Order, Get Position)\"]\n M[\"Data Mappers
(API ↔ Domain)\"]\n end\n \n subgraph IL[\"Infrastructure Layer\"]\n C[\"API Clients
(REST, WebSocket)\"]\n A[\"Authentication
(HMAC, Ed25519)\"]\n end\n \n subgraph EX[\"External Systems\"]\n B[\"Binance Futures API\"]\n D[\"Deribit API\"]\n K[\"Kraken Futures API\"]\n end\n \n I -.-> C\n UC --> I\n M --> E\n C --> A\n C --> B\n C --> D\n C --> K\n UC --> M\n```\n\n### Provider Architecture\n\n```\nYour Application\n ↓ Uses\nITradingService / IMarketDataService\n ↓ Implemented by\n┌─────────────────┬─────────────────┬─────────────────┐\n│ BinanceService │ DeribitService │ KrakenService │\n│ (REST + WS) │ (JSON-RPC WS) │ (REST + WS) │\n└─────────────────┴─────────────────┴─────────────────┘\n ↓ Uses\nIAuthStrategy\n ↓ Implemented by\n┌─────────────────┬─────────────────┬─────────────────┐\n│ HMAC-SHA256 │ Ed25519 │ HMAC-SHA256 │\n│ (Binance) │ (Deribit) │ (Kraken) │\n└─────────────────┴─────────────────┴─────────────────┘\n ↓ Connects to\n┌─────────────────┬─────────────────┬─────────────────┐\n│ fapi.binance.com│ www.deribit.com │ futures.kraken │\n│ testnet.binance │ test.deribit.com│ demo-futures │\n└─────────────────┴─────────────────┴─────────────────┘\n```\n\n### Directory Structure\n\n```\nsrc/Ark.Alliance.Trading.Providers.Lib/\n├── Src/\n│ ├── Common/\n│ │ ├── Domain/ # IOrder, IPosition, ITrade\n│ │ ├── result.ts # Result pattern\n│ │ └── errors/ # Trading errors\n│ ├── Binance/\n│ │ ├── clients/ # REST + WebSocket\n│ │ ├── services/ # Trading + MarketData\n│ │ ├── mappers/ # API ↔ Domain\n│ │ └── auth/ # HMAC-SHA256\n│ ├── Deribit/\n│ │ ├── clients/ # JSON-RPC Client\n│ │ ├── services/ # Trading + MarketData\n│ │ └── auth/ # Ed25519\n│ └── Kraken/\n│ ├── clients/ # REST + WebSocket\n│ ├── services/ # Trading + MarketData\n│ └── auth/ # HMAC-SHA256\n└── index.ts # Exports\n```" + }, + { + "title": "Providers & Examples", + "type": "FUNCTIONAL", + "navOrder": 3, + "content": "## Provider Details\n\n### Binance Futures\n\n**Features**: Full order lifecycle, position tracking, leverage control, WebSocket streams\n\n```typescript\nimport { BinanceRestClient, BinanceWebSocketClient } from 'ark-alliance-trading-providers-lib/Binance';\n\n// REST Client\nconst rest = new BinanceRestClient(apiKey, secret, { testnet: true });\n\n// Place limit order\nconst order = await rest.placeOrder({\n symbol: 'BTCUSDT',\n side: 'BUY',\n type: 'LIMIT',\n price: 40000,\n quantity: 0.01,\n timeInForce: 'GTC'\n});\n\n// WebSocket for real-time data\nconst ws = new BinanceWebSocketClient({ testnet: true });\nws.subscribeBookTicker('BTCUSDT');\nws.on('bookTicker', (ticker) => console.log(ticker));\n```\n\n---\n\n### Deribit\n\n**Features**: Options + Futures, JSON-RPC over WebSocket, portfolio margin\n\n```typescript\nimport { DeribitTradingService, DeribitEnvironment } from 'ark-alliance-trading-providers-lib/Deribit';\n\nconst service = new DeribitTradingService({\n apiKey: clientId,\n apiSecret: clientSecret,\n environment: DeribitEnvironment.TESTNET\n});\n\nawait service.connect();\n\n// Place order\nconst order = await service.placeOrder({\n instrument: 'BTC-PERPETUAL',\n side: 'BUY',\n type: 'LIMIT',\n quantity: '100',\n price: '40000'\n});\n\n// Event callbacks\nservice.onOrderUpdate((o) => console.log('Order:', o));\nservice.onPositionUpdate((p) => console.log('Position:', p));\n```\n\n---\n\n### Kraken Futures\n\n**Features**: REST + WebSocket, market/limit/stop orders, position management\n\n```typescript\nimport { KrakenRestClient, KrakenWebSocketClient } from 'ark-alliance-trading-providers-lib/Kraken';\n\n// REST Client\nconst client = new KrakenRestClient({\n apiKey: key,\n apiSecret: secret,\n testnet: true\n});\n\n// Get open positions\nconst positions = await client.getOpenPositions();\n\n// WebSocket for real-time data\nconst ws = new KrakenWebSocketClient({ testnet: true });\nawait ws.connect();\nws.subscribeTicker(['PI_XBTUSD']);\nws.on('ticker', (t) => console.log('Ticker:', t));\n```\n\n---\n\n### Multi-Provider Portfolio\n\n```typescript\nclass MultiExchangePortfolio {\n async getTotalValue(): Promise {\n const [binance, deribit, kraken] = await Promise.all([\n this.binanceClient.getPositions(),\n this.deribitClient.getPositions(),\n this.krakenClient.getOpenPositions()\n ]);\n \n return this.calculateTotalValue([\n ...binance.data || [],\n ...deribit.data || [],\n ...kraken.data || []\n ]);\n }\n}\n```" + }, + { + "title": "Testing & Quality", + "type": "TECHNICAL", + "navOrder": 4, + "content": "## Test Suite\n\n### Test Coverage Summary\n\n| Provider | Scenarios | Tests | Pass Rate |\n|:---------|:---------:|:-----:|:---------:|\n| **Binance** | 8 files | 70+ | ✅ 100% |\n| **Deribit** | 3 files | 48 | ✅ 100% |\n| **Kraken** | 2 files | 29 | ✅ 100% |\n| **Total** | **13 files** | **100+** | ✅ **100%** |\n\n### Binance Scenarios\n\n| File | Category | Scenarios |\n|:-----|:---------|:---------:|\n| `account.scenarios.json` | Account | 8 |\n| `market-data.scenarios.json` | Market Data | 8 |\n| `orders.scenarios.json` | Orders | 12 |\n| `positions.scenarios.json` | Positions | 14 |\n| `gtx-orders.scenarios.json` | Post-Only | 13 |\n| `market-orders.scenarios.json` | Market Orders | 8 |\n| `algo-orders.scenarios.json` | Algo Orders | 10 |\n| `mixed-orders.scenarios.json` | Mixed | 10 |\n\n### Deribit Scenarios\n\n| File | Category | Scenarios |\n|:-----|:---------|:---------:|\n| `market-data.scenarios.json` | Market Data | 8 |\n| `user-data.scenarios.json` | User Data | 12 |\n| `trading.scenarios.json` | Trading | 14 |\n\n### Kraken Scenarios\n\n| File | Category | Scenarios |\n|:-----|:---------|:---------:|\n| `market-data.scenarios.json` | REST | 14 |\n| `streaming.scenarios.json` | WebSocket | 15 |\n\n---\n\n### Running Tests\n\n```bash\ncd src/Ark.Alliance.Trading.Providers.Lib.Test\n\n# Run all tests\nnpm test\n\n# Run by provider\nnpm test -- --testPathPattern=Binance\nnpm test -- --testPathPattern=Deribit\nnpm test -- --testPathPattern=Kraken\n\n# With coverage\nnpm run test:coverage\n```\n\n---\n\n### Quality Metrics\n\n- **TypeScript**: Strict mode enabled\n- **Code Coverage**: 100% pass rate\n- **Documentation**: Microsoft style code comments\n- **Architecture**: Clean Architecture + SOLID principles\n- **Error Handling**: Result pattern throughout" + }, + { + "title": "Roadmap", + "type": "ROADMAP", + "navOrder": 5, + "content": "## Development Roadmap\n\n### ✅ Completed (Q4 2025 - Q1 2026)\n\n| Milestone | Status | Completion |\n|:----------|:------:|:----------:|\n| Binance Futures Provider | ✅ Complete | Oct 2025 |\n| Deribit Provider (Market Data) | ✅ Complete | Nov 2025 |\n| Deribit User Data & Trading | ✅ Complete | Jan 2026 |\n| Kraken Futures Provider | ✅ Complete | Jan 2026 |\n| npm Package v1.1.0 | ✅ Published | Jan 2026 |\n\n---\n\n### 🎯 Current Progress\n\n```mermaid\ngantt\n title Library Development Timeline\n dateFormat YYYY-MM\n section Core\n Binance Provider :done, 2025-10, 2025-11\n Deribit Provider :done, 2025-11, 2026-01\n Kraken Provider :done, 2026-01, 2026-01\n section Planned\n OKX Provider :active, 2026-02, 2026-02\n Bybit Provider : 2026-03, 2026-03\n v2.0.0 Release : 2026-03, 2026-03\n```\n\n---\n\n### 📋 Planned (Q1-Q2 2026)\n\n| Milestone | Target | Status |\n|:----------|:------:|:------:|\n| OKX Provider | Feb 2026 | 📋 Planned |\n| Bybit Provider | Mar 2026 | 📋 Planned |\n| Library v2.0.0 | Mar 2026 | 📋 Planned |\n| Unified Order Book | Q2 2026 | 📋 Research |\n| Cross-Exchange Arbitrage | Q2 2026 | 📋 Research |\n\n---\n\n### Version History\n\n| Version | Date | Changes |\n|:--------|:----:|:--------|\n| v1.1.0 | Jan 2026 | + Kraken Futures, + Deribit Trading, 100+ tests |\n| v1.0.0 | Dec 2025 | Initial release: Binance + Deribit Market Data |" + } + ] +} \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/trends-calculator.json b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/trends-calculator.json new file mode 100644 index 0000000000000000000000000000000000000000..dc0be1c4186f76bb617eef6d67ff2475ecc8e33c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/ProjectData/trends-calculator.json @@ -0,0 +1,78 @@ +{ + "title": "Ark.Alliance.Trading.TrendsCalculator", + "description": "Production-ready real-time cryptocurrency trend analysis microservice combining advanced statistical indicators (Hurst Exponent, GARCH, Linear Regression, EMA) with optional Google Gemini AI integration. Features WebSocket streaming, configurable thresholds, walk-forward validation, and seamless integration with distributed trading systems. Built with TypeScript, React 19, Express, and Socket.IO.", + "status": "Featured", + "imageUrl": "/Assets/Projects/Ark.Alliance.TrendsCalculator/trends-hero.png", + "repoUrl": "https://github.com/M2H-Machine-to-Human-Race/Ark.Alliance.Trading.TrendsCalculator", + "demoUrl": "", + "packageUrl": "", + "technologies": [ + "react", + "typescript", + "nodejs", + "express", + "socketio", + "vite", + "zod", + "binance", + "gemini" + ], + "features": [ + { + "title": "Real-Time Trend Analysis", + "description": "Sub-second WebSocket streaming of trend calculations with composite scoring from multiple mathematical indicators. Maintains rolling buffer of 200 price points per symbol for accurate statistical analysis.", + "icon": "chart-line" + }, + { + "title": "Multi-Indicator Engine", + "description": "Combines Hurst Exponent (market persistence), GARCH (volatility forecasting), Linear Regression (trend slope/R²), and EMA crossovers (50/200) into a unified composite score for LONG/SHORT/WAIT decisions.", + "icon": "calculator" + }, + { + "title": "AI-Augmented Insights", + "description": "Optional Google Gemini integration provides contextual analysis and confidence scoring. Mathematical indicators ground AI recommendations in quantitative data for informed trading decisions.", + "icon": "brain" + }, + { + "title": "Distributed Trading Integration", + "description": "REST API and WebSocket events designed for seamless integration with trading bots, portfolio managers, and analytics platforms. Event-driven architecture enables real-time signal consumption.", + "icon": "network-wired" + }, + { + "title": "Walk-Forward Validation", + "description": "Training mode for backtesting and parameter optimization. Evaluate strategy performance across historical data with accuracy metrics per direction (LONG/SHORT/WAIT).", + "icon": "graduation-cap" + }, + { + "title": "MVVM Architecture", + "description": "Clean separation of concerns with Model-View-ViewModel pattern in React frontend. Zod-validated DTOs ensure type safety across the entire stack from backend to UI.", + "icon": "layer-group" + } + ], + "pages": [ + { + "title": "Overview", + "type": "OVERVIEW", + "navOrder": 1, + "content": "## Ark.Alliance.Trading.TrendsCalculator\n\n**Real-Time Cryptocurrency Trend Analysis Microservice**\n\nA production-grade service that transforms raw market data into actionable trend intelligence. It answers one critical question for trading systems: **Should I go LONG, SHORT, or WAIT?**\n\n### Core Capabilities\n\n| Capability | Description |\n|------------|-------------|\n| **Real-Time Analysis** | Sub-second trend updates via WebSocket |\n| **Multi-Indicator** | Hurst, GARCH, Linear Regression, EMA |\n| **AI Integration** | Optional Gemini for contextual insights |\n| **Training Mode** | Walk-forward validation for optimization |\n| **Type Safety** | Zod-validated DTOs across stack |\n\n### Perfect For\n\n- 🤖 **Algorithmic Trading Bots** — Entry/exit signal generation\n- 📊 **Analytics Platforms** — Multi-symbol trend monitoring\n- 💼 **Portfolio Managers** — Risk assessment and allocation\n- 🔬 **Quant Research** — Strategy backtesting and validation\n\n### Technology Stack\n\n- **Frontend**: React 19, TypeScript, Vite, Socket.IO Client\n- **Backend**: Node.js 18+, Express, Socket.IO Server\n- **Math Engine**: Hurst, GARCH, Linear Regression, EMA\n- **External**: Binance Futures API, Google Gemini AI\n- **Shared**: Zod validation, TypeScript DTOs" + }, + { + "title": "Architecture & Data Model", + "type": "ARCHITECTURE", + "navOrder": 2, + "content": "## System Architecture\n\n### Three-Layer Structure\n\n```\nPresentation Layer (TrendsCalculator.Ui)\n├── React 19 + TypeScript\n├── MVVM Component Pattern\n├── Socket.IO Real-Time\n└── Vite Build Tool\n ↓\nShared Layer (TrendsCalculator.Share)\n├── 23 Data Transfer Objects\n├── 12 Enumerations\n└── Zod Validation Schemas\n ↓\nApplication Layer (TrendsCalculator.Backend)\n├── Express REST API\n├── Socket.IO WebSocket\n├── 5 Core Services\n└── Mathematical Engine\n ↓\nExternal Systems\n├── Binance Futures (Market Data)\n└── Google Gemini (AI Analysis)\n```\n\n### Core Services\n\n| Service | Lines | Responsibility |\n|---------|-------|----------------|\n| `TrendCalculatorService` | 751 | Trend calculation engine |\n| `BinanceStreamService` | 290 | Market data ingestion |\n| `SymbolTrackingService` | 220 | Symbol lifecycle |\n| `StreamingAnalysisService` | 330 | Real-time processing |\n| `MarketDataService` | 180 | Data aggregation |\n\n### Mathematical Indicators\n\n**Hurst Exponent** — Market memory analysis\n- H < 0.5: Mean-reverting (range trading)\n- H ≈ 0.5: Random walk (neutral)\n- H > 0.5: Trending (momentum)\n\n**GARCH** — Volatility forecasting\n- Predicts next-period variance\n- Identifies volatility clustering\n\n**Linear Regression** — Trend direction\n- Slope: direction and steepness\n- R²: trend reliability (0-1)\n\n**EMA Crossover** — MA bias\n- EMA 50 > EMA 200: Bullish\n- EMA 50 < EMA 200: Bearish" + }, + { + "title": "Features & Use Cases", + "type": "FUNCTIONAL", + "navOrder": 3, + "content": "## Functional Use Cases\n\n### 1. Algorithmic Trading Bot Integration\n\n**Scenario**: Trading bot needs trend signals for entry timing\n\n**Workflow**:\n1. Bot connects via WebSocket\n2. Subscribes to symbol (e.g., BTCUSDT)\n3. Receives `buffer:progress` events during warmup\n4. Receives `trend:update` events with direction + strength\n5. Executes trades based on composite score threshold\n\n**Benefits**:\n- Sub-second latency\n- Confidence scoring\n- Multiple indicator confirmation\n\n### 2. Multi-Symbol Portfolio Screening\n\n**Scenario**: Analyst monitors 20 symbols for strong trends\n\n**Workflow**:\n1. Track multiple symbols via REST API\n2. Receive parallel WebSocket updates\n3. Filter by strength > 0.7\n4. Request AI analysis for top candidates\n5. Make informed allocation decisions\n\n**Benefits**:\n- Parallel processing\n- Unified trend view\n- AI-augmented insights\n\n### 3. Walk-Forward Strategy Validation\n\n**Scenario**: Quant validates indicator parameters\n\n**Workflow**:\n1. Start training session with historical data\n2. System splits into train/test periods\n3. Parameters optimized on training data\n4. Validated against test data\n5. Accuracy metrics reported per direction\n\n**Benefits**:\n- Avoid overfitting\n- Direction-specific accuracy\n- Parameter optimization\n\n### 4. Real-Time Dashboard Monitoring\n\n**UI Pages**:\n- **Overview**: Summary metrics and active symbols\n- **Symbols**: Add/remove tracking with buffer progress\n- **Visualization**: Live price charts with trend overlays\n- **Configuration**: AI settings and parameters\n- **Training**: Validation metrics and history" + }, + { + "title": "Deployment & Integration", + "type": "TECHNICAL", + "navOrder": 4, + "content": "## Deployment\n\n### Prerequisites\n\n- Node.js >= 18.0.0\n- npm >= 8.0.0\n- Binance account (optional, for live data)\n- Google Gemini API key (optional, for AI)\n\n### Quick Start\n\n```bash\n# 1. Install Share layer\ncd Ark.Alliance.TrendsCalculator.Share\nnpm install && npm run build\n\n# 2. Start Backend\ncd ../Ark.Alliance.TrendsCalculator.Backend\nnpm install\ncp .env.example .env\nnpm run dev\n\n# 3. Start Frontend\ncd ../Ark.Alliance.TrendsCalculator.Ui\nnpm install\nnpm run dev\n```\n\n### Environment Variables\n\n```env\n# Server\nPORT=3001\nNODE_ENV=development\n\n# Binance\nBINANCE_API_KEY=your_key\nBINANCE_SECRET_KEY=your_secret\nBINANCE_USE_TESTNET=true\n\n# AI (optional)\nGEMINI_API_KEY=your_gemini_key\n\n# Calculation\nDEFAULT_BUFFER_SIZE=200\nMIN_BUFFER_SIZE=50\n```\n\n## API Integration\n\n### REST Endpoints\n\n| Endpoint | Method | Purpose |\n|----------|--------|---------||\n| `/api/health` | GET | Health check |\n| `/api/symbol/track` | POST | Start tracking |\n| `/api/symbol/:symbol/track` | DELETE | Stop tracking |\n| `/api/symbol/:symbol/status` | GET | Buffer status |\n| `/api/trend/:symbol/analyze` | GET | Current trend |\n| `/api/trend/:symbol/history` | GET | Trend history |\n\n### WebSocket Events\n\n**Server → Client**:\n- `trend:update` — Trend analysis result\n- `buffer:progress` — Buffer filling status\n- `symbol:added` — Symbol tracking started\n- `ai:analysis` — AI analysis result\n\n**Client → Server**:\n- `subscribe:symbol` — Subscribe to updates\n- `unsubscribe:symbol` — Unsubscribe\n\n### Example Consumer\n\n```typescript\nimport { io } from 'socket.io-client';\n\nconst socket = io('wss://trends-api.example.com');\n\nsocket.emit('subscribe:symbol', 'BTCUSDT');\n\nsocket.on('trend:update', (data) => {\n console.log(`${data.symbol}: ${data.direction}`);\n console.log(`Strength: ${data.strength}`);\n console.log(`Composite: ${data.compositeScore}`);\n});\n```" + } + ] +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/README.md b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3c193af7706047b6b14fffe51988244e7c458897 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/InitDbAsset/README.md @@ -0,0 +1,177 @@ +# InitDbAsset - Database Initialization Assets + +This folder contains all JSON seed data files for database initialization. + +## Folder Structure + +``` +InitDbAsset/ +├── README.md # This file +├── JsonDatas/ # Core entity seed data +│ ├── profile.json # Portfolio owner profile +│ ├── experience.json # Professional work history +│ ├── skills.json # Technical skills by category +│ ├── carousel.json # Homepage carousel items +│ └── technologies.json # Master technology catalog +└── ProjectData/ # Project-specific seed data + └── projects.json # Full project hierarchy +``` + +## File Specifications + +### JsonDatas/profile.json + +**Entity**: `Profile` +**Records**: 1 + +| Field | Type | Description | +|-------|------|-------------| +| firstName | string | First name | +| lastName | string | Last name | +| title | string | Professional title | +| overview | string | Professional summary | +| email | string | Contact email | +| githubUrl | string | GitHub profile URL | +| linkedinUrl | string | LinkedIn profile URL | +| avatarUrl | string | Path to avatar image | + +--- + +### JsonDatas/experience.json + +**Entity**: `Experience` +**Records**: 8 + +| Field | Type | Description | +|-------|------|-------------| +| company | string | Company name with industry | +| project | string | Project name/description | +| period | string | Date range (display only) | +| role | string | Job title/position | +| tech | string | Technologies used (comma-separated) | +| desc | string | Role description | + +--- + +### JsonDatas/skills.json + +**Entity**: `Skill` +**Records**: 40+ (across 5 categories) + +```json +{ + "languages": ["C#", "Python", "Java", ...], + "frameworks": [".NET", "React", ...], + "databases": ["PostgreSQL", "MongoDB", ...], + "tools": ["Docker", "Kubernetes", ...], + "methodologies": ["DDD", "CQRS", ...] +} +``` + +--- + +### JsonDatas/carousel.json + +**Entity**: `CarouselItem` +**Records**: 4 + +| Field | Type | Description | +|-------|------|-------------| +| title | string | Project title | +| subtitle | string | Short tagline | +| description | string | Extended description | +| imageUrl | string | Path to hero image (in /Assets) | +| linkUrl | string | Navigation link | +| linkText | string | CTA button text | +| order | number | Display order | +| isActive | boolean | Visibility flag | + +--- + +### JsonDatas/technologies.json + +**Entity**: `Technology` +**Records**: 90+ (across 15 categories) + +Master catalog of technologies/frameworks with rich metadata for display. + +| Field | Type | Description | +|-------|------|-------------| +| key | string | Unique identifier (kebab-case) | +| name | string | Display name | +| label | string | Extended label | +| category | string | Category ID (frontend, backend, etc.) | +| description | string | Brief description | +| icon | string | Font Awesome or Devicon class | +| color | string | Brand color (hex) | +| website | string | Official website URL | +| versions | string[] | Supported versions (optional) | + +**Categories**: +- `frontend` - React, Angular, Vue, etc. +- `languages` - TypeScript, Python, C#, etc. +- `runtimes` - Node.js, .NET, Unity, etc. +- `backend` - Express, NestJS, FastAPI, etc. +- `databases` - PostgreSQL, MongoDB, Redis, etc. +- `cloud` - AWS, Azure, GCP, etc. +- `devops` - Docker, Kubernetes, Terraform, etc. +- `messaging` - RabbitMQ, Kafka, etc. +- `ai` - PyTorch, OpenAI, Anthropic, etc. +- `enterprise` - SAP, Salesforce, etc. +- `patterns` - Microservices, CQRS, DDD, etc. +- `apis` - Binance, Stripe, Twilio, etc. +- `testing` - Jest, Cypress, Playwright, etc. +- `mobile` - React Native, Flutter, Swift, etc. +- `styling` - TailwindCSS, Sass, Bootstrap, etc. + +--- + +### ProjectData/projects.json + +**Entities**: `Project`, `ProjectPage`, `ProjectFeature`, `ProjectTechnology` +**Records**: 3 projects with nested data + +```json +{ + "title": "Project Name", + "description": "...", + "status": "Featured", + "imageUrl": "/Assets/Projects/...", + "repoUrl": "https://github.com/...", + "demoUrl": "", + "technologies": ["React", "TypeScript", ...], + "features": [ + { + "title": "Feature Name", + "description": "...", + "icon": "font-awesome-icon-name" + } + ], + "pages": [ + { + "title": "Overview", + "type": "OVERVIEW", + "navOrder": 1, + "content": "## Markdown content..." + } + ] +} +``` + +--- + +## Adding New Seed Data + +1. Create JSON file in appropriate folder +2. Follow existing schema patterns +3. Create corresponding seeder in `../seeds/seeders/` +4. Update seeder barrel exports +5. Add to orchestrator in `../seeds/seed.ts` + +## Asset References + +Image URLs in seed data should reference files in: +- `/Assets/Projects/{ProjectName}/` - Project images +- `/Assets/Site/` - Site-wide assets (logo, favicon) + +These are served statically by the backend at runtime. diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/context/db-context.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/context/db-context.ts new file mode 100644 index 0000000000000000000000000000000000000000..c68a0cc60c30ee6f3f7a41765c0e8501b64e43dd --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/context/db-context.ts @@ -0,0 +1,12 @@ +import { AppDataSource } from '../../config/database'; + +export const initializeDatabase = async () => { + try { + await AppDataSource.initialize(); + console.log('Data Source has been initialized!'); + } catch (err) { + console.error('Error during Data Source initialization:', err); + process.exit(1); + } +}; + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/ai-settings.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/ai-settings.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..294ae8ff45f70223054bbd0c4afad72e73238d2b --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/ai-settings.entity.ts @@ -0,0 +1,133 @@ +/** + * @fileoverview AI Settings Entity + * Stores AI provider configuration with encrypted API key storage. + * + * @module entities/ai-settings + * @author Armand Richelet-Kleinberg + * @since 1.0.0 + * + * @example + * // Creating a new AI settings record + * const settings = new AiSettings(); + * settings.provider = 'openai'; + * settings.model = 'gpt-4'; + * settings.apiKeyEncrypted = encrypt(apiKey); + * await repository.save(settings); + */ + +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +/** + * Supported AI providers. + * @enum {string} + */ +export enum AiProvider { + /** OpenAI (GPT-3.5, GPT-4) */ + OPENAI = 'openai', + /** Anthropic (Claude) */ + ANTHROPIC = 'anthropic', + /** Google (Gemini) */ + GOOGLE = 'google', + /** Custom/self-hosted API */ + CUSTOM = 'custom' +} + +/** + * AI Settings entity for storing provider configuration. + * + * @class AiSettings + * @description Stores AI configuration including provider, model, and encrypted API key. + * Only one active configuration is used at a time. + * + * @remarks + * - API keys are stored encrypted using AES-256-GCM + * - Temperature and maxTokens control response generation + * - Use the AiService to manage settings with encryption handling + * + * @see {@link AiService} for configuration management + * @see {@link encrypt} for API key encryption + */ +@Entity('ai_settings') +export class AiSettings { + /** + * Unique identifier for the settings record. + * @type {number} + */ + @PrimaryGeneratedColumn() + id!: number; + + /** + * AI provider identifier. + * @type {string} + * @default 'openai' + * @example 'openai', 'anthropic', 'google', 'custom' + */ + @Column({ length: 50, default: 'openai' }) + provider!: string; + + /** + * Custom API URL for self-hosted or alternative endpoints. + * @type {string | null} + * @remarks Only used when provider is 'custom' or for proxy setups + */ + @Column({ length: 500, nullable: true }) + apiUrl!: string; + + /** + * Encrypted API key for the provider. + * @type {string | null} + * @remarks Encrypted using AES-256-GCM via encryption utility + * @see {@link encrypt} for encryption details + */ + @Column('text', { nullable: true }) + apiKeyEncrypted!: string; + + /** + * Model identifier to use. + * @type {string} + * @default 'gpt-4' + * @example 'gpt-4', 'gpt-3.5-turbo', 'claude-3-opus', 'gemini-pro' + */ + @Column({ length: 100, default: 'gpt-4' }) + model!: string; + + /** + * Sampling temperature for response generation. + * @type {number} + * @default 0.7 + * @remarks Range: 0.0 (deterministic) to 2.0 (creative) + */ + @Column({ type: 'decimal', precision: 3, scale: 2, default: 0.7 }) + temperature!: number; + + /** + * Maximum tokens in the response. + * @type {number} + * @default 2000 + */ + @Column({ type: 'integer', default: 2000 }) + maxTokens!: number; + + /** + * Whether this configuration is currently active. + * @type {boolean} + * @default true + */ + @Column({ default: true }) + isActive!: boolean; + + /** + * Record creation timestamp. + * @type {Date} + */ + @CreateDateColumn() + createdAt!: Date; + + /** + * Record last update timestamp. + * @type {Date} + */ + @UpdateDateColumn() + updatedAt!: Date; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/audit-log.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/audit-log.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0aeaf921130b18ebce6c32f5a4ef293941d8666 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/audit-log.entity.ts @@ -0,0 +1,78 @@ +/** + * @fileoverview Audit Log Entity + * Stores security-relevant events for compliance and monitoring. + * + * @author Armand Richelet-Kleinberg + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, + Index +} from 'typeorm'; +import { User } from './user.entity'; +import { AuditAction } from '@arkalliance/startupcms-ai-share'; + +/** + * AuditLog entity for security event tracking. + * @remarks + * - Logs authentication, role changes, and security events + * - userId is nullable for failed login attempts + * - Indexed for efficient querying by user and action + */ +@Entity('audit_logs') +@Index(['userId', 'createdAt']) +@Index(['action', 'createdAt']) +export class AuditLog { + @PrimaryGeneratedColumn('uuid') + id!: string; + + /** User who performed the action (null for failed logins with non-existent user) */ + @Column('uuid', { nullable: true }) + userId?: string; + + @ManyToOne(() => User, { nullable: true, onDelete: 'SET NULL' }) + @JoinColumn({ name: 'userId' }) + user?: User; + + /** Type of action performed (stored as string for SQLite compatibility) */ + @Column({ type: 'varchar', length: 50 }) + action!: AuditAction; + + /** Client IP address */ + @Column({ length: 45, nullable: true }) + ipAddress?: string; + + /** Browser/device user agent */ + @Column({ length: 500, nullable: true }) + userAgent?: string; + + /** Whether the action succeeded */ + @Column({ default: true }) + success!: boolean; + + /** Additional context as JSON string */ + @Column('text', { nullable: true }) + details?: string; + + @CreateDateColumn() + createdAt!: Date; + + /** + * Parse details JSON if present. + */ + get parsedDetails(): Record | null { + if (!this.details) return null; + try { + return JSON.parse(this.details); + } catch { + return null; + } + } +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/business-domain.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/business-domain.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..5320edab461ea3205cac889fc29ee2c063c2c80d --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/business-domain.entity.ts @@ -0,0 +1,100 @@ +/** + * @fileoverview Business Domain Entity + * Represents business domain knowledge and expertise. + * Supports userId (new) and legacy profileId/collaboratorId. + * + * @author Armand Richelet-Kleinberg + */ + +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Index } from 'typeorm'; +import { User } from './user.entity'; +import { Collaborator } from './collaborator.entity'; + +/** + * Preset list of valid business domains. + * Backend validation should enforce values from this list. + */ +export const BUSINESS_DOMAIN_PRESETS = [ + 'Logistics', + 'Finance', + 'Trading', + 'Insurance', + 'Steel Manufacturing', + 'Theatre', + 'Entertainment', + 'Music Playing', + 'Music Composing', + 'Movie Making', + 'Retail', + 'Banking', + 'Asset Management', + 'Supply Chain', + 'Healthcare', + 'E-Commerce', + 'Telecommunications', + 'Real Estate', + 'Energy', + 'Automotive' +] as const; + +export type BusinessDomainType = typeof BUSINESS_DOMAIN_PRESETS[number]; + +@Entity('business_domains') +@Index('IDX_business_domains_userId', ['userId']) +@Index('IDX_business_domains_displayOrder', ['displayOrder']) +export class BusinessDomain { + @PrimaryGeneratedColumn() + id!: number; + + /** + * Domain name - must be from preset list. + */ + @Column({ length: 100 }) + domain!: string; + + /** + * Proficiency level in this domain. + * @example 'Expert', 'Advanced', 'Intermediate', 'Beginner' + */ + @Column({ length: 50, nullable: true }) + level!: string; + + @Column('text', { nullable: true }) + description!: string; + + /** + * Years of experience in this domain. + */ + @Column({ type: 'int', nullable: true }) + yearsOfExperience!: number; + + /** + * Lucide icon name for visual representation. + */ + @Column({ length: 50, nullable: true }) + icon!: string; + + @Column({ default: 0 }) + displayOrder!: number; + + /** @deprecated Use userId instead */ + @Column({ default: 1 }) + profileId!: number; + + /** @deprecated Use userId instead - kept for migration */ + @Column('uuid', { nullable: true }) + collaboratorId?: string; + + @ManyToOne(() => Collaborator, collaborator => collaborator.businessDomains, { nullable: true }) + @JoinColumn({ name: 'collaboratorId' }) + collaborator?: Collaborator; + + /** User who owns this business domain (for personal resume) */ + @Column('uuid', { nullable: true }) + userId?: string; + + @ManyToOne(() => User, user => user.businessDomains, { nullable: true }) + @JoinColumn({ name: 'userId' }) + user?: User; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/carousel-item.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/carousel-item.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..d4e2c4c2e3a28b113deb0b473719c1494de46763 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/carousel-item.entity.ts @@ -0,0 +1,48 @@ +/** + * @fileoverview Carousel Item Entity + * Represents a slide in the homepage carousel. + * + * @author Armand Richelet-Kleinberg + */ + +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('carousel_items') +export class CarouselItem { + @PrimaryGeneratedColumn() + id!: number; + + @Column() + title!: string; + + @Column({ nullable: true }) + subtitle!: string; + + @Column({ type: 'text', nullable: true }) + description!: string; + + @Column() + imageUrl!: string; + + @Column({ nullable: true }) + linkUrl!: string; + + @Column({ nullable: true }) + linkText!: string; + + @Column({ default: 0 }) + order!: number; + + @Column({ default: true }) + isActive!: boolean; + + @Column({ nullable: true }) + projectId!: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/collaborator.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/collaborator.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2838ba3e19a6728e6f6f4cffbad7e2f4367e87a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/collaborator.entity.ts @@ -0,0 +1,173 @@ +/** + * @fileoverview Collaborator Entity + * Represents a team member in an organization with hierarchical structure. + * Evolves from the original Profile entity with added hierarchy support. + * + * @author Armand Richelet-Kleinberg + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + OneToMany, + OneToOne, + JoinColumn, + Index +} from 'typeorm'; +import { Organization } from './organization.entity'; +import { User } from './user.entity'; +import { Experience } from './experience.entity'; +import { Education } from './education.entity'; +import { Skill } from './skill.entity'; +import { Language } from './language.entity'; +import { Hobby } from './hobby.entity'; +import { BusinessDomain } from './business-domain.entity'; + +/** + * Collaborator entity for organization team members. + * @remarks + * - Supports hierarchy via self-referential reportsTo relationship + * - Optionally linked to User for authentication + * - Owns resume data (experiences, education, skills, etc.) + */ +@Entity('collaborators') +export class Collaborator { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ length: 100 }) + firstName!: string; + + @Column({ length: 100 }) + lastName!: string; + + @Column({ length: 200, nullable: true }) + email?: string; + + /** Job title/position */ + @Column({ length: 200 }) + position!: string; + + /** Department or team */ + @Column({ length: 100, nullable: true }) + department?: string; + + /** Bio/overview text */ + @Column('text', { nullable: true }) + bio?: string; + + @Column({ length: 500, nullable: true }) + avatarUrl?: string; + + @Column({ length: 255, nullable: true }) + linkedinUrl?: string; + + @Column({ length: 255, nullable: true }) + githubUrl?: string; + + @Column({ length: 255, nullable: true }) + twitterUrl?: string; + + @Column({ length: 50, nullable: true }) + phone?: string; + + /** Date when collaborator joined */ + @Column({ type: 'date', nullable: true }) + hireDate?: Date; + + /** Whether collaborator is currently active */ + @Column({ default: true }) + isActive!: boolean; + + /** Display order in lists */ + @Column({ default: 0 }) + displayOrder!: number; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + // ============================================ + // Organization Relationship + // ============================================ + + @Index() + @Column('uuid') + organizationId!: string; + + @ManyToOne(() => Organization, org => org.collaborators) + @JoinColumn({ name: 'organizationId' }) + organization!: Organization; + + // ============================================ + // Hierarchy (Self-Reference) + // ============================================ + + /** ID of the manager this person reports to */ + @Index() + @Column('uuid', { nullable: true }) + reportsToId?: string; + + @ManyToOne(() => Collaborator, collaborator => collaborator.directReports, { nullable: true }) + @JoinColumn({ name: 'reportsToId' }) + reportsTo?: Collaborator; + + @OneToMany(() => Collaborator, collaborator => collaborator.reportsTo) + directReports!: Collaborator[]; + + // ============================================ + // User Authentication Link (Optional) + // ============================================ + + /** Linked user account for authentication */ + @Column('uuid', { nullable: true }) + userId?: string; + + @OneToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'userId' }) + user?: User; + + // ============================================ + // Resume Data Relations + // ============================================ + + @OneToMany(() => Experience, experience => experience.collaborator) + experiences!: Experience[]; + + @OneToMany(() => Education, education => education.collaborator) + educations!: Education[]; + + @OneToMany(() => Skill, skill => skill.collaborator) + skills!: Skill[]; + + @OneToMany(() => Language, language => language.collaborator) + languages!: Language[]; + + @OneToMany(() => Hobby, hobby => hobby.collaborator) + hobbies!: Hobby[]; + + @OneToMany(() => BusinessDomain, domain => domain.collaborator) + businessDomains!: BusinessDomain[]; + + // ============================================ + // Tasks / Accomplishments (Recognition System) + // ============================================ + + @OneToMany('Task', 'collaborator') + tasks!: import('./task.entity').Task[]; + + // ============================================ + // Computed Properties + // ============================================ + + /** Full name for display */ + get fullName(): string { + return `${this.firstName} ${this.lastName}`; + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/education.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/education.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..fd0352ddc9a9ae0e8e13f593b22932cb47b32cf7 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/education.entity.ts @@ -0,0 +1,60 @@ +/** + * @fileoverview Education Entity + * Supports userId (new) and legacy profileId/collaboratorId. + * + * @author Armand Richelet-Kleinberg + */ + +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Index } from 'typeorm'; +import { User } from './user.entity'; +import { Collaborator } from './collaborator.entity'; + +@Entity('education') +@Index('IDX_education_userId', ['userId']) +export class Education { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ length: 200 }) + institution!: string; + + @Column({ length: 100, nullable: true }) + degree!: string; + + @Column({ length: 100, nullable: true }) + fieldOfStudy!: string; + + @Column({ type: 'date', nullable: true }) + startDate!: Date; + + @Column({ type: 'date', nullable: true }) + endDate!: Date; + + @Column('text', { nullable: true }) + description!: string; + + /** @deprecated Use userId instead */ + @Column({ default: 1 }) + profileId!: number; + + @Column({ default: 0 }) + displayOrder!: number; + + /** @deprecated Use userId instead - kept for migration */ + @Column('uuid', { nullable: true }) + collaboratorId?: string; + + @ManyToOne(() => Collaborator, collaborator => collaborator.educations, { nullable: true }) + @JoinColumn({ name: 'collaboratorId' }) + collaborator?: Collaborator; + + /** User who owns this education (for personal resume) */ + @Column('uuid', { nullable: true }) + userId?: string; + + @ManyToOne(() => User, user => user.educations, { nullable: true }) + @JoinColumn({ name: 'userId' }) + user?: User; +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/experience.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/experience.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..3104e3580a2ad7302df6d22c8df1dd6e56e68f1b --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/experience.entity.ts @@ -0,0 +1,68 @@ +/** + * @fileoverview Experience Entity + * Updated with all required fields for CV management. + * Supports userId (new) and legacy profileId/collaboratorId. + * + * @author Armand Richelet-Kleinberg + */ + +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Index } from 'typeorm'; +import { User } from './user.entity'; +import { Collaborator } from './collaborator.entity'; + +@Entity('experience') +@Index('IDX_experience_userId', ['userId']) +@Index('IDX_experience_displayOrder', ['displayOrder']) +export class Experience { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ length: 200 }) + company!: string; + + @Column({ length: 100 }) + position!: string; + + @Column({ length: 200, nullable: true }) + project!: string; + + @Column({ type: 'date', nullable: true }) + startDate!: Date; + + @Column({ type: 'date', nullable: true }) + endDate!: Date | null; + + @Column('text', { nullable: true }) + description!: string; + + @Column('simple-array', { nullable: true }) + technologies!: string[]; + + @Column({ default: false }) + isHighlighted!: boolean; + + @Column({ default: 0 }) + displayOrder!: number; + + /** @deprecated Use userId instead */ + @Column({ default: 1 }) + profileId!: number; + + /** @deprecated Use userId instead - kept for migration */ + @Column('uuid', { nullable: true }) + collaboratorId?: string; + + @ManyToOne(() => Collaborator, collaborator => collaborator.experiences, { nullable: true }) + @JoinColumn({ name: 'collaboratorId' }) + collaborator?: Collaborator; + + /** User who owns this experience (for personal resume) */ + @Column('uuid', { nullable: true }) + userId?: string; + + @ManyToOne(() => User, user => user.experiences, { nullable: true }) + @JoinColumn({ name: 'userId' }) + user?: User; +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/hobby.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/hobby.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..33c9a7ac699666de4aac2f5b75c17adce723b55a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/hobby.entity.ts @@ -0,0 +1,56 @@ +/** + * @fileoverview Hobby Entity + * Represents personal hobbies and interests. + * Supports userId (new) and legacy profileId/collaboratorId. + * + * @author Armand Richelet-Kleinberg + */ + +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Index } from 'typeorm'; +import { User } from './user.entity'; +import { Collaborator } from './collaborator.entity'; + +@Entity('hobbies') +@Index('IDX_hobbies_userId', ['userId']) +@Index('IDX_hobbies_displayOrder', ['displayOrder']) +export class Hobby { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ length: 100 }) + name!: string; + + @Column('text', { nullable: true }) + description!: string; + + /** + * Lucide icon name for visual representation. + * @example 'Music', 'Camera', 'Gamepad2' + */ + @Column({ length: 50, nullable: true }) + icon!: string; + + @Column({ default: 0 }) + displayOrder!: number; + + /** @deprecated Use userId instead */ + @Column({ default: 1 }) + profileId!: number; + + /** @deprecated Use userId instead - kept for migration */ + @Column('uuid', { nullable: true }) + collaboratorId?: string; + + @ManyToOne(() => Collaborator, collaborator => collaborator.hobbies, { nullable: true }) + @JoinColumn({ name: 'collaboratorId' }) + collaborator?: Collaborator; + + /** User who owns this hobby (for personal resume) */ + @Column('uuid', { nullable: true }) + userId?: string; + + @ManyToOne(() => User, user => user.hobbies, { nullable: true }) + @JoinColumn({ name: 'userId' }) + user?: User; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/language.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/language.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..36704bb76cfd0be4f4a928a876f3ab18504dd694 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/language.entity.ts @@ -0,0 +1,67 @@ +/** + * @fileoverview Language Entity + * Represents language proficiency with three dimensions. + * Supports userId (new) and legacy profileId/collaboratorId. + * + * @author Armand Richelet-Kleinberg + */ + +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Index } from 'typeorm'; +import { User } from './user.entity'; +import { Collaborator } from './collaborator.entity'; + +@Entity('languages') +@Index('IDX_languages_userId', ['userId']) +@Index('IDX_languages_displayOrder', ['displayOrder']) +export class Language { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ length: 100 }) + language!: string; + + /** + * Speaking proficiency (1-5 scale) + * 1 = Basic, 5 = Native/Fluent + */ + @Column({ type: 'int', default: 1 }) + speaking!: number; + + /** + * Writing proficiency (1-5 scale) + * 1 = Basic, 5 = Native/Fluent + */ + @Column({ type: 'int', default: 1 }) + writing!: number; + + /** + * Presenting proficiency (1-5 scale) + * 1 = Basic, 5 = Native/Fluent + */ + @Column({ type: 'int', default: 1 }) + presenting!: number; + + @Column({ default: 0 }) + displayOrder!: number; + + /** @deprecated Use userId instead */ + @Column({ default: 1 }) + profileId!: number; + + /** @deprecated Use userId instead - kept for migration */ + @Column('uuid', { nullable: true }) + collaboratorId?: string; + + @ManyToOne(() => Collaborator, collaborator => collaborator.languages, { nullable: true }) + @JoinColumn({ name: 'collaboratorId' }) + collaborator?: Collaborator; + + /** User who owns this language (for personal resume) */ + @Column('uuid', { nullable: true }) + userId?: string; + + @ManyToOne(() => User, user => user.languages, { nullable: true }) + @JoinColumn({ name: 'userId' }) + user?: User; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/media.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/media.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..6db5681741f0e4321d5dd1def1d2b6ad17401c2a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/media.entity.ts @@ -0,0 +1,220 @@ +/** + * @fileoverview Media Entity + * Stores uploaded media files with comprehensive metadata for widgets and AI integration. + * + * @module entities/media + * @author Armand Richelet-Kleinberg + * @since 1.0.0 + * + * @description + * The Media entity supports multiple file types including images, videos, audio, + * documents (PDF, Word, Excel), and data files (JSON, Markdown). Each media item + * can be assigned a unique key for programmatic reference in widgets or AI context. + * + * @example + * // Creating a new media record from upload + * const media = new Media(); + * media.name = 'Company Logo'; + * media.key = 'logo-main'; + * media.url = '/uploads/image/uuid-filename.png'; + * media.type = MediaType.IMAGE; + * media.mimeType = 'image/png'; + * media.tags = ['branding', 'logo']; + * await repository.save(media); + * + * @example + * // Using key to reference media in widgets + * const logo = await mediaRepo.findOne({ where: { key: 'logo-main' } }); + */ + +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +/** + * Media asset types supported by the system. + * + * @enum {string} + * @description Used to categorize uploaded files for filtering and display purposes. + */ +export enum MediaType { + /** Image files - JPEG, PNG, GIF, WebP, SVG, ICO */ + IMAGE = 'image', + /** Video files - MP4, WebM, MOV, AVI */ + VIDEO = 'video', + /** Audio files - MP3, WAV, OGG, FLAC */ + AUDIO = 'audio', + /** PDF documents */ + PDF = 'pdf', + /** Microsoft Word documents - DOC, DOCX */ + WORD = 'word', + /** Microsoft Excel spreadsheets - XLS, XLSX */ + EXCEL = 'excel', + /** Markdown text files - MD */ + MARKDOWN = 'markdown', + /** JSON data files */ + JSON = 'json', + /** Other unclassified document types */ + OTHER = 'other' +} + +/** + * Media entity for storing uploaded files and external media references. + * + * @class Media + * @description Stores media files with rich metadata including dimensions, tags, + * and custom metadata for AI assistant integration. + * + * @remarks + * - Use `key` for stable programmatic references (survives re-uploads) + * - Use `tags` for categorization and filtering + * - Use `metadata` for AI context or widget-specific configuration + * - Files are stored locally in type-organized subdirectories + * + * @see {@link AdminMediaService} for CRUD operations + * @see {@link upload.middleware} for file upload handling + */ +@Entity('media') +export class Media { + /** + * Unique identifier (UUID) for the media record. + * @type {string} + * @remarks Auto-generated UUID v4 + */ + @PrimaryGeneratedColumn('uuid') + id!: string; + + /** + * Human-readable display name. + * @type {string} + * @example 'Company Logo', 'Profile Photo', 'Product Brochure' + */ + @Column({ length: 200 }) + name!: string; + + /** + * Unique key for programmatic reference. + * @type {string | null} + * @example 'profile-avatar', 'cv-document', 'hero-banner' + * @remarks + * - Optional but recommended for media used in widgets or AI context + * - Must be unique across all media items + * - Use kebab-case for consistency + */ + @Column({ length: 100, unique: true, nullable: true }) + key!: string; + + /** + * Full URL or relative path to the media file. + * @type {string} + * @example '/uploads/image/abc123-photo.jpg' + * @example 'https://example.com/image.png' + */ + @Column({ length: 500 }) + url!: string; + + /** + * Media type classification. + * @type {MediaType} + * @default MediaType.IMAGE + * @see MediaType for available types + */ + @Column({ type: 'text', default: MediaType.IMAGE }) + type!: MediaType; + + /** + * MIME type of the file. + * @type {string | null} + * @example 'image/jpeg', 'application/pdf', 'video/mp4' + */ + @Column({ length: 100, nullable: true }) + mimeType!: string; + + /** + * Original filename as uploaded by user. + * @type {string | null} + * @remarks Preserved for reference; actual stored filename may differ + */ + @Column({ length: 255, nullable: true }) + originalFileName!: string; + + /** + * File size in bytes. + * @type {number} + * @default 0 + */ + @Column({ type: 'integer', default: 0 }) + fileSize!: number; + + /** + * Alt text for accessibility. + * @type {string | null} + * @remarks Required for images per WCAG guidelines + */ + @Column({ length: 500, nullable: true }) + altText!: string; + + /** + * Description or caption for the media. + * @type {string | null} + * @remarks Can be used for AI context or display purposes + */ + @Column('text', { nullable: true }) + description!: string; + + /** + * Tags for categorization and filtering. + * @type {string[]} + * @example ['branding', 'logo'], ['documentation', 'api'] + * @remarks Stored as JSON array + */ + @Column('simple-json', { nullable: true }) + tags!: string[]; + + /** + * Additional metadata for AI/widget integration. + * @type {Record} + * @example { aiContext: 'company info', widgetId: 'header' } + * @remarks Flexible JSON storage for custom data + */ + @Column('simple-json', { nullable: true }) + metadata!: Record; + + /** + * Width of image/video in pixels. + * @type {number | null} + * @remarks Only applicable for visual media + */ + @Column({ type: 'integer', nullable: true }) + width!: number; + + /** + * Height of image/video in pixels. + * @type {number | null} + * @remarks Only applicable for visual media + */ + @Column({ type: 'integer', nullable: true }) + height!: number; + + /** + * Whether the media is publicly accessible. + * @type {boolean} + * @default true + * @remarks Set to false for private/draft media + */ + @Column({ default: true }) + isPublic!: boolean; + + /** + * Record creation timestamp. + * @type {Date} + */ + @CreateDateColumn() + createdAt!: Date; + + /** + * Record last update timestamp. + * @type {Date} + */ + @UpdateDateColumn() + updatedAt!: Date; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/menu-item.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/menu-item.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7f3935767edca6ddbae37a56f9662efe3e82d7c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/menu-item.entity.ts @@ -0,0 +1,59 @@ +/** + * @fileoverview Menu Item Entity + * Represents a navigation menu item in the database. + * + * @author Armand Richelet-Kleinberg + */ + +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany, JoinColumn } from 'typeorm'; +import { MenuPositionEnum } from '@arkalliance/startupcms-ai-share'; + +@Entity('menu_items') +export class MenuItem { + @PrimaryGeneratedColumn() + id!: number; + + @Column() + label!: string; + + @Column({ nullable: true }) + icon!: string; + + @Column() + route!: string; + + @Column({ + type: 'simple-enum', + enum: MenuPositionEnum, + default: MenuPositionEnum.HEADER + }) + position!: MenuPositionEnum; + + @Column({ default: 0 }) + order!: number; + + @Column({ default: true }) + isVisible!: boolean; + + @Column({ default: false }) + openInNewTab!: boolean; + + @ManyToOne(() => MenuItem, item => item.children, { nullable: true, onDelete: 'CASCADE' }) + @JoinColumn({ name: 'parentId' }) + parent!: MenuItem; + + @Column({ nullable: true }) + parentId!: number; + + @OneToMany(() => MenuItem, item => item.parent) + children!: MenuItem[]; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/organization.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/organization.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..bc5fb711d9290272fb1bf39eb54e5f679d5596b5 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/organization.entity.ts @@ -0,0 +1,118 @@ +/** + * @fileoverview Organization Entity + * Represents a company/organization in the corporate portfolio. + * + * @author Armand Richelet-Kleinberg + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + OneToMany +} from 'typeorm'; +import { Collaborator } from './collaborator.entity'; + +/** + * Organization entity for company/organization data. + * @remarks + * - Stores company profile, location, and branding + * - One organization can have many collaborators + * - Default organization is created during seeding + */ +@Entity('organizations') +export class Organization { + @PrimaryGeneratedColumn('uuid') + id!: string; + + /** Organization display name */ + @Column({ length: 200 }) + name!: string; + + /** Legal/registered business name */ + @Column({ length: 300, nullable: true }) + legalName?: string; + + /** Short tagline/slogan */ + @Column({ length: 300, nullable: true }) + tagline?: string; + + /** Mission statement */ + @Column('text', { nullable: true }) + mission?: string; + + /** Vision statement */ + @Column('text', { nullable: true }) + vision?: string; + + /** Full description */ + @Column('text', { nullable: true }) + description?: string; + + /** Logo image URL */ + @Column({ length: 500, nullable: true }) + logoUrl?: string; + + /** Icon/favicon URL */ + @Column({ length: 500, nullable: true }) + iconUrl?: string; + + // Location + @Column({ length: 300, nullable: true }) + address?: string; + + @Column({ length: 100, nullable: true }) + city?: string; + + @Column({ length: 100, nullable: true }) + country?: string; + + @Column({ length: 20, nullable: true }) + postalCode?: string; + + @Column({ length: 50, nullable: true }) + timezone?: string; + + /** GPS Latitude */ + @Column('decimal', { precision: 10, scale: 8, nullable: true }) + latitude?: number; + + /** GPS Longitude */ + @Column('decimal', { precision: 11, scale: 8, nullable: true }) + longitude?: number; + + // Social Links (stored as JSON for flexibility) + @Column('simple-json', { nullable: true }) + socialLinks?: { + website?: string; + linkedIn?: string; + twitter?: string; + github?: string; + youtube?: string; + instagram?: string; + }; + + /** Year company was founded */ + @Column({ nullable: true }) + foundedYear?: number; + + /** Number of employees (for display) */ + @Column({ nullable: true }) + employeeCount?: number; + + /** Whether organization is active */ + @Column({ default: true }) + isActive!: boolean; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + // Relations + @OneToMany(() => Collaborator, collaborator => collaborator.organization) + collaborators!: Collaborator[]; +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/outbox.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/outbox.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b39357b57299cd832b0770cff98ceda72eba7e8 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/outbox.entity.ts @@ -0,0 +1,19 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm'; + +@Entity('outbox') +export class Outbox { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column() + type!: string; + + @Column('simple-json') + payload!: any; + + @CreateDateColumn() + elementCreatedAt!: Date; + + @Column({ default: false }) + processed!: boolean; +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/page-definition.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/page-definition.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..a37dd0ca814fb578047e9deaf2d08e01fb26b931 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/page-definition.entity.ts @@ -0,0 +1,47 @@ +/** + * @fileoverview Page Definition Entity + * Represents configurable page definitions for static export. + * + * @author Armand Richelet-Kleinberg + */ + +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; +import { StaticPageType } from '@arkalliance/startupcms-ai-share'; + +@Entity('page_definition') +export class PageDefinition { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ + type: 'simple-enum', + enum: StaticPageType + }) + pageType!: StaticPageType; + + @Column() + title!: string; + + @Column() + route!: string; + + @Column('simple-json', { nullable: true }) + sections!: string[] | null; + + @Column({ default: true }) + isEnabled!: boolean; + + @Column({ default: 0 }) + displayOrder!: number; + + @Column('simple-json', { nullable: true }) + metadata!: Record | null; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/page.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/page.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f905eae0e01e86e3f608acde967327cd6a213df --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/page.entity.ts @@ -0,0 +1,115 @@ +/** + * @fileoverview Page Entity - Core CMS page with SEO configuration + * @description Dynamic page entity for SEO-optimized content management. + * Supports multiple page types (home, about, blog, project, custom) with + * full SEO metadata and structured data relationships. + * + * @author Armand Richelet-Kleinberg + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + OneToOne, + OneToMany, + JoinColumn, + Index +} from 'typeorm'; +import { User } from './user.entity'; +import { SeoMeta } from './seo-meta.entity'; +import { StructuredData } from './structured-data.entity'; + +/** + * Page type enumeration for content categorization + */ +export enum PageType { + HOME = 'home', + ABOUT = 'about', + BLOG = 'blog', + PROJECT = 'project', + TEAM = 'team', + CONTACT = 'contact', + CUSTOM = 'custom' +} + +/** + * Page entity for dynamic CMS content + * @remarks + * - Supports SEO metadata via SeoMeta relation + * - Multiple structured data schemas per page + * - Markdown/HTML content support + * - Author tracking for content attribution + */ +@Entity('pages') +export class Page { + @PrimaryGeneratedColumn('uuid') + id!: string; + + /** URL-friendly slug (unique) */ + @Column({ unique: true, length: 255 }) + @Index('IDX_page_slug') + slug!: string; + + /** Page title */ + @Column({ length: 255 }) + title!: string; + + /** Page content (Markdown or HTML) */ + @Column('text') + content!: string; + + /** Page type for template selection */ + @Column({ + type: 'varchar', + length: 50, + default: PageType.CUSTOM + }) + @Index('IDX_page_type') + pageType!: PageType; + + /** Publication status */ + @Column({ default: false }) + @Index('IDX_page_published') + isPublished!: boolean; + + /** Publication date */ + @Column({ nullable: true }) + publishedAt?: Date; + + /** Author user ID */ + @Column('uuid', { nullable: true }) + authorId?: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + // ============================================ + // Relations + // ============================================ + + /** Page author */ + @ManyToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'authorId' }) + author?: User; + + /** SEO metadata (1:1 relationship) */ + @OneToOne(() => SeoMeta, seoMeta => seoMeta.page, { + cascade: true, + eager: true + }) + seoMeta?: SeoMeta; + + /** Structured data schemas (1:many relationship) */ + @OneToMany(() => StructuredData, structuredData => structuredData.page, { + cascade: true, + eager: true + }) + structuredData?: StructuredData[]; +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/profile.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/profile.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b164af55dd145b282f55f6583a9053adcfe433d --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/profile.entity.ts @@ -0,0 +1,38 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('profiles') +export class Profile { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ length: 100 }) + firstName!: string; + + @Column({ length: 100 }) + lastName!: string; + + @Column({ length: 200, nullable: true }) + title!: string; + + @Column('text', { nullable: true }) + overview!: string; + + @Column({ length: 200 }) + email!: string; + + @Column({ length: 255, nullable: true }) + linkedinUrl!: string; + + @Column({ length: 255, nullable: true }) + githubUrl!: string; + + @Column({ length: 255, nullable: true }) + avatarUrl!: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-controller.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-controller.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..3fa5ca723b9614f93bbf12afdd5081084a753f60 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-controller.entity.ts @@ -0,0 +1,26 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm'; +import { Project } from './project.entity'; +import { ProjectEndpoint } from './project-endpoint.entity'; + +@Entity('project_controllers') +export class ProjectController { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column() + name!: string; + + @Column() + basePath!: string; + + @Column({ nullable: true }) + description!: string; + + @ManyToOne(() => Project, project => project.controllers, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'project_id' }) + project!: Project; + + @OneToMany(() => ProjectEndpoint, endpoint => endpoint.controller, { cascade: true }) + endpoints!: ProjectEndpoint[]; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-endpoint.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-endpoint.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..f02972e945a8c93659aea9f9683693b3558d73b2 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-endpoint.entity.ts @@ -0,0 +1,22 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { ProjectController } from './project-controller.entity'; + +@Entity('project_endpoints') +export class ProjectEndpoint { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column() + method!: string; + + @Column() + path!: string; + + @Column({ nullable: true }) + description!: string; + + @ManyToOne(() => ProjectController, controller => controller.endpoints, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'controller_id' }) + controller!: ProjectController; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-feature.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-feature.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..4d2dd5baa59875ccb4cdd9d50c9dae7800a3e110 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-feature.entity.ts @@ -0,0 +1,25 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { Project } from './project.entity'; + +@Entity('project_features') +export class ProjectFeature { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column() + title!: string; + + @Column('text') + description!: string; + + @Column({ nullable: true }) + icon!: string; + + @Column({ nullable: true }) + imageUrl?: string; + + @ManyToOne(() => Project, project => project.features, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'project_id' }) + project!: Project; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-page.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-page.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..19f7a98399f23c19a600d3a8e5ebb352e8b6a661 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-page.entity.ts @@ -0,0 +1,37 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm'; +import { Project } from './project.entity'; + +export enum PageType { + OVERVIEW = 'OVERVIEW', + FUNCTIONAL = 'FUNCTIONAL', + TECHNICAL = 'TECHNICAL', + ROADMAP = 'ROADMAP', + TEAM = 'TEAM', + CONTACT = 'CONTACT' +} + +@Entity('project_pages') +export class ProjectPage { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column() + title!: string; + + @Column({ + type: 'text', + default: PageType.OVERVIEW + }) + type!: PageType; + + @Column({ default: 0 }) + navOrder!: number; + + @Column({ type: 'text', nullable: true }) + content!: string; + + @ManyToOne(() => Project, project => project.pages, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'project_id' }) + project!: Project; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-technology.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-technology.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..4c855c0ebb0330e0392080751463dbe441ceb7da --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project-technology.entity.ts @@ -0,0 +1,16 @@ +import { Entity, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { Project } from './project.entity'; + +@Entity('project_technologies') +export class ProjectTechnology { + @PrimaryColumn('uuid') + projectId!: string; + + @PrimaryColumn({ length: 50 }) + technology!: string; + + @ManyToOne(() => Project, project => project.technologies, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'projectId' }) + project!: Project; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..bcd5fb933bbd9da489a02fdf1c6720654dc8e20f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/project.entity.ts @@ -0,0 +1,54 @@ +import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToMany, JoinTable } from 'typeorm'; +import { ProjectTechnology } from './project-technology.entity'; +import { ProjectPage } from './project-page.entity'; +import { ProjectFeature } from './project-feature.entity'; +import { ProjectController } from './project-controller.entity'; + +@Entity('projects') +export class Project { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column() + title!: string; + + @Column('text') + description!: string; + + @Column() + status!: string; + + @Column({ default: false }) + isFeatured!: boolean; + + @Column({ nullable: true }) + imageUrl!: string; + + @Column({ nullable: true }) + repoUrl!: string; + + @Column({ nullable: true }) + demoUrl!: string; + + @Column({ nullable: true }) + packageUrl!: string; + + @Column({ type: 'date', nullable: true }) + startDate!: Date; + + @Column({ type: 'date', nullable: true }) + endDate!: Date; + + @OneToMany(() => ProjectTechnology, pt => pt.project) + technologies!: ProjectTechnology[]; + + @OneToMany(() => ProjectPage, page => page.project, { cascade: true }) + pages!: ProjectPage[]; + + @OneToMany(() => ProjectFeature, feature => feature.project, { cascade: true }) + features!: ProjectFeature[]; + + @OneToMany(() => ProjectController, controller => controller.project, { cascade: true }) + controllers!: ProjectController[]; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/prompt-template.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/prompt-template.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..f74d2c00ddece1a4d85b0300adedd1af8810bd7c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/prompt-template.entity.ts @@ -0,0 +1,103 @@ +/** + * @fileoverview PromptTemplate Entity + * Stores AI prompt templates for various AI-assisted features. + * Enables reusable, configurable prompts for profile generation, resume enhancement, etc. + * + * @module entities/prompt-template + * @author Armand Richelet-Kleinberg + * @since 1.0.0 + * + * @example + * // Creating a new prompt template + * const template = new PromptTemplate(); + * template.name = 'profile-generation'; + * template.systemPrompt = 'You are an expert data architect...'; + * template.clientPromptTemplate = 'Generate profile for {firstName} {lastName}...'; + * await repository.save(template); + */ + +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +/** + * Prompt Template entity for storing AI prompts. + * + * @class PromptTemplate + * @description Stores reusable AI prompt configurations with system and client templates. + * Supports placeholder substitution for dynamic content generation. + * + * @remarks + * - Use unique `name` to identify prompts (e.g., 'profile-generation', 'resume-enhancement') + * - `clientPromptTemplate` supports placeholders like {firstName}, {description}, etc. + * - `outputFormat` describes expected AI output schema + */ +@Entity('prompt_templates') +export class PromptTemplate { + /** + * Unique identifier for the prompt template. + * @type {number} + */ + @PrimaryGeneratedColumn() + id!: number; + + /** + * Unique name identifier for the prompt. + * @type {string} + * @example 'profile-generation', 'resume-enhancement', 'task-generation' + */ + @Column({ unique: true, length: 100 }) + name!: string; + + /** + * Human-readable description of the prompt's purpose. + * @type {string | null} + */ + @Column('text', { nullable: true }) + description?: string; + + /** + * System prompt with AI instructions and rules. + * @type {string} + * @remarks Defines AI behavior, output format, and constraints + */ + @Column('text') + systemPrompt!: string; + + /** + * Client prompt template with placeholders. + * @type {string} + * @remarks Use {placeholders} for dynamic content substitution + * @example 'Generate profile for {firstName} {lastName}. Function: {function}' + */ + @Column('text') + clientPromptTemplate!: string; + + /** + * Expected output format description or schema. + * @type {string | null} + * @example 'JSON conforming to UserCollaboratorSchema' + */ + @Column({ length: 500, nullable: true }) + outputFormat?: string; + + /** + * Whether this prompt template is active. + * @type {boolean} + * @default true + */ + @Column({ default: true }) + isActive!: boolean; + + /** + * Record creation timestamp. + * @type {Date} + */ + @CreateDateColumn() + createdAt!: Date; + + /** + * Record last update timestamp. + * @type {Date} + */ + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/refresh-token.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/refresh-token.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..e07f514d3f25f091151ed70aa51c0417e9c213e9 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/refresh-token.entity.ts @@ -0,0 +1,66 @@ +/** + * @fileoverview Refresh Token Entity + * Stores refresh tokens for JWT token rotation. + * + * @author Armand Richelet-Kleinberg + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn +} from 'typeorm'; +import { User } from './user.entity'; + +/** + * RefreshToken entity for secure token management. + * @remarks + * - Stores hash of token, not the token itself + * - Supports token rotation (revoke old on use) + * - Tracks device info for session management + */ +@Entity('refresh_tokens') +export class RefreshToken { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column('uuid') + userId!: string; + + @ManyToOne(() => User, user => user.refreshTokens, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'userId' }) + user!: User; + + /** SHA-256 hash of the refresh token */ + @Column({ length: 255 }) + tokenHash!: string; + + /** When this token expires */ + @Column() + expiresAt!: Date; + + /** Whether this token has been revoked */ + @Column({ default: false }) + isRevoked!: boolean; + + /** Browser/device user agent string */ + @Column({ length: 500, nullable: true }) + userAgent?: string; + + /** Client IP address (IPv6 compatible) */ + @Column({ length: 45, nullable: true }) + ipAddress?: string; + + @CreateDateColumn() + createdAt!: Date; + + /** + * Check if token is valid (not revoked and not expired). + */ + get isValid(): boolean { + return !this.isRevoked && new Date() < this.expiresAt; + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/seo-meta.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/seo-meta.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c3868bb6da75245a2a4c1face39feaa913e935f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/seo-meta.entity.ts @@ -0,0 +1,123 @@ +/** + * @fileoverview SeoMeta Entity - SEO metadata for pages + * @description Stores comprehensive SEO metadata including meta tags, + * Open Graph tags, Twitter Card data, and robots directives. + * + * @author Armand Richelet-Kleinberg + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + OneToOne, + JoinColumn +} from 'typeorm'; +import { Page } from './page.entity'; + +/** + * SEO metadata entity for page-level SEO configuration + * @remarks + * - Meta tags (title, description, keywords) + * - Open Graph tags for social sharing + * - Twitter Card configuration + * - Canonical URL and robots directives + */ +@Entity('seo_meta') +export class SeoMeta { + @PrimaryGeneratedColumn('uuid') + id!: string; + + /** Related page ID */ + @Column('uuid') + pageId!: string; + + // ============================================ + // Basic Meta Tags + // ============================================ + + /** Meta title (overrides page title if set) */ + @Column({ length: 255, nullable: true }) + metaTitle?: string; + + /** Meta description for search results */ + @Column({ length: 500, nullable: true }) + metaDescription?: string; + + /** Meta keywords (comma-separated) */ + @Column({ length: 500, nullable: true }) + metaKeywords?: string; + + /** Canonical URL (prevents duplicate content issues) */ + @Column({ length: 500, nullable: true }) + canonicalUrl?: string; + + /** Prevent indexing by search engines */ + @Column({ default: false }) + noindex!: boolean; + + /** Prevent following links on this page */ + @Column({ default: false }) + nofollow!: boolean; + + // ============================================ + // Open Graph (OG) Tags + // ============================================ + + /** OG title for social sharing */ + @Column({ length: 255, nullable: true }) + ogTitle?: string; + + /** OG description for social sharing */ + @Column({ length: 500, nullable: true }) + ogDescription?: string; + + /** OG image URL (absolute URL) */ + @Column({ length: 500, nullable: true }) + ogImage?: string; + + /** OG type (website, article, profile, etc.) */ + @Column({ length: 50, default: 'website' }) + ogType!: string; + + /** OG locale (e.g., en_US) */ + @Column({ length: 10, nullable: true }) + ogLocale?: string; + + // ============================================ + // Twitter Card Tags + // ============================================ + + /** Twitter card type (summary, summary_large_image, etc.) */ + @Column({ length: 50, default: 'summary_large_image' }) + twitterCard!: string; + + /** Twitter title */ + @Column({ length: 255, nullable: true }) + twitterTitle?: string; + + /** Twitter description */ + @Column({ length: 500, nullable: true }) + twitterDescription?: string; + + /** Twitter image URL (absolute URL) */ + @Column({ length: 500, nullable: true }) + twitterImage?: string; + + /** Twitter site handle (@username) */ + @Column({ length: 50, nullable: true }) + twitterSite?: string; + + /** Twitter creator handle (@username) */ + @Column({ length: 50, nullable: true }) + twitterCreator?: string; + + // ============================================ + // Relations + // ============================================ + + /** Related page (1:1 inverse side) */ + @OneToOne(() => Page, page => page.seoMeta, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'pageId' }) + page!: Page; +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/site-settings.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/site-settings.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..e005727885677c70825db7cf9e1cc226d729f743 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/site-settings.entity.ts @@ -0,0 +1,181 @@ +/** + * @fileoverview SiteSettings Entity - Global site configuration + * @description Stores global site settings including company information, + * branding, social links, and default SEO metadata. + * + * @author Armand Richelet-Kleinberg + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn +} from 'typeorm'; + +/** + * Site settings entity for global configuration + * @remarks + * - Singleton pattern (only one record should exist) + * - Company branding and information + * - Default SEO fallbacks + * - Social media links + * - Analytics configuration + */ +@Entity('site_settings') +export class SiteSettings { + @PrimaryGeneratedColumn('uuid') + id!: string; + + // ============================================ + // Site Identity + // ============================================ + + /** Site/company name */ + @Column({ length: 255 }) + siteName!: string; + + /** Full site URL (e.g., https://example.com) */ + @Column({ length: 255 }) + siteUrl!: string; + + /** Company/organization name */ + @Column({ length: 255 }) + companyName!: string; + + /** Company logo URL (absolute URL) */ + @Column({ length: 500, nullable: true }) + logoUrl?: string; + + /** Favicon URL */ + @Column({ length: 500, nullable: true }) + faviconUrl?: string; + + // ============================================ + // Default SEO Metadata + // ============================================ + + /** Default meta description (fallback) */ + @Column({ length: 500, nullable: true }) + defaultMetaDescription?: string; + + /** Default OG image (fallback) */ + @Column({ length: 500, nullable: true }) + defaultOgImage?: string; + + /** Default site keywords */ + @Column({ length: 500, nullable: true }) + defaultKeywords?: string; + + // ============================================ + // Social Media Links + // ============================================ + + /** Twitter/X profile URL */ + @Column({ length: 255, nullable: true }) + twitterUrl?: string; + + /** LinkedIn profile/company URL */ + @Column({ length: 255, nullable: true }) + linkedinUrl?: string; + + /** GitHub organization URL */ + @Column({ length: 255, nullable: true }) + githubUrl?: string; + + /** Facebook page URL */ + @Column({ length: 255, nullable: true }) + facebookUrl?: string; + + /** Instagram profile URL */ + @Column({ length: 255, nullable: true }) + instagramUrl?: string; + + /** YouTube channel URL */ + @Column({ length: 255, nullable: true }) + youtubeUrl?: string; + + /** Additional social links (JSON array of {name, url}) */ + @Column('text', { nullable: true }) + additionalSocialLinks?: string; // JSON array + + // ============================================ + // Contact Information + // ============================================ + + /** Primary contact email */ + @Column({ length: 255, nullable: true }) + contactEmail?: string; + + /** Primary phone number */ + @Column({ length: 50, nullable: true }) + contactPhone?: string; + + /** Physical address */ + @Column('text', { nullable: true }) + address?: string; + + // ============================================ + // Analytics & Verification + // ============================================ + + /** Google Analytics measurement ID (e.g., G-XXXXXXXXXX) */ + @Column({ length: 50, nullable: true }) + googleAnalyticsId?: string; + + /** Google Search Console verification code */ + @Column({ length: 255, nullable: true }) + googleSiteVerification?: string; + + /** Bing Webmaster Tools verification code */ + @Column({ length: 255, nullable: true }) + bingVerification?: string; + + // ============================================ + // Timestamps + // ============================================ + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + // ============================================ + // Helper Methods + // ============================================ + + /** + * Get all social links as array + */ + getAllSocialLinks(): Array<{ platform: string; url: string }> { + const links: Array<{ platform: string; url: string }> = []; + + if (this.twitterUrl) links.push({ platform: 'Twitter', url: this.twitterUrl }); + if (this.linkedinUrl) links.push({ platform: 'LinkedIn', url: this.linkedinUrl }); + if (this.githubUrl) links.push({ platform: 'GitHub', url: this.githubUrl }); + if (this.facebookUrl) links.push({ platform: 'Facebook', url: this.facebookUrl }); + if (this.instagramUrl) links.push({ platform: 'Instagram', url: this.instagramUrl }); + if (this.youtubeUrl) links.push({ platform: 'YouTube', url: this.youtubeUrl }); + + // Parse additional links + if (this.additionalSocialLinks) { + try { + const additional = JSON.parse(this.additionalSocialLinks); + links.push(...additional); + } catch { + // Invalid JSON, skip + } + } + + return links; + } + + /** + * Get social links formatted for Schema.org sameAs property + */ + getSameAsLinks(): string[] { + return this.getAllSocialLinks().map(link => link.url); + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/skill-category.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/skill-category.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..03505f46e7140a1ddaea16e07d04b77db4409940 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/skill-category.entity.ts @@ -0,0 +1,108 @@ +/** + * @fileoverview Skill Category Entity + * Organizes skills into named categories with visual customization. + * + * @module entities/skill-category + * @author Armand Richelet-Kleinberg + * @since 1.0.0 + * + * @example + * // Creating a skill category + * const category = new SkillCategory(); + * category.name = 'Frontend'; + * category.description = 'UI development technologies'; + * category.icon = 'Monitor'; + * category.color = '#3B82F6'; + * await repository.save(category); + */ + +import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn, UpdateDateColumn } from 'typeorm'; +import { Skill } from './skill.entity'; + +/** + * Skill Category entity for organizing skills. + * + * @class SkillCategory + * @description Groups related skills together with visual customization options. + * Used in the CV/Resume section for organized skill presentation. + * + * @remarks + * - Categories can have custom colors and icons for visual distinction + * - displayOrder controls the rendering sequence + * - Skills are linked via foreign key relationship + * + * @see {@link Skill} for the related skill entity + */ +@Entity('skill_categories') +export class SkillCategory { + /** + * Unique identifier for the category. + * @type {number} + */ + @PrimaryGeneratedColumn() + id!: number; + + /** + * Display name of the category. + * @type {string} + * @example 'Frontend', 'Backend', 'DevOps', 'Languages' + */ + @Column({ length: 100 }) + name!: string; + + /** + * Optional description of the category. + * @type {string | null} + */ + @Column({ length: 255, nullable: true }) + description!: string; + + /** + * Lucide icon name for visual representation. + * @type {string | null} + * @example 'Monitor', 'Server', 'Cloud', 'Code' + * @see https://lucide.dev/icons for available icons + */ + @Column({ length: 50, nullable: true }) + icon!: string; + + /** + * Hex color code for category styling. + * @type {string | null} + * @example '#3B82F6', '#10B981', '#8B5CF6' + */ + @Column({ length: 7, nullable: true }) + color!: string; + + /** + * Display order for sorting categories. + * @type {number} + * @default 0 + * @remarks Lower values appear first + */ + @Column({ default: 0 }) + displayOrder!: number; + + /** + * Skills belonging to this category. + * @type {Skill[]} + * @remarks One-to-many relationship with Skill entity + */ + @OneToMany(() => Skill, skill => skill.category) + skills!: Skill[]; + + /** + * Record creation timestamp. + * @type {Date} + */ + @CreateDateColumn() + createdAt!: Date; + + /** + * Record last update timestamp. + * @type {Date} + */ + @UpdateDateColumn() + updatedAt!: Date; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/skill.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/skill.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..dc679f21df1938a40597ac9d042fcc56e0b1542b --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/skill.entity.ts @@ -0,0 +1,61 @@ +/** + * @fileoverview Skill Entity + * Represents a skill in the CV with category support. + * Supports userId (new) and legacy profileId/collaboratorId. + * + * @author Armand Richelet-Kleinberg + */ + +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Index } from 'typeorm'; +import { SkillCategory } from './skill-category.entity'; +import { User } from './user.entity'; +import { Collaborator } from './collaborator.entity'; + +@Entity('skills') +@Index('IDX_skills_userId', ['userId']) +@Index('IDX_skills_displayOrder', ['displayOrder']) +export class Skill { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ length: 100 }) + name!: string; + + @Column({ length: 50, nullable: true }) + level!: string; + + @Column({ nullable: true }) + categoryId!: number; + + @ManyToOne(() => SkillCategory, category => category.skills, { nullable: true }) + @JoinColumn({ name: 'categoryId' }) + category!: SkillCategory; + + @Column({ type: 'decimal', precision: 3, scale: 1, nullable: true }) + yearsOfExperience!: number; + + @Column({ default: 0 }) + displayOrder!: number; + + /** @deprecated Use userId instead */ + @Column({ default: 1 }) + profileId!: number; + + /** @deprecated Use userId instead - kept for migration */ + @Column('uuid', { nullable: true }) + collaboratorId?: string; + + @ManyToOne(() => Collaborator, collaborator => collaborator.skills, { nullable: true }) + @JoinColumn({ name: 'collaboratorId' }) + collaborator?: Collaborator; + + /** User who owns this skill (for personal resume) */ + @Column('uuid', { nullable: true }) + userId?: string; + + @ManyToOne(() => User, user => user.skills, { nullable: true }) + @JoinColumn({ name: 'userId' }) + user?: User; +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/structured-data.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/structured-data.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..827867d2d3bd83ee314485cb488ef60a201270ac --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/structured-data.entity.ts @@ -0,0 +1,102 @@ +/** + * @fileoverview StructuredData Entity - JSON-LD structured data storage + * @description Stores Schema.org structured data (JSON-LD) for enhanced + * search engine understanding and rich results in SERPs. + * + * @author Armand Richelet-Kleinberg + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + Index +} from 'typeorm'; +import { Page } from './page.entity'; + +/** + * Common Schema.org types for type safety + */ +export enum SchemaType { + WEBSITE = 'WebSite', + ORGANIZATION = 'Organization', + WEBPAGE = 'WebPage', + ARTICLE = 'Article', + BLOG_POSTING = 'BlogPosting', + PERSON = 'Person', + FAQ_PAGE = 'FAQPage', + BREADCRUMB_LIST = 'BreadcrumbList', + CREATIVE_WORK = 'CreativeWork', + HOW_TO = 'HowTo', + ITEM_LIST = 'ItemList', + PRODUCT = 'Product', + CONTACT_PAGE = 'ContactPage', + ABOUT_PAGE = 'AboutPage' +} + +/** + * Structured data entity for Schema.org JSON-LD storage + * @remarks + * - Flexible JSON storage for any Schema.org type + * - Multiple schemas per page supported + * - Active/inactive toggle for testing + */ +@Entity('structured_data') +export class StructuredData { + @PrimaryGeneratedColumn('uuid') + id!: string; + + /** Related page ID */ + @Column('uuid') + pageId!: string; + + /** Schema.org type (e.g., Article, Organization, FAQPage) */ + @Column({ length: 100 }) + @Index('IDX_structured_data_type') + schemaType!: SchemaType | string; + + /** JSON-LD schema data (flexible object storage) */ + @Column('text') + schemaData!: string; // Stored as JSON string + + /** Whether this schema is active (allows A/B testing) */ + @Column({ default: true }) + isActive!: boolean; + + /** Display order for multiple schemas on same page */ + @Column({ default: 0 }) + displayOrder!: number; + + // ============================================ + // Relations + // ============================================ + + /** Related page */ + @ManyToOne(() => Page, page => page.structuredData, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'pageId' }) + page!: Page; + + // ============================================ + // Helper Methods + // ============================================ + + /** + * Parse JSON schema data + */ + getParsedSchema(): Record { + try { + return JSON.parse(this.schemaData); + } catch { + return {}; + } + } + + /** + * Set schema data from object + */ + setSchemaData(data: Record): void { + this.schemaData = JSON.stringify(data); + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/style-config.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/style-config.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..d65289247f3cfdc378c475e15e53b7b71b631274 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/style-config.entity.ts @@ -0,0 +1,67 @@ +/** + * @fileoverview Style Config Entity + * Represents theme configuration settings in the database. + * + * @author Armand Richelet-Kleinberg + */ + +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; +import { ThemeColorSchemeEnum } from '@arkalliance/startupcms-ai-share'; + +@Entity('style_config') +export class StyleConfig { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ unique: true }) + name!: string; + + @Column({ + type: 'simple-enum', + enum: ThemeColorSchemeEnum, + default: ThemeColorSchemeEnum.LIGHT + }) + colorScheme!: ThemeColorSchemeEnum; + + @Column({ default: '#ffffff' }) + primaryColor!: string; + + @Column({ default: '#000000' }) + secondaryColor!: string; + + @Column({ default: '#3b82f6' }) + accentColor!: string; + + @Column({ default: '#ffffff' }) + backgroundColor!: string; + + @Column({ default: '#1f2937' }) + textColor!: string; + + @Column('simple-json', { nullable: true }) + colorPalette!: { name: string; value: string; description?: string }[]; + + @Column('simple-json', { nullable: true }) + headingFont!: { family: string; fallback: string; weights: string[]; googleFontUrl?: string }; + + @Column('simple-json', { nullable: true }) + bodyFont!: { family: string; fallback: string; weights: string[]; googleFontUrl?: string }; + + @Column({ default: 16 }) + baseFontSize!: number; + + @Column({ default: 4 }) + borderRadius!: number; + + @Column({ default: false }) + isActive!: boolean; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/task.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/task.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..f58cb763df932b206fa28fec08876fc9af16c97a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/task.entity.ts @@ -0,0 +1,176 @@ +/** + * @fileoverview Task Entity + * Represents a work item/accomplishment for a collaborator with recognition features. + * + * Design Philosophy: + * - Public visibility celebrates achievements and learned lessons (emulation) + * - Private visibility protects ongoing work (psychological safety) + * - Deadline metrics emphasize efficiency and growth, never punishment + * + * @author Armand Richelet-Kleinberg + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, + Index +} from 'typeorm'; +import { Collaborator } from './collaborator.entity'; +import { Project } from './project.entity'; + +/** + * Task status values matching TaskStatus enum in Share layer. + */ +export type TaskStatusType = 'backlog' | 'ongoing' | 'achieved' | 'mistake'; + +/** + * Task entity for tracking accomplishments and work items. + * Supports public recognition for achievements and lessons learned. + */ +@Entity('tasks') +export class Task { + @PrimaryGeneratedColumn('uuid') + id!: string; + + // ============================================ + // Core Information + // ============================================ + + /** Task title - short, descriptive */ + @Column({ length: 200 }) + title!: string; + + /** Detailed description of the task/achievement */ + @Column('text', { nullable: true }) + description?: string; + + /** Contributor's role in this task (e.g., "Lead Developer", "Architect") */ + @Column({ length: 100, nullable: true }) + role?: string; + + /** Current workflow status */ + @Index() + @Column({ + type: 'varchar', + length: 20, + default: 'backlog' + }) + status!: TaskStatusType; + + // ============================================ + // Deadline & Performance Metrics + // ============================================ + + /** Estimated effort in hours (set at task creation) */ + @Column({ type: 'float', nullable: true }) + estimatedDurationHours?: number; + + /** Actual effort in hours (recorded at completion) */ + @Column({ type: 'float', nullable: true }) + actualDurationHours?: number; + + /** Target completion date */ + @Column({ type: 'date', nullable: true }) + dueDate?: Date; + + /** Actual completion date */ + @Column({ type: 'date', nullable: true }) + closingDate?: Date; + + // ============================================ + // Peer Evaluation (Recognition) + // ============================================ + + /** Average peer rating (1-5 stars) */ + @Column({ type: 'float', default: 0 }) + averageRating!: number; + + /** Number of ratings received */ + @Column({ type: 'int', default: 0 }) + ratingCount!: number; + + // ============================================ + // Lesson Learned (Growth Mindset) + // ============================================ + + /** Whether a valuable lesson was extracted (makes mistakes visible publicly) */ + @Column({ default: false }) + lessonLearned!: boolean; + + /** Title/summary of the lesson (safe for public display) */ + @Column({ length: 200, nullable: true }) + lessonTitle?: string; + + /** Detailed lesson description (internal only) */ + @Column('text', { nullable: true }) + lessonDescription?: string; + + // ============================================ + // Display Order & Metadata + // ============================================ + + /** Priority/display order within collaborator's tasks */ + @Column({ default: 0 }) + displayOrder!: number; + + /** Whether to highlight this task prominently */ + @Column({ default: false }) + isHighlighted!: boolean; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + // ============================================ + // Relations + // ============================================ + + /** Collaborator who owns/performed this task */ + @Index() + @Column('uuid') + collaboratorId!: string; + + @ManyToOne(() => Collaborator, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'collaboratorId' }) + collaborator!: Collaborator; + + /** Optional related project */ + @Column('uuid', { nullable: true }) + projectId?: string; + + @ManyToOne(() => Project, { nullable: true, onDelete: 'SET NULL' }) + @JoinColumn({ name: 'projectId' }) + project?: Project; + + // ============================================ + // Computed Properties + // ============================================ + + /** + * Calculate deadline performance in hours. + * Positive = ahead of schedule, Negative = overrun. + */ + get deadlinePerformanceHours(): number | null { + if (this.estimatedDurationHours != null && this.actualDurationHours != null) { + return this.estimatedDurationHours - this.actualDurationHours; + } + return null; + } + + /** + * Check if this task qualifies for public visibility. + * Only Achieved tasks and Mistakes with lessons are public. + */ + get isPubliclyVisible(): boolean { + if (this.status === 'achieved') return true; + if (this.status === 'mistake' && this.lessonLearned) return true; + return false; + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/team-member.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/team-member.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..c98d24b893d09dc6ad4cb4e43146dda9f6466d41 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/team-member.entity.ts @@ -0,0 +1,26 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity('team_members') +export class TeamMember { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column() + name!: string; + + @Column() + role!: string; + + @Column({ nullable: true }) + avatarUrl!: string; + + @Column({ nullable: true }) + bio!: string; + + @Column({ nullable: true }) + githubUrl!: string; + + @Column({ nullable: true }) + linkedinUrl!: string; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/technology.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/technology.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..66d0e5dd1853b310e68001ce45bd50dee2236ab1 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/technology.entity.ts @@ -0,0 +1,149 @@ +/** + * @fileoverview Technology Entity + * Represents a technology/framework used in portfolio projects. + * + * @module database/entities/technology.entity + * @author Armand Richelet-Kleinberg + * + * @description + * This entity stores master data for technologies, making them + * extensible via database without code changes. Each technology + * includes metadata like category, description, icon, and color + * for rich UI rendering. + * + * @example + * ```typescript + * const tech = new Technology(); + * tech.key = 'react'; + * tech.name = 'React'; + * tech.label = 'React.js'; + * tech.category = 'frontend'; + * tech.description = 'A JavaScript library for building user interfaces'; + * tech.icon = 'fab fa-react'; + * tech.color = '#61DAFB'; + * tech.website = 'https://react.dev'; + * ``` + */ + +import { Entity, PrimaryColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'; + +/** + * Technology category enumeration for filtering and grouping. + */ +export enum TechnologyCategory { + FRONTEND = 'frontend', + BACKEND = 'backend', + LANGUAGES = 'languages', + RUNTIMES = 'runtimes', + DATABASES = 'databases', + CLOUD = 'cloud', + DEVOPS = 'devops', + MESSAGING = 'messaging', + AI = 'ai', + ENTERPRISE = 'enterprise', + PATTERNS = 'patterns', + APIS = 'apis', + TESTING = 'testing', + MOBILE = 'mobile', + STYLING = 'styling' +} + +/** + * Technology entity for storing technology/framework master data. + * + * @class Technology + * @description Stores technologies with rich metadata for display and categorization. + * Technologies are linked to projects via the ProjectTechnology junction entity. + */ +@Entity('technologies') +export class Technology { + /** + * Unique key identifier (kebab-case, e.g., 'react', 'typescript', 'dotnet-8'). + * Used as the primary key for stable references. + */ + @PrimaryColumn({ length: 50 }) + key!: string; + + /** + * Display name (e.g., 'React', 'TypeScript', '.NET 8'). + */ + @Column({ length: 100 }) + name!: string; + + /** + * Extended label for display (e.g., 'React.js', 'Microsoft .NET 8'). + * Falls back to name if not provided. + */ + @Column({ length: 150, nullable: true }) + label?: string; + + /** + * Category for grouping (frontend, backend, databases, etc.). + */ + @Column({ length: 50 }) + category!: string; + + /** + * Brief description of the technology. + */ + @Column({ type: 'text', nullable: true }) + description?: string; + + /** + * Common abbreviation or acronym. + * @example 'PoW', 'ERC-20', 'ETH', 'SOL' + */ + @Column({ length: 20, nullable: true }) + abbreviation?: string; + + /** + * Icon class (Font Awesome or Devicon). + * @example 'fab fa-react', 'devicon-typescript-plain' + */ + @Column({ length: 100, nullable: true }) + icon?: string; + + /** + * Brand color in hex format. + * @example '#61DAFB' + */ + @Column({ length: 20, nullable: true }) + color?: string; + + /** + * Official website URL. + */ + @Column({ nullable: true }) + website?: string; + + /** + * Supported versions (optional, JSON array). + * @example ['16', '17', '18', '19'] + */ + @Column({ type: 'simple-array', nullable: true }) + versions?: string[]; + + /** + * Display order within category. + */ + @Column({ type: 'int', default: 0 }) + order!: number; + + /** + * Whether this technology is actively used/displayed. + */ + @Column({ default: true }) + isActive!: boolean; + + /** + * Creation timestamp. + */ + @CreateDateColumn() + createdAt!: Date; + + /** + * Last update timestamp. + */ + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/theme.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/theme.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..0505c40d2e646c4348373434e5c931bd84d24b54 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/theme.entity.ts @@ -0,0 +1,111 @@ +/** + * @fileoverview Theme Entity + * Represents a visual theme stored in the database with full CSS content. + * + * @module database/entities/theme.entity + * @author Armand Richelet-Kleinberg + * + * @description + * This entity stores complete theme CSS content in the database, + * enabling dynamic theme loading and switching without redeployment. + * Each theme contains pre-compiled CSS that is injected at runtime. + * + * @example + * ```typescript + * const theme = new Theme(); + * theme.name = 'Default Cyber'; + * theme.slug = 'default-cyber'; + * theme.cssContent = ':root { --neon-primary: #00d4ff; ... }'; + * theme.isDefault = true; + * ``` + */ + +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +/** + * Theme entity for storing visual themes with full CSS content. + * + * @class Theme + * @description Stores themes with complete CSS for dynamic runtime injection. + * Themes are loaded via API and injected into the document head. + */ +@Entity('themes') +export class Theme { + /** + * Auto-generated primary key. + */ + @PrimaryGeneratedColumn() + id!: number; + + /** + * Human-readable theme name. + * @example 'Default Cyber', 'Neon Cyber', 'Minimal' + */ + @Column({ length: 100 }) + name!: string; + + /** + * URL-safe unique identifier (kebab-case). + * @example 'default-cyber', 'neon-cyber', 'minimal' + */ + @Column({ length: 100, unique: true }) + slug!: string; + + /** + * Brief description of the theme style. + */ + @Column({ type: 'text', nullable: true }) + description?: string; + + /** + * Full CSS content for this theme. + * Contains CSS variables, utility classes, and component styles. + */ + @Column({ type: 'text' }) + cssContent!: string; + + /** + * Preview color for theme selector (hex). + * @example '#00d4ff' + */ + @Column({ length: 20, nullable: true }) + previewColor?: string; + + /** + * Icon or emoji for theme selector. + * @example '⚡', '✨', '○', '◇' + */ + @Column({ length: 10, nullable: true }) + icon?: string; + + /** + * Whether this is the default theme. + * Only one theme should have this set to true. + */ + @Column({ default: false }) + isDefault!: boolean; + + /** + * Display order in theme selector. + */ + @Column({ type: 'int', default: 0 }) + order!: number; + + /** + * Whether this theme is active/visible. + */ + @Column({ default: true }) + isActive!: boolean; + + /** + * Creation timestamp. + */ + @CreateDateColumn() + createdAt!: Date; + + /** + * Last update timestamp. + */ + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/user-role.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/user-role.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..8043626130ec87c3f5e202b72b2d51c7f30e452b --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/user-role.entity.ts @@ -0,0 +1,60 @@ +/** + * @fileoverview User Role Entity + * Junction table for many-to-many relationship between User and Role. + * Enables multi-role support where users can have multiple roles simultaneously. + * + * @author Armand Richelet-Kleinberg + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, + Unique, + Index +} from 'typeorm'; +import { User } from './user.entity'; +import { Role } from '@arkalliance/startupcms-ai-share'; + +/** + * UserRole junction entity for multi-role support. + * @remarks + * - Each row represents one role assigned to one user + * - Unique constraint prevents duplicate role assignments + * - Tracks who assigned the role and when + */ +@Entity('user_roles') +@Unique(['userId', 'role']) +export class UserRole { + @PrimaryGeneratedColumn() + id!: number; + + @Column('uuid') + userId!: string; + + @ManyToOne(() => User, user => user.userRoles, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'userId' }) + user!: User; + + /** Role enum value (stored as string for SQLite compatibility) */ + @Index() + @Column({ type: 'varchar', length: 50 }) + role!: Role; + + /** When was this role assigned */ + @CreateDateColumn() + assignedAt!: Date; + + /** Who assigned this role (for audit trail) */ + @Column('uuid', { nullable: true }) + assignedById?: string; + + @ManyToOne(() => User, { nullable: true, onDelete: 'SET NULL' }) + @JoinColumn({ name: 'assignedById' }) + assignedBy?: User; +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/user.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/user.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b9100a7e081a2f799fa4ee77210f6452b749b14 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/user.entity.ts @@ -0,0 +1,220 @@ +/** + * @fileoverview User Entity + * Extended to support multi-role RBAC, email confirmation, lockout, + * personal information, and resume data ownership. + * + * @author Armand Richelet-Kleinberg + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + OneToMany, + OneToOne +} from 'typeorm'; +import { UserRole } from './user-role.entity'; +import { RefreshToken } from './refresh-token.entity'; +import { Experience } from './experience.entity'; +import { Education } from './education.entity'; +import { Skill } from './skill.entity'; +import { Language } from './language.entity'; +import { Hobby } from './hobby.entity'; +import { BusinessDomain } from './business-domain.entity'; +import { Role } from '@arkalliance/startupcms-ai-share'; + +/** + * User entity for authentication and personal resume data. + * @remarks + * - Supports multi-role via UserRole junction table + * - Owns resume data (experiences, education, skills, etc.) + * - Can be optionally linked to Collaborator for team membership + */ +@Entity('users') +export class User { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ unique: true }) + username!: string; + + @Column() + passwordHash!: string; + + // ============================================ + // Personal Information + // ============================================ + + /** User's first name */ + @Column({ length: 100, nullable: true }) + firstName?: string; + + /** User's last name */ + @Column({ length: 100, nullable: true }) + lastName?: string; + + /** User's email address */ + @Column({ unique: true, nullable: true }) + email?: string; + + /** User avatar/photo URL */ + @Column({ length: 500, nullable: true }) + avatarUrl?: string; + + /** Bio/overview text for resume */ + @Column('text', { nullable: true }) + bio?: string; + + /** LinkedIn profile URL */ + @Column({ length: 255, nullable: true }) + linkedinUrl?: string; + + /** GitHub profile URL */ + @Column({ length: 255, nullable: true }) + githubUrl?: string; + + /** Twitter/X profile URL */ + @Column({ length: 255, nullable: true }) + twitterUrl?: string; + + /** Job title for resume display */ + @Column({ length: 200, nullable: true }) + title?: string; + + // ============================================ + // Account Status + // ============================================ + + /** + * @deprecated Use userRoles relation instead. + * Kept for backward compatibility during migration. + */ + @Column({ default: 'admin', name: 'role' }) + roleLegacy!: string; + + /** Whether user account is active */ + @Column({ default: true }) + isActive!: boolean; + + /** Whether email has been confirmed */ + @Column({ default: false }) + emailConfirmed!: boolean; + + /** Token for email confirmation */ + @Column({ length: 255, nullable: true }) + emailConfirmToken?: string; + + /** When email confirmation token expires */ + @Column({ nullable: true }) + emailConfirmExpiry?: Date; + + /** Token for password reset */ + @Column({ length: 255, nullable: true }) + passwordResetToken?: string; + + /** When password reset token expires */ + @Column({ nullable: true }) + passwordResetExpiry?: Date; + + /** Number of consecutive failed login attempts */ + @Column({ default: 0 }) + failedLoginAttempts!: number; + + /** Account locked until this time */ + @Column({ nullable: true }) + lockoutUntil?: Date; + + @Column({ nullable: true }) + lastLogin?: Date; + + /** Linked collaborator for team membership */ + @Column('uuid', { nullable: true }) + collaboratorId?: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + // ============================================ + // Authentication Relations + // ============================================ + + @OneToMany(() => UserRole, userRole => userRole.user) + userRoles!: UserRole[]; + + @OneToMany(() => RefreshToken, token => token.user) + refreshTokens!: RefreshToken[]; + + // ============================================ + // Resume Data Relations (Personal) + // ============================================ + + @OneToMany(() => Experience, experience => experience.user) + experiences!: Experience[]; + + @OneToMany(() => Education, education => education.user) + educations!: Education[]; + + @OneToMany(() => Skill, skill => skill.user) + skills!: Skill[]; + + @OneToMany(() => Language, language => language.user) + languages!: Language[]; + + @OneToMany(() => Hobby, hobby => hobby.user) + hobbies!: Hobby[]; + + @OneToMany(() => BusinessDomain, domain => domain.user) + businessDomains!: BusinessDomain[]; + + // ============================================ + // Computed Properties + // ============================================ + + /** Full name for display */ + get fullName(): string { + if (this.firstName && this.lastName) { + return `${this.firstName} ${this.lastName}`; + } + return this.username; + } + + /** + * Get all role names for this user. + */ + get roles(): Role[] { + if (!this.userRoles || this.userRoles.length === 0) { + // Fallback to legacy role if no UserRoles + return this.roleLegacy ? [this.roleLegacy as Role] : []; + } + return this.userRoles.map(ur => ur.role); + } + + /** + * Check if user has a specific role. + */ + hasRole(role: Role): boolean { + return this.roles.includes(role); + } + + /** + * Check if user has any of the specified roles. + */ + hasAnyRole(...roles: Role[]): boolean { + return roles.some(role => this.hasRole(role)); + } + + /** + * Check if account is currently locked. + */ + get isLocked(): boolean { + return this.lockoutUntil != null && new Date() < this.lockoutUntil; + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/widget.entity.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/widget.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..f0fe0f40d70aba486020771348d0466e789b551d --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/entities/widget.entity.ts @@ -0,0 +1,43 @@ +/** + * @fileoverview Widget Entity + * Database entity for dynamic widgets on portfolio pages. + * + * @author Armand Richelet-Kleinberg + */ + +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; +import { WidgetTypeEnum } from '@arkalliance/startupcms-ai-share'; + +@Entity('widgets') +export class Widget { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ + type: 'text', // using text for sqlite compatibility + default: WidgetTypeEnum.TEXT + }) + type!: WidgetTypeEnum; + + @Column() + title!: string; + + @Column({ default: 0 }) + order!: number; + + @Column('simple-json', { nullable: true }) + config!: Record; // Flexible JSON storage for specific widget settings + + @Column({ nullable: true }) + context!: string; // e.g., 'HOME', 'PROJECT_123' + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} + + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/ai-settings.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/ai-settings.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe61e0befb88011097e6a8bf02c6f8f15b158ba1 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/ai-settings.repository.ts @@ -0,0 +1,22 @@ +/** + * @fileoverview AI Settings Repository + * Repository for AI configuration management. + */ + +import { GenericRepository } from './generic.repository'; +import { AiSettings } from '../entities/ai-settings.entity'; + +export class AiSettingsRepository extends GenericRepository { + constructor() { + super(AiSettings); + } + + async getActive(): Promise { + return this.repository.findOne({ where: { isActive: true } }); + } + + async getFirst(): Promise { + return this.repository.findOne({ where: {} }); + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/audit-log.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/audit-log.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..70d67ee662b67f5bae4189c91ce109ed3a5a7b87 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/audit-log.repository.ts @@ -0,0 +1,147 @@ +/** + * @fileoverview Audit Log Repository + * Data access layer for AuditLog entity with query support. + * + * @author Armand Richelet-Kleinberg + */ + +import { Repository, DataSource, Between, Like, In } from 'typeorm'; +import { AuditLog } from '../entities/audit-log.entity'; +import { AuditAction } from '@arkalliance/startupcms-ai-share'; +import { AppDataSource } from '../../config/database'; + +/** + * Query options for audit log search. + */ +export interface AuditLogQueryOptions { + userId?: string; + actions?: AuditAction[]; + startDate?: Date; + endDate?: Date; + success?: boolean; + ipAddress?: string; + page?: number; + limit?: number; +} + +/** + * Repository for AuditLog entity operations. + */ +export class AuditLogRepository { + private repository: Repository; + + constructor(dataSource?: DataSource) { + this.repository = (dataSource || AppDataSource).getRepository(AuditLog); + } + + /** + * Create a new audit log entry. + */ + async log(entry: { + userId?: string; + action: AuditAction; + ipAddress?: string; + userAgent?: string; + success?: boolean; + details?: Record; + }): Promise { + const auditLog = this.repository.create({ + ...entry, + success: entry.success ?? true, + details: entry.details ? JSON.stringify(entry.details) : undefined + }); + return this.repository.save(auditLog); + } + + /** + * Query audit logs with filters. + */ + async query(options: AuditLogQueryOptions): Promise<{ logs: AuditLog[]; total: number }> { + const { + userId, + actions, + startDate, + endDate, + success, + ipAddress, + page = 1, + limit = 50 + } = options; + + const queryBuilder = this.repository.createQueryBuilder('log') + .leftJoinAndSelect('log.user', 'user') + .orderBy('log.createdAt', 'DESC'); + + if (userId) { + queryBuilder.andWhere('log.userId = :userId', { userId }); + } + + if (actions && actions.length > 0) { + queryBuilder.andWhere('log.action IN (:...actions)', { actions }); + } + + if (startDate && endDate) { + queryBuilder.andWhere('log.createdAt BETWEEN :startDate AND :endDate', { + startDate, + endDate + }); + } else if (startDate) { + queryBuilder.andWhere('log.createdAt >= :startDate', { startDate }); + } else if (endDate) { + queryBuilder.andWhere('log.createdAt <= :endDate', { endDate }); + } + + if (success !== undefined) { + queryBuilder.andWhere('log.success = :success', { success }); + } + + if (ipAddress) { + queryBuilder.andWhere('log.ipAddress LIKE :ipAddress', { ipAddress: `%${ipAddress}%` }); + } + + const total = await queryBuilder.getCount(); + const logs = await queryBuilder + .skip((page - 1) * limit) + .take(limit) + .getMany(); + + return { logs, total }; + } + + /** + * Get recent login attempts for a user. + */ + async getRecentLoginAttempts(userId: string, limit = 10): Promise { + return this.repository.find({ + where: { + userId, + action: In([AuditAction.LOGIN_SUCCESS, AuditAction.LOGIN_FAILURE]) + }, + order: { createdAt: 'DESC' }, + take: limit + }); + } + + /** + * Get failed login count in last X minutes. + */ + async getRecentFailedLoginCount(userId: string, minutes: number): Promise { + const since = new Date(Date.now() - minutes * 60 * 1000); + return this.repository.count({ + where: { + userId, + action: AuditAction.LOGIN_FAILURE, + createdAt: Between(since, new Date()) + } + }); + } + + /** + * Count total events. + */ + async countAll(): Promise { + return this.repository.count(); + } +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/carousel.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/carousel.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..bab475afd9a2abe65001790825ec800899699074 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/carousel.repository.ts @@ -0,0 +1,42 @@ +/** + * @fileoverview Carousel Repository + * Data access layer for carousel item entities. + * + * @author Armand Richelet-Kleinberg + */ + +import { GenericRepository } from './generic.repository'; +import { CarouselItem } from '../entities/carousel-item.entity'; + +export class CarouselRepository extends GenericRepository { + constructor() { + super(CarouselItem); + } + + /** + * Get all carousel items ordered by display order. + */ + async findAllOrdered(): Promise { + return this.repository.find({ order: { order: 'ASC' } }); + } + + /** + * Get only active carousel items. + */ + async findActive(): Promise { + return this.repository.find({ + where: { isActive: true }, + order: { order: 'ASC' } + }); + } + + /** + * Update order of carousel items. + */ + async updateOrder(itemIds: number[]): Promise { + for (let i = 0; i < itemIds.length; i++) { + await this.repository.update(itemIds[i], { order: i }); + } + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/collaborator.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/collaborator.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b83e0c0e774d230b51350e4c0de9773152a7793 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/collaborator.repository.ts @@ -0,0 +1,106 @@ +/** + * @fileoverview Collaborator Repository + * Data access layer for Collaborator entity with hierarchy support. + * + * @author Armand Richelet-Kleinberg + */ + +import { Repository, DataSource, IsNull } from 'typeorm'; +import { Collaborator } from '../entities/collaborator.entity'; +import { AppDataSource } from '../../config/database'; + +/** + * Repository for Collaborator entity operations. + */ +export class CollaboratorRepository { + private repository: Repository; + + constructor(dataSource?: DataSource) { + this.repository = (dataSource || AppDataSource).getRepository(Collaborator); + } + + /** + * Find collaborator by ID. + */ + async findById(id: string): Promise { + return this.repository.findOne({ + where: { id }, + relations: ['organization', 'reportsTo', 'directReports'] + }); + } + + /** + * Find collaborator by user ID. + */ + async findByUserId(userId: string): Promise { + return this.repository.findOne({ + where: { userId }, + relations: ['organization'] + }); + } + + /** + * Get all collaborators for an organization. + */ + async findByOrganization(organizationId: string): Promise { + return this.repository.find({ + where: { organizationId, isActive: true }, + relations: ['reportsTo'], + order: { displayOrder: 'ASC', firstName: 'ASC' } + }); + } + + /** + * Get top-level collaborators (no manager). + */ + async findRootCollaborators(organizationId: string): Promise { + return this.repository.find({ + where: { organizationId, reportsToId: IsNull(), isActive: true }, + relations: ['directReports'], + order: { displayOrder: 'ASC' } + }); + } + + /** + * Get direct reports for a collaborator. + */ + async findDirectReports(collaboratorId: string): Promise { + return this.repository.find({ + where: { reportsToId: collaboratorId, isActive: true }, + order: { displayOrder: 'ASC', firstName: 'ASC' } + }); + } + + /** + * Create new collaborator. + */ + async create(data: Partial): Promise { + const collaborator = this.repository.create(data); + return this.repository.save(collaborator); + } + + /** + * Update collaborator. + */ + async update(id: string, data: Partial): Promise { + await this.repository.update(id, data); + return this.findById(id); + } + + /** + * Update hierarchy (change manager). + */ + async updateReportsTo(id: string, reportsToId: string | null): Promise { + const result = await this.repository.update(id, { reportsToId: reportsToId || undefined }); + return (result.affected ?? 0) > 0; + } + + /** + * Count collaborators in organization. + */ + async countByOrganization(organizationId: string): Promise { + return this.repository.count({ + where: { organizationId, isActive: true } + }); + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/education.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/education.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..33815182c8774117a51664de1d6a37af35cb840e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/education.repository.ts @@ -0,0 +1,20 @@ +/** + * @fileoverview Education Repository + * Repository for education management. + */ + +import { GenericRepository } from './generic.repository'; +import { Education } from '../entities/education.entity'; + +export class EducationRepository extends GenericRepository { + constructor() { + super(Education); + } + + async findAllOrdered(): Promise { + return this.repository.find({ + order: { startDate: 'DESC' } + }); + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/experience.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/experience.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..39ee9e6989753039aa1a8eac1576f03b533ded39 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/experience.repository.ts @@ -0,0 +1,33 @@ +/** + * @fileoverview Experience Repository + * Repository for experience management. + */ + +import { GenericRepository } from './generic.repository'; +import { Experience } from '../entities/experience.entity'; + +export class ExperienceRepository extends GenericRepository { + constructor() { + super(Experience); + } + + async findAllOrdered(): Promise { + return this.repository.find({ + order: { displayOrder: 'ASC', startDate: 'DESC' } + }); + } + + async findHighlighted(): Promise { + return this.repository.find({ + where: { isHighlighted: true }, + order: { startDate: 'DESC' } + }); + } + + async updateOrder(experienceIds: number[]): Promise { + for (let i = 0; i < experienceIds.length; i++) { + await this.repository.update(experienceIds[i], { displayOrder: i }); + } + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/generic.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/generic.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..f4849c9f110ccd03f631b3caf7623a76259480d5 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/generic.repository.ts @@ -0,0 +1,35 @@ +import { Repository, DeepPartial, FindOptionsWhere, FindOneOptions } from 'typeorm'; +import { AppDataSource } from '../../config/database'; + +export class GenericRepository { + protected repository: Repository; + + constructor(entity: any) { + this.repository = AppDataSource.getRepository(entity); + } + + async findAll(options?: any): Promise { + return this.repository.find(options); + } + + async findById(id: any, options?: FindOneOptions): Promise { + // @ts-ignore - TypeORM generic handling + return this.repository.findOne({ where: { id }, ...options }); + } + + async create(data: DeepPartial): Promise { + const entity = this.repository.create(data); + return this.repository.save(entity); + } + + async update(id: any, data: DeepPartial): Promise { + // @ts-ignore + await this.repository.update(id, data); + return this.findById(id); + } + + async delete(id: any): Promise { + await this.repository.delete(id); + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/media.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/media.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6c47829252554f354c3db8ea4ff2b99bb29922e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/media.repository.ts @@ -0,0 +1,33 @@ +/** + * @fileoverview Media Repository + * Data access layer for media asset entities. + * + * @author Armand Richelet-Kleinberg + */ + +import { GenericRepository } from './generic.repository'; +import { Media } from '../entities/media.entity'; + +export class MediaRepository extends GenericRepository { + constructor() { + super(Media); + } + + /** + * Get media by type filter. + */ + async findByType(type: string): Promise { + return this.repository.find({ + where: { type: type as any }, + order: { createdAt: 'DESC' } + }); + } + + /** + * Get all media ordered by creation date. + */ + async findAllOrdered(): Promise { + return this.repository.find({ order: { createdAt: 'DESC' } }); + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/menu.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/menu.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..22e3bad32001d4eb56a617dba0ebe862d7152f96 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/menu.repository.ts @@ -0,0 +1,45 @@ +/** + * @fileoverview Menu Repository + * Data access layer for menu item entities. + * + * @author Armand Richelet-Kleinberg + */ + +import { GenericRepository } from './generic.repository'; +import { MenuItem } from '../entities/menu-item.entity'; +import { MenuPositionEnum } from '@arkalliance/startupcms-ai-share'; + +export class MenuRepository extends GenericRepository { + constructor() { + super(MenuItem); + } + + /** + * Get menu items by position. + */ + async findByPosition(position: MenuPositionEnum): Promise { + return this.repository.find({ + where: { position: position as any }, + order: { order: 'ASC' } + }); + } + + /** + * Get all menu items ordered. + */ + async findAllOrdered(): Promise { + return this.repository.find({ order: { position: 'ASC', order: 'ASC' } }); + } + + /** + * Update order of menu items. + */ + async updateOrder(itemIds: number[]): Promise { + for (let i = 0; i < itemIds.length; i++) { + await this.repository.update(itemIds[i], { order: i }); + } + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/organization.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/organization.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..ad02760b1e8fac7529cc52681a6a1f4a2fa4dcec --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/organization.repository.ts @@ -0,0 +1,64 @@ +/** + * @fileoverview Organization Repository + * Data access layer for Organization entity. + * + * @author Armand Richelet-Kleinberg + */ + +import { Repository, DataSource } from 'typeorm'; +import { Organization } from '../entities/organization.entity'; +import { AppDataSource } from '../../config/database'; + +/** + * Repository for Organization entity operations. + */ +export class OrganizationRepository { + private repository: Repository; + + constructor(dataSource?: DataSource) { + this.repository = (dataSource || AppDataSource).getRepository(Organization); + } + + /** + * Get the default/active organization. + */ + async getDefault(): Promise { + return this.repository.findOne({ + where: { isActive: true }, + order: { createdAt: 'ASC' } + }); + } + + /** + * Find organization by ID. + */ + async findById(id: string): Promise { + return this.repository.findOne({ where: { id } }); + } + + /** + * Find organization with team count. + */ + async findWithStats(id: string): Promise { + return this.repository.findOne({ + where: { id }, + relations: ['collaborators'] + }); + } + + /** + * Update organization. + */ + async update(id: string, data: Partial): Promise { + await this.repository.update(id, data); + return this.findById(id); + } + + /** + * Create new organization. + */ + async create(data: Partial): Promise { + const org = this.repository.create(data); + return this.repository.save(org); + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/profile.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/profile.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..919b9c2fef3755b0406fcd38a0cd4fb83a777606 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/profile.repository.ts @@ -0,0 +1,5 @@ +import { AppDataSource } from '../../config/database'; +import { Profile } from '../entities/profile.entity'; + +export const ProfileRepository = AppDataSource.getRepository(Profile); + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/project.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/project.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e78153029eeb6903086562e76cd4051d7fd6042 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/project.repository.ts @@ -0,0 +1,69 @@ +import { GenericRepository } from './generic.repository'; +import { Project } from '../entities/project.entity'; + +export class ProjectRepository extends GenericRepository { + constructor() { + super(Project); + } + + /** + * Find a project by UUID. + * @param id - UUID of the project + * @returns Project with full relations or null + */ + async findFullProjectTree(id: string): Promise { + // First try as UUID + let project = await this.repository.findOne({ + where: { id }, + relations: { + technologies: true, + pages: true, + features: true, + controllers: { + endpoints: true + } + } + }); + + // If not found and not a UUID pattern, try as slug + if (!project) { + project = await this.findBySlug(id); + } + + return project; + } + + /** + * Find a project by slug (derived from title). + * Slug conversion: lowercase, dots and spaces to hyphens. + * + * @param slug - URL-friendly slug (e.g., 'ark-alliance') + * @returns Project with full relations or null + */ + async findBySlug(slug: string): Promise { + const projects = await this.repository.find({ + relations: { + technologies: true, + pages: true, + features: true, + controllers: { + endpoints: true + } + } + }); + + // Convert slug back to title pattern and find matching project + const normalizedSlug = slug.toLowerCase(); + const project = projects.find(p => { + const projectSlug = p.title + .toLowerCase() + .replace(/\./g, '-') + .replace(/\s+/g, '-') + .replace(/-+/g, '-'); + return projectSlug === normalizedSlug; + }); + + return project || null; + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/resume.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/resume.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..f50acf4d45cc2338812e30d795e84836e372469a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/resume.repository.ts @@ -0,0 +1,9 @@ +import { AppDataSource } from '../../config/database'; +import { Education } from '../entities/education.entity'; +import { Experience } from '../entities/experience.entity'; +import { Skill } from '../entities/skill.entity'; + +export const EducationRepository = AppDataSource.getRepository(Education); +export const ExperienceRepository = AppDataSource.getRepository(Experience); +export const SkillRepository = AppDataSource.getRepository(Skill); + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/skill-category.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/skill-category.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..099e03466b09d17d0e00c95e931b5743dd553daf --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/skill-category.repository.ts @@ -0,0 +1,27 @@ +/** + * @fileoverview Skill Category Repository + * Repository for skill category management. + */ + +import { GenericRepository } from './generic.repository'; +import { SkillCategory } from '../entities/skill-category.entity'; + +export class SkillCategoryRepository extends GenericRepository { + constructor() { + super(SkillCategory); + } + + async findAllOrdered(): Promise { + return this.repository.find({ + order: { displayOrder: 'ASC', name: 'ASC' }, + relations: ['skills'] + }); + } + + async updateOrder(categoryIds: number[]): Promise { + for (let i = 0; i < categoryIds.length; i++) { + await this.repository.update(categoryIds[i], { displayOrder: i }); + } + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/skill.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/skill.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..429d4657004fc11758823c8a76e1307fba3ffbf5 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/skill.repository.ts @@ -0,0 +1,34 @@ +/** + * @fileoverview Skill Repository + * Repository for skill management with category support. + */ + +import { GenericRepository } from './generic.repository'; +import { Skill } from '../entities/skill.entity'; + +export class SkillRepository extends GenericRepository { + constructor() { + super(Skill); + } + + async findAllWithCategories(): Promise { + return this.repository.find({ + order: { displayOrder: 'ASC', name: 'ASC' }, + relations: ['category'] + }); + } + + async findByCategory(categoryId: number): Promise { + return this.repository.find({ + where: { categoryId }, + order: { displayOrder: 'ASC' } + }); + } + + async updateOrder(skillIds: number[]): Promise { + for (let i = 0; i < skillIds.length; i++) { + await this.repository.update(skillIds[i], { displayOrder: i }); + } + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/style.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/style.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..aaa113ca8312f971b8dfff121758931431462b54 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/style.repository.ts @@ -0,0 +1,42 @@ +/** + * @fileoverview Style Repository + * Data access layer for style configuration entities. + * + * @author Armand Richelet-Kleinberg + */ + +import { GenericRepository } from './generic.repository'; +import { StyleConfig } from '../entities/style-config.entity'; +import { Not } from 'typeorm'; + +export class StyleRepository extends GenericRepository { + constructor() { + super(StyleConfig); + } + + /** + * Get the currently active style. + */ + async findActive(): Promise { + return this.repository.findOne({ where: { isActive: true } }); + } + + /** + * Activate a style and deactivate all others. + */ + async activateStyle(id: number): Promise { + // Deactivate all styles + await this.repository.update({}, { isActive: false }); + // Activate the selected one + await this.repository.update(id, { isActive: true }); + return this.findById(id); + } + + /** + * Get all styles ordered by name. + */ + async findAllOrdered(): Promise { + return this.repository.find({ order: { name: 'ASC' } }); + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/theme.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/theme.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..14c1a4059799e547be4c31bdc2fda5ee208dbe3b --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/theme.repository.ts @@ -0,0 +1,63 @@ +/** + * @fileoverview Theme Repository + * Repository for theme CRUD operations. + * + * @module database/repositories/theme.repository + * @author Armand Richelet-Kleinberg + */ + +import { GenericRepository } from './generic.repository'; +import { Theme } from '../entities/theme.entity'; + +/** + * Repository for Theme entity operations. + * Extends GenericRepository with theme-specific query methods. + */ +export class ThemeRepository extends GenericRepository { + constructor() { + super(Theme); + } + + /** + * Find all active themes ordered by display order. + * @returns Array of active themes (without CSS content for list view) + */ + async findAllActive(): Promise { + return this.repository.find({ + where: { isActive: true }, + order: { order: 'ASC', name: 'ASC' }, + select: ['id', 'name', 'slug', 'description', 'previewColor', 'icon', 'isDefault', 'order'] + }); + } + + /** + * Find theme by slug identifier. + * @param slug - Theme slug (e.g., 'default-cyber') + * @returns Theme with full CSS content or null + */ + async findBySlug(slug: string): Promise { + return this.repository.findOne({ + where: { slug, isActive: true } + }); + } + + /** + * Find the default theme. + * @returns Default theme with full CSS content or null + */ + async findDefault(): Promise { + return this.repository.findOne({ + where: { isDefault: true, isActive: true } + }); + } + + /** + * Check if a theme with the given slug exists. + * @param slug - Theme slug to check + * @returns True if theme exists + */ + async existsBySlug(slug: string): Promise { + const count = await this.repository.count({ where: { slug } }); + return count > 0; + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/user-role.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/user-role.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb9d9ec61e3fb3e93904b3d3c1658620cb9e6224 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/user-role.repository.ts @@ -0,0 +1,102 @@ +/** + * @fileoverview User Role Repository + * Data access layer for UserRole junction table. + * + * @author Armand Richelet-Kleinberg + */ + +import { Repository, DataSource } from 'typeorm'; +import { UserRole } from '../entities/user-role.entity'; +import { Role } from '@arkalliance/startupcms-ai-share'; +import { AppDataSource } from '../../config/database'; + +/** + * Repository for UserRole entity operations. + */ +export class UserRoleRepository { + private repository: Repository; + + constructor(dataSource?: DataSource) { + this.repository = (dataSource || AppDataSource).getRepository(UserRole); + } + + /** + * Get all roles for a user. + */ + async findByUserId(userId: string): Promise { + return this.repository.find({ + where: { userId }, + relations: ['assignedBy'] + }); + } + + /** + * Check if user has a specific role. + */ + async hasRole(userId: string, role: Role): Promise { + const count = await this.repository.count({ + where: { userId, role } + }); + return count > 0; + } + + /** + * Assign a role to a user. + * Uses optimistic insert pattern to handle concurrent assignments safely. + */ + async assignRole(userId: string, role: Role, assignedById?: string): Promise { + try { + // Optimistic insert - try to create directly + const userRole = this.repository.create({ + userId, + role, + assignedById + }); + return await this.repository.save(userRole); + } catch (error: any) { + // Handle unique constraint violation (concurrent insert or already exists) + const isUniqueViolation = + error.code === 'SQLITE_CONSTRAINT' || + error.code === '23505' || // PostgreSQL + error.code === 'ER_DUP_ENTRY' || // MySQL + error.message?.includes('UNIQUE constraint failed'); + + if (isUniqueViolation) { + // Role already assigned - return existing + const existing = await this.repository.findOne({ + where: { userId, role } + }); + if (existing) return existing; + } + throw error; + } + } + + /** + * Revoke a role from a user. + */ + async revokeRole(userId: string, role: Role): Promise { + const result = await this.repository.delete({ userId, role }); + return (result.affected ?? 0) > 0; + } + + /** + * Get all users with a specific role. + */ + async findUsersByRole(role: Role): Promise { + return this.repository.find({ + where: { role }, + relations: ['user'] + }); + } + + /** + * Revoke all roles from a user. + */ + async revokeAllRoles(userId: string): Promise { + const result = await this.repository.delete({ userId }); + return result.affected ?? 0; + } +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/user.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/user.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ced3ec060a47eeba27d748acaa5e8020ba202ce --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/user.repository.ts @@ -0,0 +1,31 @@ +/** + * @fileoverview User Repository + * Data access layer for user entities. + * + * @author Armand Richelet-Kleinberg + */ + +import { GenericRepository } from './generic.repository'; +import { User } from '../entities/user.entity'; + +export class UserRepository extends GenericRepository { + constructor() { + super(User); + } + + /** + * Find user by username. + */ + async findByUsername(username: string): Promise { + return this.repository.findOne({ where: { username } }); + } + + /** + * Check if username exists. + */ + async usernameExists(username: string): Promise { + const count = await this.repository.count({ where: { username } }); + return count > 0; + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/widget.repository.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/widget.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..84a8b79dbdf221c6a028f4559255bb7cce6da6bc --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/repositories/widget.repository.ts @@ -0,0 +1,16 @@ +import { GenericRepository } from './generic.repository'; +import { Widget } from '../entities/widget.entity'; + +export class WidgetRepository extends GenericRepository { + constructor() { + super(Widget); + } + + async findByContext(context: string): Promise { + return this.repository.find({ + where: { context }, + order: { order: 'ASC' } + }); + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/ai_settings.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/ai_settings.sql new file mode 100644 index 0000000000000000000000000000000000000000..ee35bd664123fa0bb77285516bfd9f187f637b3d --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/ai_settings.sql @@ -0,0 +1,14 @@ +-- AI Settings Table Schema +-- Stores AI provider configuration with encrypted API key +CREATE TABLE IF NOT EXISTS ai_settings ( + id SERIAL PRIMARY KEY, + provider VARCHAR(50) DEFAULT 'openai', + api_url VARCHAR(500), + api_key_encrypted TEXT, + model VARCHAR(100) DEFAULT 'gpt-4', + temperature DECIMAL(3, 2) DEFAULT 0.7, + max_tokens INTEGER DEFAULT 2000, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/business_domain.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/business_domain.sql new file mode 100644 index 0000000000000000000000000000000000000000..31fef59c640078a2329d943a0099437a6de1ba00 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/business_domain.sql @@ -0,0 +1,13 @@ +-- Business Domains Table Schema +-- Stores business domain knowledge and expertise +CREATE TABLE IF NOT EXISTS business_domains ( + id SERIAL PRIMARY KEY, + domain VARCHAR(100) NOT NULL, + level VARCHAR(50), + description TEXT, + years_of_experience INTEGER, + icon VARCHAR(50), + display_order INTEGER DEFAULT 0, + profile_id INTEGER DEFAULT 1 +); +CREATE INDEX IF NOT EXISTS idx_business_domains_display_order ON business_domains(display_order); \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/carousel_item.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/carousel_item.sql new file mode 100644 index 0000000000000000000000000000000000000000..4ce2f97cad40d5e65dcf91adf533d30494148f9c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/carousel_item.sql @@ -0,0 +1,18 @@ +-- Carousel Items Table Schema +-- Stores homepage carousel slides +CREATE TABLE IF NOT EXISTS carousel_items ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + subtitle VARCHAR(255), + description TEXT, + image_url VARCHAR(500) NOT NULL, + link_url VARCHAR(500), + link_text VARCHAR(100), + "order" INTEGER DEFAULT 0, + is_active BOOLEAN DEFAULT TRUE, + project_id VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_carousel_items_order ON carousel_items("order"); +CREATE INDEX IF NOT EXISTS idx_carousel_items_is_active ON carousel_items(is_active);soin \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/hobby.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/hobby.sql new file mode 100644 index 0000000000000000000000000000000000000000..3c3b5ab672fae8e6e777db39617b109110f17775 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/hobby.sql @@ -0,0 +1,11 @@ +-- Hobbies Table Schema +-- Stores personal hobbies and interests +CREATE TABLE IF NOT EXISTS hobbies ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + description TEXT, + icon VARCHAR(50), + display_order INTEGER DEFAULT 0, + profile_id INTEGER DEFAULT 1 +); +CREATE INDEX IF NOT EXISTS idx_hobbies_display_order ON hobbies(display_order); \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/init.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/init.sql new file mode 100644 index 0000000000000000000000000000000000000000..131aa21098a7a8bc1a68bdbb26434ffe67efcc6f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/init.sql @@ -0,0 +1,64 @@ +-- Database Initialization Script for Ark Portfolio + +CREATE TABLE IF NOT EXISTS profiles ( + id SERIAL PRIMARY KEY, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + title VARCHAR(200), + bio TEXT, + email VARCHAR(200) NOT NULL, + linkedin_url VARCHAR(255), + github_url VARCHAR(255), + avatar_url VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS projects ( + id SERIAL PRIMARY KEY, + title VARCHAR(200) NOT NULL, + description TEXT, + status VARCHAR(50) NOT NULL, + image_url VARCHAR(255), + repo_url VARCHAR(255), + demo_url VARCHAR(255), + start_date DATE, + end_date DATE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS project_technologies ( + project_id INTEGER REFERENCES projects(id), + technology VARCHAR(50), + PRIMARY KEY (project_id, technology) +); + +CREATE TABLE IF NOT EXISTS education ( + id SERIAL PRIMARY KEY, + institution VARCHAR(200) NOT NULL, + degree VARCHAR(100), + field_of_study VARCHAR(100), + start_date DATE, + end_date DATE, + description TEXT, + profile_id INTEGER DEFAULT 1 -- Assuming single user portfolio usually +); + +CREATE TABLE IF NOT EXISTS experience ( + id SERIAL PRIMARY KEY, + company VARCHAR(200) NOT NULL, + position VARCHAR(100) NOT NULL, + start_date DATE, + end_date DATE, + description TEXT, + profile_id INTEGER DEFAULT 1 +); + +CREATE TABLE IF NOT EXISTS skills ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + level VARCHAR(50), + category VARCHAR(100), + profile_id INTEGER DEFAULT 1 +); diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/language.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/language.sql new file mode 100644 index 0000000000000000000000000000000000000000..b128231563a4f6a142cd0ac5c65ce2a6ac81279c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/language.sql @@ -0,0 +1,12 @@ +-- Languages Table Schema +-- Stores language proficiency with three dimensions +CREATE TABLE IF NOT EXISTS languages ( + id SERIAL PRIMARY KEY, + language VARCHAR(100) NOT NULL, + speaking INTEGER DEFAULT 1, + writing INTEGER DEFAULT 1, + presenting INTEGER DEFAULT 1, + display_order INTEGER DEFAULT 0, + profile_id INTEGER DEFAULT 1 +); +CREATE INDEX IF NOT EXISTS idx_languages_display_order ON languages(display_order); \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/media.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/media.sql new file mode 100644 index 0000000000000000000000000000000000000000..acef8fffe2d06fe1d4ef40399fb971cf7362ea6c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/media.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS media ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + url VARCHAR(255) NOT NULL, + type TEXT NOT NULL, + alt_text VARCHAR(255), + caption VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/menu_item.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/menu_item.sql new file mode 100644 index 0000000000000000000000000000000000000000..7f7122514f34aee031fd753ff465c9f6cfa2c503 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/menu_item.sql @@ -0,0 +1,17 @@ +-- Menu Items Table Schema +-- Stores navigation menu items with hierarchical support +CREATE TABLE IF NOT EXISTS menu_items ( + id SERIAL PRIMARY KEY, + label VARCHAR(100) NOT NULL, + icon VARCHAR(50), + route VARCHAR(255) NOT NULL, + position VARCHAR(20) DEFAULT 'header', + "order" INTEGER DEFAULT 0, + is_visible BOOLEAN DEFAULT TRUE, + open_in_new_tab BOOLEAN DEFAULT FALSE, + parent_id INTEGER REFERENCES menu_items(id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_menu_items_position ON menu_items(position); +CREATE INDEX IF NOT EXISTS idx_menu_items_parent_id ON menu_items(parent_id); \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/profile.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/profile.sql new file mode 100644 index 0000000000000000000000000000000000000000..9837c77286be2f81d9be52eb1c3592f9fbe8d506 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/profile.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS profiles ( + id SERIAL PRIMARY KEY, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + title VARCHAR(200), + bio TEXT, + email VARCHAR(200) NOT NULL, + linkedin_url VARCHAR(255), + github_url VARCHAR(255), + avatar_url VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project.sql new file mode 100644 index 0000000000000000000000000000000000000000..1c77b783bba4893b37c8b8bd4c4e2a6b84dc0e5d --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS projects ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + title VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + status VARCHAR(50) NOT NULL, + image_url VARCHAR(255), + repo_url VARCHAR(255), + demo_url VARCHAR(255), + start_date DATE, + end_date DATE +); diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_controller.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_controller.sql new file mode 100644 index 0000000000000000000000000000000000000000..8dc3822b1132750a30bc55268d868d811a649caa --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_controller.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS project_controllers ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + base_path VARCHAR(255) NOT NULL, + description VARCHAR(255), + project_id UUID, + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +); diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_endpoint.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_endpoint.sql new file mode 100644 index 0000000000000000000000000000000000000000..427bff297708145c248a835b8312743cc9d5f32f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_endpoint.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS project_endpoints ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + method VARCHAR(10) NOT NULL, + path VARCHAR(255) NOT NULL, + description VARCHAR(255), + controller_id UUID, + FOREIGN KEY (controller_id) REFERENCES project_controllers(id) ON DELETE CASCADE +); diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_feature.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_feature.sql new file mode 100644 index 0000000000000000000000000000000000000000..38942b9b5605adc471ff385e72b019463818d791 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_feature.sql @@ -0,0 +1,12 @@ +-- Project Features Table Schema +-- Stores feature highlights for projects +CREATE TABLE IF NOT EXISTS project_features ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + title VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + icon VARCHAR(255), + image_url TEXT, + project_id UUID, + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_project_features_project_id ON project_features(project_id); \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_page.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_page.sql new file mode 100644 index 0000000000000000000000000000000000000000..82bb315b535c06979ab2d988df812c35af479cb6 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_page.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS project_pages ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + title VARCHAR(255) NOT NULL, + type TEXT NOT NULL, + nav_order INTEGER DEFAULT 0, + content TEXT, + project_id UUID, + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +); diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_technology.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_technology.sql new file mode 100644 index 0000000000000000000000000000000000000000..22edceeb01dfc9f04f79daac09364a389fe20330 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/project_technology.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS project_technologies ( + project_id UUID NOT NULL, + technology VARCHAR(50) NOT NULL, + PRIMARY KEY (project_id, technology), + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +); diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/skill.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/skill.sql new file mode 100644 index 0000000000000000000000000000000000000000..d479deb04af16117b84c0f5feda27b9b766536f9 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/skill.sql @@ -0,0 +1,15 @@ +-- Skills Table Schema +-- Stores individual skills linked to categories +CREATE TABLE IF NOT EXISTS skills ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + level VARCHAR(50), + percentage INTEGER DEFAULT 0, + description TEXT, + display_order INTEGER DEFAULT 0, + category_id INTEGER REFERENCES skill_categories(id) ON DELETE + SET NULL, + profile_id INTEGER DEFAULT 1 +); +CREATE INDEX IF NOT EXISTS idx_skills_category_id ON skills(category_id); +CREATE INDEX IF NOT EXISTS idx_skills_display_order ON skills(display_order); \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/skill_category.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/skill_category.sql new file mode 100644 index 0000000000000000000000000000000000000000..151b317116d19918488d52097b1ee08ce6677f72 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/skill_category.sql @@ -0,0 +1,13 @@ +-- Skill Categories Table Schema +-- Organizes skills into named categories with visual customization +CREATE TABLE IF NOT EXISTS skill_categories ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + description VARCHAR(255), + icon VARCHAR(50), + color VARCHAR(7), + display_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_skill_categories_display_order ON skill_categories(display_order); \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/style_config.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/style_config.sql new file mode 100644 index 0000000000000000000000000000000000000000..1db6d4644c47138fc6813ad816a061ee2ba51db8 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/style_config.sql @@ -0,0 +1,24 @@ +-- Style Config Table Schema +-- Stores theme configuration settings +CREATE TABLE IF NOT EXISTS style_config ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + color_scheme VARCHAR(20) DEFAULT 'light', + primary_color VARCHAR(20) DEFAULT '#ffffff', + secondary_color VARCHAR(20) DEFAULT '#000000', + accent_color VARCHAR(20) DEFAULT '#3b82f6', + background_color VARCHAR(20) DEFAULT '#ffffff', + text_color VARCHAR(20) DEFAULT '#1f2937', + color_palette TEXT, + -- JSON array + heading_font TEXT, + -- JSON object + body_font TEXT, + -- JSON object + base_font_size INTEGER DEFAULT 16, + border_radius INTEGER DEFAULT 4, + is_active BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_style_config_is_active ON style_config(is_active); \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/team_member.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/team_member.sql new file mode 100644 index 0000000000000000000000000000000000000000..26f5dab99b63d4348eb72ab51022bfb92f004d7b --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/team_member.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS team_members ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + role VARCHAR(255) NOT NULL, + avatar_url VARCHAR(255), + bio TEXT, + github_url VARCHAR(255), + linkedin_url VARCHAR(255) +); diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/technology.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/technology.sql new file mode 100644 index 0000000000000000000000000000000000000000..76f191a61f9d8fb0c99ef5680fd687ba6e5e1771 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/technology.sql @@ -0,0 +1,20 @@ +-- Technology Table Schema +-- Master data for technologies/frameworks used in projects +CREATE TABLE IF NOT EXISTS technologies ( + key VARCHAR(50) PRIMARY KEY, + name VARCHAR(100) NOT NULL, + label VARCHAR(150), + category VARCHAR(50) NOT NULL, + description TEXT, + icon VARCHAR(100), + color VARCHAR(20), + website VARCHAR(255), + versions TEXT, + -- Simple array stored as comma-separated + "order" INTEGER DEFAULT 0, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_technologies_category ON technologies(category); +CREATE INDEX IF NOT EXISTS idx_technologies_is_active ON technologies(is_active); \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/theme.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/theme.sql new file mode 100644 index 0000000000000000000000000000000000000000..b090859fdfb63c548170ea65e01f976d71152780 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/theme.sql @@ -0,0 +1,18 @@ +-- Theme Table Schema +-- Stores visual themes with full CSS content for dynamic loading +CREATE TABLE IF NOT EXISTS themes ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + slug VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + css_content TEXT NOT NULL, + preview_color VARCHAR(20), + icon VARCHAR(10), + is_default BOOLEAN DEFAULT FALSE, + "order" INTEGER DEFAULT 0, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_themes_slug ON themes(slug); +CREATE INDEX IF NOT EXISTS idx_themes_is_default ON themes(is_default); \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/user.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/user.sql new file mode 100644 index 0000000000000000000000000000000000000000..25ffca746205e3581571ff0c6f22d5a63e0fe50e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/user.sql @@ -0,0 +1,12 @@ +-- Users Table Schema +-- Stores admin user accounts with hashed passwords +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username VARCHAR(100) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + role VARCHAR(50) DEFAULT 'admin', + last_login TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/widget.sql b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/widget.sql new file mode 100644 index 0000000000000000000000000000000000000000..a0e03d133c9989ba43d0c75198ee80befc6a1e90 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/schemas/widget.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS widgets ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + type TEXT NOT NULL, + title VARCHAR(255) NOT NULL, + "order" INTEGER DEFAULT 0, + config TEXT, -- Stored as JSON string in SQLite/Simple JSON + context VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/README.md b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a22f36a1e62e3e2bd6949d10a6afb63a783f1aad --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/README.md @@ -0,0 +1,130 @@ +# Database Seed Data + +This folder contains all database initialization and seeding functionality for Ark.Portfolio. + +## Structure + +``` +seeds/ +├── seed.ts # Main orchestrator - imports and runs all seeders +├── seeders/ # Domain-specific seeders +│ ├── index.ts # Barrel exports +│ ├── profile.seeder.ts # Profile/identity seeder +│ ├── experience.seeder.ts # Work history seeder +│ ├── skills.seeder.ts # Technical skills seeder +│ ├── carousel.seeder.ts # Homepage carousel seeder +│ ├── media.seeder.ts # Media assets seeder +│ └── projects.seeder.ts # Portfolio projects seeder +└── README.md # This file +``` + +## Seed Data Sources + +### JSON Data Files (`../InitDbAsset/`) + +| File | Seeder | Description | +|------|--------|-------------| +| `JsonDatas/profile.json` | `profile.seeder.ts` | Portfolio owner's personal info | +| `JsonDatas/experience.json` | `experience.seeder.ts` | Professional work history (8 records) | +| `JsonDatas/skills.json` | `skills.seeder.ts` | Technical skills by category | +| `JsonDatas/carousel.json` | `carousel.seeder.ts` | Homepage slider items (4 items) | +| `ProjectData/projects.json` | `projects.seeder.ts` | Full project data with pages/features | + +### Shared Layer Constants + +| Source | Seeder | Description | +|--------|--------|-------------| +| `@ark/portfolio-share` → `DEFAULT_MEDIA_SEED` | `media.seeder.ts` | Media asset references (5 items) | + +## Usage + +### CLI Execution (Full Reset + Reseed) + +```bash +npx ts-node src/database/seeds/seed.ts +``` + +### Programmatic Usage + +```typescript +import { seedDatabase, runSeed, clearDatabase } from './database/seeds/seed'; + +// With existing DataSource (incremental) +await seedDatabase(dataSource); + +// Full reset and reseed +await runSeed(); + +// Clear only +await clearDatabase(dataSource); +``` + +### Individual Seeders + +```typescript +import { seedProfile, seedProjects } from './database/seeds/seeders'; + +// Seed specific domain +await seedProfile(dataSource); +await seedProjects(dataSource); +``` + +## Seeding Order + +Seeders run in dependency order: + +1. **Profile** - Standalone, no dependencies +2. **Experience** - Standalone, no dependencies +3. **Skills** - Standalone, no dependencies +4. **Carousel** - References project images in `/Assets` +5. **Media** - References asset files in `/Assets` +6. **Projects** - Full project hierarchy (pages, features, technologies) + +## Data Summary + +| Entity | Records | Source | +|--------|---------|--------| +| Profile | 1 | profile.json | +| Experience | 8 | experience.json | +| Skill | 40+ | skills.json (5 categories) | +| CarouselItem | 4 | carousel.json | +| Media | 5 | DEFAULT_MEDIA_SEED | +| Project | 3 | projects.json | +| ProjectPage | 12 | projects.json (4 per project) | +| ProjectFeature | 18 | projects.json (6 per project) | +| ProjectTechnology | 30+ | projects.json | + +## Adding New Seed Data + +### To add a new seeder: + +1. Create `seeders/newdomain.seeder.ts` +2. Export `seedNewDomain()` and `clearNewDomain()` functions +3. Add exports to `seeders/index.ts` +4. Import and call in `seed.ts` orchestrator +5. Add JSON data file to `../InitDbAsset/JsonDatas/` if needed + +### Template for new seeder: + +```typescript +import { DataSource } from 'typeorm'; +import { MyEntity } from '../../entities/my-entity.entity'; +import * as fs from 'fs'; +import * as path from 'path'; + +const DATA_PATH = path.join(__dirname, '../../InitDbAsset/JsonDatas/mydata.json'); + +export async function seedMyEntity(dataSource: DataSource): Promise { + const repo = dataSource.getRepository(MyEntity); + + if (await repo.count() === 0) { + const data = JSON.parse(fs.readFileSync(DATA_PATH, 'utf-8')); + // ... seeding logic + console.log('✓ MyEntity seeded'); + } +} + +export async function clearMyEntity(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(MyEntity).execute(); +} +``` diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seed.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seed.ts new file mode 100644 index 0000000000000000000000000000000000000000..f55a0e83d74d4393aa7f7a9f02a6c22da4807ae1 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seed.ts @@ -0,0 +1,330 @@ +/** + * @fileoverview Database Seed Orchestrator + * Main entry point for database seeding operations. + * Coordinates all domain-specific seeders in the correct order. + * + * @module database/seeds/seed + * @author Armand Richelet-Kleinberg + * + * @description + * This module provides the main seeding functionality for initializing the database + * with default data. It orchestrates multiple domain-specific seeders to populate: + * + * - **Profile**: Portfolio owner's personal information + * - **Experience**: Professional work history (8 records) + * - **Skills**: Technical competencies (40+ technologies across 5 categories) + * - **Carousel**: Homepage slider items (4 featured projects) + * - **Media**: Asset references for admin management (5 default images) + * - **Projects**: Portfolio projects with pages, features, and technologies (3 projects) + * + * @example Running as CLI: + * ```bash + * npx ts-node src/database/seeds/seed.ts + * ``` + * + * @example Programmatic usage: + * ```typescript + * import { seedDatabase, runSeed } from './database/seeds/seed'; + * + * // With existing DataSource + * await seedDatabase(dataSource); + * + * // Full reset and reseed + * await runSeed(); + * ``` + */ + +import { DataSource } from 'typeorm'; +import { AppDataSource } from '../../config/database'; + +// Import all seeders +import { + // Corporate CMS (NEW) + seedOrganization, + seedCollaborators, + seedUserRoles, + seedCEO, + seedCEOResumeData, + updateFounderDisplayOrder, + seedMarioConsultant, + seedMarioResumeData, + seedRaduDirector, + seedRaduResumeData, + seedArmandArchitect, + seedArmandResumeData, + seedLennyCSM, + seedLennyResumeData, + seedCollaboratorAvatars, + linkResumeDataToCollaborator, + clearOrganization, + clearCollaborators, + clearUserRoles, + // Legacy Profile + seedProfile, + clearProfile, + // Resume Components + seedExperience, + seedSkills, + seedEducation, + seedLanguages, + seedHobbies, + seedBusinessDomains, + clearExperience, + clearSkills, + clearEducation, + clearLanguages, + clearHobbies, + clearBusinessDomains, + // Content + seedCarousel, + seedMedia, + clearCarousel, + clearMedia, + // Master Data + seedTechnologies, + clearTechnologies, + // Portfolio + seedProjects, + clearProjects, + // Themes + seedThemes, + clearThemes, + // Tasks + seedTasks, + clearTasks, + // AI Prompts + seedPromptTemplates +} from './seeders'; + +/** + * Seeds the database with initial data from all domain seeders. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when all seeding is complete + * + * @remarks + * Seeding order is important due to potential dependencies: + * 1. Profile (standalone, no dependencies) + * 2. Experience (standalone, no dependencies) + * 3. Skills (standalone, no dependencies) + * 4. Carousel (references project images) + * 5. Media (references asset files) + * 6. Technologies (master data, referenced by projects) + * 7. Projects (references technologies) + * + * Each seeder checks for existing data before inserting to prevent duplicates. + */ +export const seedDatabase = async (dataSource: DataSource): Promise => { + console.log('╔════════════════════════════════════════════════════════════╗'); + console.log('║ Database Seeding - Ark.Alliance.StartupCms.Ai ║'); + console.log('║ Corporate CMS + Authentication ║'); + console.log('╚════════════════════════════════════════════════════════════╝'); + console.log(''); + + // 0. Organization & Team (NEW - Corporate CMS) + console.log('─── Organization & Team ───'); + await seedOrganization(dataSource); + + // 0.1 CEO & Founder (Bryan Wiedmer Soler) - FIRST + console.log(''); + console.log('─── CEO & Founder (Bryan) ───'); + await seedCEO(dataSource); + + // 0.2 Public Relations Director (Radu Dinulescu) + console.log(''); + console.log('─── Public Relations Director (Radu) ───'); + await seedRaduDirector(dataSource); + + // 0.3 AI Principal Architect (Armand Richelet-Kleinberg) + console.log(''); + console.log('─── AI Principal Architect (Armand) ───'); + await seedArmandArchitect(dataSource); + + // Legacy collaborator seeder (for backward compatibility) + await seedCollaborators(dataSource); + await updateFounderDisplayOrder(dataSource); + + // 0.3 Consultant Team (Mario Schmidt - Prompt Testing) - Reports to Armand + console.log(''); + console.log('─── Consultant Team (Mario) ───'); + await seedMarioConsultant(dataSource); + + // 1. Profile & Identity (Legacy - for backward compatibility) + console.log(''); + console.log('─── Profile & Identity (Legacy) ───'); + await seedProfile(dataSource); + + // 2. Resume/CV Components + console.log(''); + console.log('─── Resume Components ───'); + await seedExperience(dataSource); + await seedSkills(dataSource); + await seedEducation(dataSource); + await seedLanguages(dataSource); + await seedHobbies(dataSource); + await seedBusinessDomains(dataSource); + + // 2.5 Link resume data to founder collaborator (Armand) + console.log(''); + console.log('─── Resume Data Migration ───'); + await linkResumeDataToCollaborator(dataSource); + + // 2.6 Seed CEO resume data (Bryan) + console.log(''); + console.log('─── CEO Resume Data ───'); + await seedCEOResumeData(dataSource); + + // 2.7 Seed Mario resume data + console.log(''); + console.log('─── Mario Resume Data ───'); + await seedMarioResumeData(dataSource); + + // 2.8 Seed Radu resume data + console.log(''); + console.log('─── Radu Resume Data ───'); + await seedRaduResumeData(dataSource); + + // 2.9 Seed Armand resume data + console.log(''); + console.log('─── Armand Resume Data ───'); + await seedArmandResumeData(dataSource); + + // 2.10 Seed Lenny (CSM) + console.log(''); + console.log('─── Customer Success (Lenny) ───'); + await seedLennyCSM(dataSource); + + // 2.10 Seed Lenny resume data + console.log(''); + console.log('─── Lenny Resume Data ───'); + await seedLennyResumeData(dataSource); + + // 3. Media Assets + console.log(''); + console.log('─── Media Assets ───'); + await seedMedia(dataSource); + + // 4. Master Data + console.log(''); + console.log('─── Master Data ───'); + await seedTechnologies(dataSource); + + // 5. Themes + console.log(''); + console.log('─── Themes ───'); + await seedThemes(dataSource); + + // 6. Portfolio Projects + console.log(''); + console.log('─── Portfolio Projects ───'); + await seedProjects(dataSource); + + // 7. Carousel (seeded after projects to link by project ID) + console.log(''); + console.log('─── Carousel ───'); + await seedCarousel(dataSource); + + // 8. User Roles (after admin user is created by auth seeder) + console.log(''); + console.log('─── User Roles ───'); + await seedUserRoles(dataSource); + + // 9. Tasks / Accomplishments (after projects for linking) + console.log(''); + console.log('─── Tasks / Accomplishments ───'); + await seedTasks(dataSource); + + // 10. AI Prompt Templates + console.log(''); + console.log('─── AI Prompt Templates ───'); + await seedPromptTemplates(dataSource); + + // 11. Collaborator Avatars (after all collaborators are created) + console.log(''); + console.log('─── Collaborator Avatars ───'); + await seedCollaboratorAvatars(dataSource); + + console.log(''); + console.log('════════════════════════════════════════════════════════════'); + console.log(' ✅ All seed data loaded successfully'); + console.log('════════════════════════════════════════════════════════════'); +}; + +/** + * Clears all seeded data from the database. + * Clears in reverse order of dependencies to respect foreign key constraints. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when all data is cleared + */ +export const clearDatabase = async (dataSource: DataSource): Promise => { + console.log('Clearing existing data...'); + + // Clear in reverse order of dependencies + await clearTasks(dataSource); // Tasks (depend on projects & collaborators) + await clearUserRoles(dataSource); // UserRole (junction table) + await clearProjects(dataSource); // ProjectTechnology, ProjectFeature, ProjectPage, Project + await clearThemes(dataSource); // Theme (standalone) + await clearTechnologies(dataSource); // Technology (master data) + await clearMedia(dataSource); // Media + await clearCarousel(dataSource); // CarouselItem + await clearBusinessDomains(dataSource); // BusinessDomain + await clearHobbies(dataSource); // Hobby + await clearLanguages(dataSource); // Language + await clearEducation(dataSource); // Education + await clearSkills(dataSource); // Skill + await clearExperience(dataSource); // Experience + await clearProfile(dataSource); // Profile + await clearCollaborators(dataSource); // Collaborator (after resume data) + await clearOrganization(dataSource); // Organization (last) + + console.log('✓ Database cleared'); +}; + +/** + * Full reset and reseed operation. + * Initializes DataSource, clears existing data, seeds fresh data, and closes connection. + * + * @returns Promise resolving when operation is complete + * + * @remarks + * This is the main entry point for CLI execution. + * It handles: + * - DataSource initialization + * - Full data clear (in dependency order) + * - Fresh seed of all data + * - Proper connection cleanup + * + * @throws Will throw an error if seeding fails, after logging the error + */ +export const runSeed = async (): Promise => { + const dataSource = await AppDataSource.initialize(); + console.log('Data Source has been initialized!'); + console.log(''); + + try { + // Clear all existing data + await clearDatabase(dataSource); + console.log(''); + + // Seed fresh data + await seedDatabase(dataSource); + + console.log(''); + console.log('Database seeding process finished.'); + } catch (error) { + console.error('Seeding failed:', error); + throw error; + } finally { + await dataSource.destroy(); + } +}; + +// Check if file is run directly (CLI) +if (require.main === module) { + runSeed().catch(error => { + console.error('Fatal error during seeding:', error); + process.exit(1); + }); +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/armand-architect.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/armand-architect.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c71460c7fe82283bd2cee4b222dd2c5041bf7c3 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/armand-architect.seeder.ts @@ -0,0 +1,397 @@ +/** + * @fileoverview Armand Richelet-Kleinberg (AI Principal Architect) User & Resume Seeder + * Seeds the AI Principal Architect with complete profile and extensive resume data. + * Consolidates user, collaborator, and all resume data in one seeder. + * + * @module database/seeds/seeders/armand-architect.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { User } from '../../entities/user.entity'; +import { Collaborator } from '../../entities/collaborator.entity'; +import { UserRole } from '../../entities/user-role.entity'; +import { Experience } from '../../entities/experience.entity'; +import { Education } from '../../entities/education.entity'; +import { Skill } from '../../entities/skill.entity'; +import { Language } from '../../entities/language.entity'; +import { Hobby } from '../../entities/hobby.entity'; +import { BusinessDomain } from '../../entities/business-domain.entity'; +import { Role } from '@arkalliance/startupcms-ai-share'; +import { DEFAULT_ORGANIZATION_ID } from './organization.seeder'; +import { CEO_COLLABORATOR_ID } from './bryan-ceo.seeder'; +import bcrypt from 'bcryptjs'; + +/** + * Armand Collaborator ID. + * Exported for use in other seeders that reference Armand. + */ +export const ARMAND_COLLABORATOR_ID = 'collab-founder-001'; + +/** + * Seeds Armand Richelet-Kleinberg as AI Principal Architect. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when architect is seeded + */ +export async function seedArmandArchitect(dataSource: DataSource): Promise { + const userRepo = dataSource.getRepository(User); + const collaboratorRepo = dataSource.getRepository(Collaborator); + const userRoleRepo = dataSource.getRepository(UserRole); + + // 1. Create or Update Armand User + const hashedPassword = await bcrypt.hash(process.env.ADMIN_KEY_CMS || 'ArkAlliance2026!Secure', 10); + + let armandUser = await userRepo.findOne({ where: { email: 'arkleinberg@gmail.com' } }); + + if (!armandUser) { + // Create new user + armandUser = userRepo.create({ + username: 'armandkleinberg', + email: 'arkleinberg@gmail.com', + passwordHash: hashedPassword, + firstName: 'Armand', + lastName: 'Richelet-Kleinberg', + title: 'AI Principal Solutions Architect - Full Stack Dev - Business & Technical Analyst', + bio: 'Experienced AI Principal Architect with 20+ years in software development and system architecture, focused on the AI industry. Specialized in designing and delivering AI-driven solutions, MLOps pipelines, and cloud-native systems using C#, TypeScript (React), Js, Python. Proven ability to build scalable, event-driven architectures and integrate advanced AI frameworks. Expert in Agile/DevOps environments, translating complex AI business needs into robust, production-grade platforms.', + emailConfirmed: true, + isActive: true, + collaboratorId: ARMAND_COLLABORATOR_ID, + avatarUrl: '/Assets/Site/Icon.png', + linkedinUrl: 'https://do.linkedin.com/in/arkleinberg/es', + githubUrl: 'https://github.com/ArmandRicheletKleinberg' + }); + console.log('Creating new Armand user...'); + } else { + // Update existing user + armandUser.username = 'armandkleinberg'; + armandUser.passwordHash = hashedPassword; + armandUser.firstName = 'Armand'; + armandUser.lastName = 'Richelet-Kleinberg'; + armandUser.title = 'AI Principal Solutions Architect - Full Stack Dev - Business & Technical Analyst'; + armandUser.bio = 'Experienced AI Principal Architect with 20+ years in software development and system architecture, focused on the AI industry. Specialized in designing and delivering AI-driven solutions, MLOps pipelines, and cloud-native systems using C#, TypeScript (React), Js, Python. Proven ability to build scalable, event-driven architectures and integrate advanced AI frameworks. Expert in Agile/DevOps environments, translating complex AI business needs into robust, production-grade platforms.'; + armandUser.emailConfirmed = true; + armandUser.isActive = true; + armandUser.collaboratorId = ARMAND_COLLABORATOR_ID; + armandUser.avatarUrl = '/Assets/Site/Icon.png'; + armandUser.linkedinUrl = 'https://do.linkedin.com/in/arkleinberg/es'; + armandUser.githubUrl = 'https://github.com/ArmandRicheletKleinberg'; + console.log('Updating existing Armand user...'); + } + + await userRepo.save(armandUser); + console.log(`✓ Armand user ready (armandkleinberg) ID: ${armandUser.id}`); + + // 2. Assign ADMIN + COLLABORATOR roles + const existingAdminRole = await userRoleRepo.findOne({ + where: { userId: armandUser.id, role: Role.ADMIN } + }); + + if (!existingAdminRole) { + const adminRole = userRoleRepo.create({ + userId: armandUser.id, + role: Role.ADMIN + }); + await userRoleRepo.save(adminRole); + + const collaboratorRole = userRoleRepo.create({ + userId: armandUser.id, + role: Role.COLLABORATOR + }); + await userRoleRepo.save(collaboratorRole); + console.log('✓ ADMIN + COLLABORATOR roles assigned to Armand'); + } + + // 3. Create or Update Armand Collaborator Profile + let armandCollab = await collaboratorRepo.findOne({ where: { id: ARMAND_COLLABORATOR_ID } }); + + if (!armandCollab) { + armandCollab = collaboratorRepo.create({ + id: ARMAND_COLLABORATOR_ID, + firstName: 'Armand', + lastName: 'Richelet-Kleinberg', + email: 'arkleinberg@gmail.com', + position: 'AI Principal Architect & Full-Stack Developer', + department: 'Technology', + bio: 'Experienced AI Principal Architect with 20+ years in software development and system architecture, focused on the AI industry. Specialized in designing and delivering AI-driven solutions, MLOps pipelines, and cloud-native systems using C#, TypeScript (React), Js, Python. Proven ability to build scalable, event-driven architectures and integrate advanced AI frameworks. Expert in Agile/DevOps environments, translating complex AI business needs into robust, production-grade platforms.', + avatarUrl: '/Assets/Site/Icon.png', + linkedinUrl: 'https://do.linkedin.com/in/arkleinberg/es', + githubUrl: 'https://github.com/ArmandRicheletKleinberg', + userId: armandUser.id, + organizationId: DEFAULT_ORGANIZATION_ID, + reportsToId: CEO_COLLABORATOR_ID, // Reports to Bryan (CEO) + isActive: true, + displayOrder: 2, // After Bryan (1) + hireDate: new Date('2024-01-01') + }); + } else { + armandCollab.firstName = 'Armand'; + armandCollab.lastName = 'Richelet-Kleinberg'; + armandCollab.email = 'arkleinberg@gmail.com'; + armandCollab.position = 'AI Principal Architect & Full-Stack Developer'; + armandCollab.department = 'Technology'; + armandCollab.bio = 'Experienced AI Principal Architect with 20+ years in software development and system architecture, focused on the AI industry. Specialized in designing and delivering AI-driven solutions, MLOps pipelines, and cloud-native systems using C#, TypeScript (React), Js, Python. Proven ability to build scalable, event-driven architectures and integrate advanced AI frameworks. Expert in Agile/DevOps environments, translating complex AI business needs into robust, production-grade platforms.'; + armandCollab.userId = armandUser.id; + armandCollab.organizationId = DEFAULT_ORGANIZATION_ID; + armandCollab.reportsToId = CEO_COLLABORATOR_ID; + armandCollab.displayOrder = 2; + } + + await collaboratorRepo.save(armandCollab); + console.log('✓ Armand collaborator profile created (displayOrder: 2, reports to Bryan)'); +} + +/** + * Seeds Armand resume data (extensive software architecture career). + * + * @param dataSource - TypeORM DataSource connection + */ +export async function seedArmandResumeData(dataSource: DataSource): Promise { + const userRepo = dataSource.getRepository(User); + const experienceRepo = dataSource.getRepository(Experience); + const skillRepo = dataSource.getRepository(Skill); + const educationRepo = dataSource.getRepository(Education); + const languageRepo = dataSource.getRepository(Language); + const hobbyRepo = dataSource.getRepository(Hobby); + const businessDomainRepo = dataSource.getRepository(BusinessDomain); + + // Get Armand user by email + const armandUser = await userRepo.findOne({ where: { email: 'arkleinberg@gmail.com' } }); + if (!armandUser) { + console.log('○ Armand user not found, skipping resume data'); + return; + } + + // Check if resume data already exists + const existingExp = await experienceRepo.findOne({ where: { userId: armandUser.id } }); + if (existingExp) { + console.log('○ Armand resume data already exists, skipping'); + return; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // EXPERIENCES (8 positions) + // ═══════════════════════════════════════════════════════════════════════════ + const experiences = [ + { + userId: armandUser.id, + company: 'M2H / AI and Technology', + position: 'AI Lead & Principal Solution Architect', + project: 'Ark.Alliance Ecosystem; Mindful AI Initiatives', + startDate: new Date('2022-12-01'), + endDate: null, + description: 'Pioneered mindful AI solutions and the Ark.Alliance Ecosystem. Architected prompt libraries and guidelines for consistent AI experiences. Led full-stack teams (C#, React, Python) in startup environments, implementing resilient patterns (Circuit Breaker, Bulkhead) and robust incident response processes. Built scalable, cloud-native systems from scratch using Agile/DevOps.', + technologies: ['C#', 'React', 'TypeScript', 'Python', 'Three.js', 'Docker', 'K8s', 'SLM', 'OpenAI', 'Anthropic', 'Mistral'], + isHighlighted: true, + displayOrder: 1 + }, + { + userId: armandUser.id, + company: 'Ahold Delhaize / Logistics Supply Chain', + position: 'Solution & Software Architect', + project: 'New Logistics Systems Design & Optimization', + startDate: new Date('2021-01-01'), + endDate: new Date('2025-02-01'), + description: 'Architected and delivered critical logistics systems (TMS integration, delivery tracking). Led technical analysis and acceptance testing. Designed generic integration tools. Coached cross-functional and offshore teams to ensure high-quality delivery during cutover and HyperCare periods.', + technologies: ['C#', '.NET 8', 'Blazor', 'Microservices', 'Azure', 'SAP', 'CQRS', 'GitHub', 'OpenAI API'], + isHighlighted: true, + displayOrder: 2 + }, + { + userId: armandUser.id, + company: 'Candriam / Asset Management', + position: 'Software Architect & Full Stack Dev', + project: 'Asset Position Integration System', + startDate: new Date('2020-04-01'), + endDate: new Date('2020-12-01'), + description: 'Reverse-engineered legacy Mainframe systems to modern .NET microservices. Designed and rewrote the asset position integration system (shares, funds) for financial account managers. Executed a seamless migration with zero downtime during cutover.', + technologies: ['C#', '.NET 5', 'Docker', 'MQSeries', 'DB2', 'Cobol', 'Microservices', 'SQL Server', 'CQRS'], + isHighlighted: true, + displayOrder: 3 + }, + { + userId: armandUser.id, + company: 'Liberty Steel / Steel Industry', + position: 'Business Analyst & Full Stack Dev', + project: 'Web-Based Application Portfolio & SAP Integration', + startDate: new Date('2019-05-01'), + endDate: new Date('2020-04-01'), + description: 'Analyzed business flows and developed web-based microservices for steel production logistics. Migrated legacy services to WCF and rewrote production planning schedulers. Integrated deeply with SAP and Mainframe systems using Azure cloud services.', + technologies: ['C#', '.NET Core', 'TypeScript', 'Python', 'Azure', 'SAP ERP', 'Clean Architecture', 'Blazor'], + isHighlighted: false, + displayOrder: 4 + }, + { + userId: armandUser.id, + company: 'Delhaize Group / Retail Logistics', + position: 'Full Stack Developer & IT Owner', + project: 'Logistics Systems Design & Cloud Transformation', + startDate: new Date('2011-09-01'), + endDate: new Date('2019-04-01'), + description: 'Owned IT delivery for global logistics reporting and supplier management apps. Modernized legacy systems with SAP and WMS integrations (dock scheduling, EDIFACT). Provided L3 support and implemented ITIL processes for resilient 24/7 logistics operations.', + technologies: ['C#', '.NET', 'Entity Framework', 'Azure', 'Java', 'SAP ERP', 'WMS', 'IoT', 'SQL Server', 'Oracle'], + isHighlighted: true, + displayOrder: 5 + }, + { + userId: armandUser.id, + company: 'Spectacles Charles Kleinberg', + position: 'Solution Architect - Developer & Artist', + project: 'Live Show Control Systems', + startDate: new Date('2005-01-01'), + endDate: new Date('2015-11-01'), + description: 'Architected real-time show control systems synchronizing audio, MIDI, lighting, and 3D Stereoscopic visuals. Developed high-performance interactive solutions in Agile environments. Blended technical engineering with artistic direction for immersive live performances.', + technologies: ['C#', 'Python', 'C++', 'DirectX', 'Unity', 'Three.js', 'Max/Msp', 'Maya', 'Real-time Systems'], + isHighlighted: true, + displayOrder: 6 + }, + { + userId: armandUser.id, + company: 'BNP Paribas Fortis / Banking', + position: 'ICT Consultant', + project: 'Voice over IP Systems', + startDate: new Date('2010-11-01'), + endDate: new Date('2011-02-01'), + description: 'Consulted on the architecture and full-stack development of robust Voice over IP systems for banking infrastructure.', + technologies: ['.NET (C#/VB)', 'SQL Server', 'SOAP'], + isHighlighted: false, + displayOrder: 7 + }, + { + userId: armandUser.id, + company: 'Mastercard / Financial Services', + position: 'Software Engineer', + project: 'Credit Card Chip Validation', + startDate: new Date('2004-11-01'), + endDate: new Date('2005-02-01'), + description: 'Developed critical testing and reporting systems for credit card chip compliance. Collaborated on feature launches to enhance global transaction security.', + technologies: ['C#', 'C', 'SQL Server', 'Assembly'], + isHighlighted: false, + displayOrder: 8 + } + ]; + + for (const exp of experiences) { + await experienceRepo.save(experienceRepo.create(exp)); + } + console.log(`✓ ${experiences.length} experiences seeded for Armand`); + + // ═══════════════════════════════════════════════════════════════════════════ + // SKILLS (Categorized) + // ═══════════════════════════════════════════════════════════════════════════ + const skills = [ + // Languages + { userId: armandUser.id, name: 'C#', level: 'Expert', yearsOfExperience: 20, category: 'Backend' }, + { userId: armandUser.id, name: 'Python', level: 'Expert', yearsOfExperience: 10, category: 'Backend' }, + { userId: armandUser.id, name: 'TypeScript', level: 'Expert', yearsOfExperience: 8, category: 'Frontend' }, + { userId: armandUser.id, name: 'JavaScript', level: 'Expert', yearsOfExperience: 15, category: 'Frontend' }, + { userId: armandUser.id, name: 'Java', level: 'Advanced', yearsOfExperience: 5, category: 'Backend' }, + { userId: armandUser.id, name: 'C/C++', level: 'Advanced', yearsOfExperience: 10, category: 'Backend' }, + { userId: armandUser.id, name: 'T-SQL', level: 'Expert', yearsOfExperience: 15, category: 'Database' }, + { userId: armandUser.id, name: 'PL-SQL', level: 'Advanced', yearsOfExperience: 8, category: 'Database' }, + // Frameworks + { userId: armandUser.id, name: '.NET up to 10', level: 'Expert', yearsOfExperience: 20, category: 'Backend' }, + { userId: armandUser.id, name: 'Entity Framework', level: 'Expert', yearsOfExperience: 12, category: 'Backend' }, + { userId: armandUser.id, name: 'React', level: 'Expert', yearsOfExperience: 6, category: 'Frontend' }, + { userId: armandUser.id, name: 'Python Ecosystem', level: 'Expert', yearsOfExperience: 10, category: 'Backend' }, + { userId: armandUser.id, name: 'Unity', level: 'Expert', yearsOfExperience: 10, category: 'Specialized' }, + { userId: armandUser.id, name: 'Adobe Suite', level: 'Expert', yearsOfExperience: 15, category: 'Specialized' }, + { userId: armandUser.id, name: 'Cinema 4D', level: 'Expert', yearsOfExperience: 10, category: 'Specialized' }, + // Databases + { userId: armandUser.id, name: 'SQL Server', level: 'Expert', yearsOfExperience: 20, category: 'Database' }, + { userId: armandUser.id, name: 'PostgreSQL', level: 'Expert', yearsOfExperience: 8, category: 'Database' }, + { userId: armandUser.id, name: 'MongoDB', level: 'Advanced', yearsOfExperience: 5, category: 'Database' }, + { userId: armandUser.id, name: 'SQLite', level: 'Expert', yearsOfExperience: 10, category: 'Database' }, + { userId: armandUser.id, name: 'DB2', level: 'Advanced', yearsOfExperience: 4, category: 'Database' }, + { userId: armandUser.id, name: 'Oracle', level: 'Advanced', yearsOfExperience: 8, category: 'Database' }, + // Tools + { userId: armandUser.id, name: 'Docker', level: 'Expert', yearsOfExperience: 8, category: 'DevOps' }, + { userId: armandUser.id, name: 'Kubernetes', level: 'Advanced', yearsOfExperience: 5, category: 'DevOps' }, + { userId: armandUser.id, name: 'Azure Services', level: 'Expert', yearsOfExperience: 10, category: 'DevOps' }, + { userId: armandUser.id, name: 'AWS Services', level: 'Advanced', yearsOfExperience: 5, category: 'DevOps' }, + { userId: armandUser.id, name: 'GitHub', level: 'Expert', yearsOfExperience: 12, category: 'DevOps' }, + { userId: armandUser.id, name: 'PyTorch', level: 'Advanced', yearsOfExperience: 3, category: 'Specialized' }, + { userId: armandUser.id, name: 'RabbitMQ', level: 'Expert', yearsOfExperience: 8, category: 'Backend' }, + { userId: armandUser.id, name: 'Three.js', level: 'Expert', yearsOfExperience: 8, category: 'Frontend' }, + // Methodologies + { userId: armandUser.id, name: 'DDD', level: 'Expert', yearsOfExperience: 10, category: 'Specialized' }, + { userId: armandUser.id, name: 'CQRS', level: 'Expert', yearsOfExperience: 8, category: 'Specialized' }, + { userId: armandUser.id, name: 'Event-Driven Architecture', level: 'Expert', yearsOfExperience: 10, category: 'Specialized' }, + { userId: armandUser.id, name: 'Microservices', level: 'Expert', yearsOfExperience: 10, category: 'Specialized' }, + { userId: armandUser.id, name: 'MLOps', level: 'Advanced', yearsOfExperience: 3, category: 'Specialized' }, + { userId: armandUser.id, name: 'Agile/Scrum', level: 'Expert', yearsOfExperience: 15, category: 'Specialized' }, + { userId: armandUser.id, name: 'DevOps', level: 'Expert', yearsOfExperience: 10, category: 'DevOps' } + ]; + + for (const skill of skills) { + await skillRepo.save(skillRepo.create(skill)); + } + console.log(`✓ ${skills.length} skills seeded for Armand`); + + // ═══════════════════════════════════════════════════════════════════════════ + // EDUCATION + // ═══════════════════════════════════════════════════════════════════════════ + const education = educationRepo.create({ + userId: armandUser.id, + institution: 'ULB (Université Libre de Bruxelles)', + degree: 'Master in Computer Science', + fieldOfStudy: 'Computer Science - Algorithmic and Software Engineering', + startDate: new Date('1999-09-01'), + endDate: new Date('2004-06-30'), + description: 'Specialization in Algorithmic and Software Engineering. Comprehensive curriculum covering data structures, algorithms, software design patterns, database systems, and artificial intelligence fundamentals.' + }); + await educationRepo.save(education); + console.log('✓ Education record seeded for Armand'); + + // ═══════════════════════════════════════════════════════════════════════════ + // LANGUAGES + // ═══════════════════════════════════════════════════════════════════════════ + const languages = [ + { userId: armandUser.id, language: 'French', speaking: 5, writing: 5, presenting: 5, displayOrder: 1 }, + { userId: armandUser.id, language: 'English', speaking: 5, writing: 5, presenting: 5, displayOrder: 2 }, + { userId: armandUser.id, language: 'Spanish', speaking: 4, writing: 3, presenting: 3, displayOrder: 3 }, + { userId: armandUser.id, language: 'Dutch', speaking: 2, writing: 1, presenting: 1, displayOrder: 4 } + ]; + + for (const lang of languages) { + await languageRepo.save(languageRepo.create(lang)); + } + console.log(`✓ ${languages.length} languages seeded for Armand`); + + // ═══════════════════════════════════════════════════════════════════════════ + // HOBBIES + // ═══════════════════════════════════════════════════════════════════════════ + const hobbies = [ + { userId: armandUser.id, name: 'Music Production', description: 'Electronic music composition and production using DAWs, synthesizers, and audio engineering techniques.', icon: 'Headphones', displayOrder: 1 }, + { userId: armandUser.id, name: '3D Graphics & Animation', description: 'Creating 3D models, animations, and visual effects using Cinema 4D, Maya, and Three.js.', icon: 'Cuboid', displayOrder: 2 }, + { userId: armandUser.id, name: 'Gaming', description: 'Strategy games, simulation, and game development exploration.', icon: 'Gamepad2', displayOrder: 3 }, + { userId: armandUser.id, name: 'Photography', description: 'Digital photography with focus on architectural and landscape subjects.', icon: 'Camera', displayOrder: 4 }, + { userId: armandUser.id, name: 'AI Research', description: 'Exploring cutting-edge AI technologies, prompt engineering, and building AI-powered applications.', icon: 'Brain', displayOrder: 5 } + ]; + + for (const hobby of hobbies) { + await hobbyRepo.save(hobbyRepo.create(hobby)); + } + console.log(`✓ ${hobbies.length} hobbies seeded for Armand`); + + // ═══════════════════════════════════════════════════════════════════════════ + // BUSINESS DOMAINS + // ═══════════════════════════════════════════════════════════════════════════ + const domains = [ + { userId: armandUser.id, domain: 'Logistics', level: 'Expert', yearsOfExperience: 12, description: 'Supply chain management, warehouse systems, TMS integration, delivery tracking, and last-mile logistics optimization.', icon: 'Truck', displayOrder: 1 }, + { userId: armandUser.id, domain: 'Finance', level: 'Expert', yearsOfExperience: 8, description: 'Asset management, portfolio systems, trading platforms, regulatory compliance, and financial data integration.', icon: 'TrendingUp', displayOrder: 2 }, + { userId: armandUser.id, domain: 'Trading', level: 'Expert', yearsOfExperience: 5, description: 'Algorithmic trading systems, market data processing, order management, and cryptocurrency platforms.', icon: 'LineChart', displayOrder: 3 }, + { userId: armandUser.id, domain: 'Banking', level: 'Advanced', yearsOfExperience: 3, description: 'Core banking systems, payment processing, VoIP infrastructure for banking operations.', icon: 'Building', displayOrder: 4 }, + { userId: armandUser.id, domain: 'Asset Management', level: 'Expert', yearsOfExperience: 4, description: 'Investment portfolio systems, fund management, asset position tracking, and regulatory reporting.', icon: 'Briefcase', displayOrder: 5 }, + { userId: armandUser.id, domain: 'Steel Manufacturing', level: 'Advanced', yearsOfExperience: 2, description: 'Production planning, SAP ERP integration, supply chain for steel industry, and manufacturing logistics.', icon: 'Factory', displayOrder: 6 }, + { userId: armandUser.id, domain: 'Retail', level: 'Expert', yearsOfExperience: 10, description: 'E-commerce platforms, inventory management, supplier systems, and omnichannel retail solutions.', icon: 'ShoppingCart', displayOrder: 7 }, + { userId: armandUser.id, domain: 'Entertainment', level: 'Expert', yearsOfExperience: 10, description: 'Live show control systems, real-time audio/visual synchronization, and interactive performance technology.', icon: 'Clapperboard', displayOrder: 8 }, + { userId: armandUser.id, domain: 'Theatre', level: 'Expert', yearsOfExperience: 10, description: 'Stage automation, lighting control systems, MIDI integration, and 3D stereoscopic visual effects for live performances.', icon: 'Theater', displayOrder: 9 }, + { userId: armandUser.id, domain: 'Music Composing', level: 'Advanced', yearsOfExperience: 15, description: 'Original music composition for performances, electronic music production, and sound design.', icon: 'Music2', displayOrder: 10 } + ]; + + for (const domain of domains) { + await businessDomainRepo.save(businessDomainRepo.create(domain)); + } + console.log(`✓ ${domains.length} business domains seeded for Armand`); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/avatar.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/avatar.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d8eab4b97cf6b1c8e4e5fa54c2ec957fe5c348e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/avatar.seeder.ts @@ -0,0 +1,147 @@ +/** + * @fileoverview Avatar Seeder - Updates collaborator avatars from image files + * Reads images from Assets/Profil_Avatars folder and converts to base64 data URLs. + * + * @module database/seeds/seeders/avatar.seeder + * @author System + */ + +import { DataSource } from 'typeorm'; +import { Collaborator } from '../../entities/collaborator.entity'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Avatar file mappings - collaborator ID to filename + */ +const AVATAR_MAPPINGS: Record = { + 'collab-ceo-bryan-001': 'Bryan', + 'collab-founder-001': 'Armand', + 'collab-mario-schmidt-001': 'Mario', + 'collab-radu-dinulescu-001': 'RaduDinulescu', + 'collab-lenny-avril-001': 'Lenny Avril' +}; + +/** + * Get MIME type from file extension + */ +function getMimeType(filename: string): string { + const ext = path.extname(filename).toLowerCase(); + const mimeTypes: Record = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.jfif': 'image/jpeg', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml' + }; + return mimeTypes[ext] || 'image/png'; +} + +/** + * Find avatar file for a given base name (checks multiple extensions) + */ +function findAvatarFile(baseName: string, avatarDir: string): string | null { + const extensions = ['.png', '.jpg', '.jpeg', '.jfif', '.gif', '.webp']; + + for (const ext of extensions) { + const filePath = path.join(avatarDir, `${baseName}${ext}`); + if (fs.existsSync(filePath)) { + return filePath; + } + } + return null; +} + +/** + * Convert image file to base64 data URL + */ +function imageToBase64DataUrl(filePath: string): string { + const imageBuffer = fs.readFileSync(filePath); + const base64 = imageBuffer.toString('base64'); + const mimeType = getMimeType(filePath); + return `data:${mimeType};base64,${base64}`; +} + +/** + * Seeds collaborator avatars from image files. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when avatars are updated + */ +export async function seedCollaboratorAvatars(dataSource: DataSource): Promise { + const collaboratorRepo = dataSource.getRepository(Collaborator); + + // Path to avatar images + const avatarDir = path.join(__dirname, '../../../Assets/Profil_Avatars'); + + if (!fs.existsSync(avatarDir)) { + console.log(`○ Avatar directory not found: ${avatarDir}`); + return; + } + + let updatedCount = 0; + + for (const [collaboratorId, baseName] of Object.entries(AVATAR_MAPPINGS)) { + try { + // Find the collaborator + const collaborator = await collaboratorRepo.findOne({ + where: { id: collaboratorId } + }); + + if (!collaborator) { + console.log(` ○ Collaborator not found: ${collaboratorId}`); + continue; + } + + // Find the avatar file + const avatarPath = findAvatarFile(baseName, avatarDir); + + if (!avatarPath) { + console.log(` ○ No avatar file found for ${baseName}`); + continue; + } + + // Convert to base64 data URL + const base64DataUrl = imageToBase64DataUrl(avatarPath); + + // Update collaborator avatar + collaborator.avatarUrl = base64DataUrl; + await collaboratorRepo.save(collaborator); + + // Also update linked User's avatar (for resume page) + if (collaborator.userId) { + const userRepo = dataSource.getRepository('User'); + await userRepo.update( + { id: collaborator.userId }, + { avatarUrl: base64DataUrl } + ); + } + + const fileSize = fs.statSync(avatarPath).size; + console.log(` ✓ Avatar updated for ${collaborator.firstName} ${collaborator.lastName} (${Math.round(fileSize / 1024)}KB)`); + updatedCount++; + + } catch (error) { + console.log(` ✗ Error updating avatar for ${baseName}:`, error); + } + } + + console.log(`✓ Updated ${updatedCount} collaborator avatars`); +} + +/** + * Clears all collaborator avatars (resets to default) + */ +export async function clearCollaboratorAvatars(dataSource: DataSource): Promise { + const collaboratorRepo = dataSource.getRepository(Collaborator); + + await collaboratorRepo + .createQueryBuilder() + .update(Collaborator) + .set({ avatarUrl: '/Assets/Site/Icon.png' }) + .execute(); + + console.log('✓ All collaborator avatars reset to default'); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/bryan-ceo.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/bryan-ceo.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3e50379ca8c25a105788a52cd90c4ef0287f0ed --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/bryan-ceo.seeder.ts @@ -0,0 +1,328 @@ +/** + * @fileoverview Bryan Wiedmer Soler (CEO) User & Resume Seeder + * Seeds the CEO founder user with complete profile and resume data. + * + * @module database/seeds/seeders/bryan-ceo.seeder + * @author Generated for Bryan Wiedmer Soler + */ + +import { DataSource } from 'typeorm'; +import { User } from '../../entities/user.entity'; +import { Collaborator } from '../../entities/collaborator.entity'; +import { UserRole } from '../../entities/user-role.entity'; +import { Experience } from '../../entities/experience.entity'; +import { Education } from '../../entities/education.entity'; +import { Skill } from '../../entities/skill.entity'; +import { Language } from '../../entities/language.entity'; +import { Hobby } from '../../entities/hobby.entity'; +import { BusinessDomain } from '../../entities/business-domain.entity'; +import { Role } from '@arkalliance/startupcms-ai-share'; +import { DEFAULT_ORGANIZATION_ID } from './organization.seeder'; +import bcrypt from 'bcryptjs'; + +/** + * CEO Collaborator ID. + */ +export const CEO_COLLABORATOR_ID = 'collab-ceo-bryan-001'; + +/** + * CEO User ID. + */ +export const CEO_USER_ID = 'user-ceo-bryan-001'; + +/** + * Seeds Bryan Wiedmer Soler as CEO & Founder. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when CEO is seeded + */ +export async function seedCEO(dataSource: DataSource): Promise { + const userRepo = dataSource.getRepository(User); + const collaboratorRepo = dataSource.getRepository(Collaborator); + const userRoleRepo = dataSource.getRepository(UserRole); + + // 1. Create or Update CEO User + const hashedPassword = await bcrypt.hash('ArkAlliance2026!Secure', 10); + + let ceoUser = await userRepo.findOne({ where: { username: 'bryanwiedmersoler' } }); + + if (!ceoUser) { + // Create new CEO user + ceoUser = userRepo.create({ + username: 'bryanwiedmersoler', + email: 'cryptoswissrd@gmail.com', + passwordHash: hashedPassword, + firstName: 'Bryan', + lastName: 'Wiedmer Soler', + title: 'CEO & Founder', + bio: `Franco-Swiss-Dominican entrepreneur and visionary founder of the Ark.Alliance initiative. At only 22 years old, Bryan brings 9 years of concrete experience in the cryptocurrency industry, demonstrating an exceptional ability to identify and seize market opportunities ahead of the curve. + +After 14 years of schooling in France, Bryan made the bold choice to leave the traditional education system during adolescence to pursue entrepreneurship. He immediately demonstrated remarkable entrepreneurial spirit by starting a mobile phone repair and resale business (microsoldering). In parallel, he built his first mining infrastructures with GPU rigs to mine Ethereum, quickly evolving to a farm of 60 Bitcoin ASICs. + +At just 18 years old, he founded CryptoSwiss, a cryptocurrency exchange in the Dominican Republic with a physical office. Five years later, the company remains highly profitable and generates exceptional returns, proof of his strategic vision and execution capability. + +Simultaneously, Bryan launched and managed a digital marketing agency where he developed innovative techniques to generate organic traffic for influencers and brands, discovering growth hacking strategies that few have mastered. His network of 70,000 followers on social media enables rapid deployment of new users on any new platform. + +As the visionary founder of Ark.Alliance, Bryan imagined a comprehensive AI-powered CMS platform designed specifically for startups, offering integrated solutions for resume drafting, task tracking, project publications, internal communication, and partial public visibility. To realize this ambitious technical vision, he met and commissioned Armand Richelet-Kleinberg as solution architect, who is now bringing the Ark.Alliance ecosystem to life through cutting-edge development. + +Self-taught by choice, Bryan embodies the modern entrepreneurial spirit: action trumps theory, results speak louder than degrees. His strength lies in his ability to transform challenges into opportunities and find angles of approach that most don't perceive. His creative problem-solving approach stems from practical rather than theoretical intelligence, favoring learning through action and concrete experimentation.`, + emailConfirmed: true, + isActive: true, + collaboratorId: CEO_COLLABORATOR_ID + }); + console.log('Creating new CEO user...'); + } else { + // Update existing CEO user with complete profile + ceoUser.email = 'cryptoswissrd@gmail.com'; + ceoUser.passwordHash = hashedPassword; + ceoUser.firstName = 'Bryan'; + ceoUser.lastName = 'Wiedmer Soler'; + ceoUser.title = 'CEO & Founder'; + ceoUser.bio = `Franco-Swiss-Dominican entrepreneur and visionary founder of the Ark.Alliance initiative. At only 22 years old, Bryan brings 9 years of concrete experience in the cryptocurrency industry, demonstrating an exceptional ability to identify and seize market opportunities ahead of the curve. + +After 14 years of schooling in France, Bryan made the bold choice to leave the traditional education system during adolescence to pursue entrepreneurship. He immediately demonstrated remarkable entrepreneurial spirit by starting a mobile phone repair and resale business (microsoldering). In parallel, he built his first mining infrastructures with GPU rigs to mine Ethereum, quickly evolving to a farm of 60 Bitcoin ASICs. + +At just 18 years old, he founded CryptoSwiss, a cryptocurrency exchange in the Dominican Republic with a physical office. Five years later, the company remains highly profitable and generates exceptional returns, proof of his strategic vision and execution capability. + +Simultaneously, Bryan launched and managed a digital marketing agency where he developed innovative techniques to generate organic traffic for influencers and brands, discovering growth hacking strategies that few have mastered. His network of 70,000 followers on social media enables rapid deployment of new users on any new platform. + +As the visionary founder of Ark.Alliance, Bryan imagined a comprehensive AI-powered CMS platform designed specifically for startups, offering integrated solutions for resume drafting, task tracking, project publications, internal communication, and partial public visibility. To realize this ambitious technical vision, he met and commissioned Armand Richelet-Kleinberg as solution architect, who is now bringing the Ark.Alliance ecosystem to life through cutting-edge development. + +Self-taught by choice, Bryan embodies the modern entrepreneurial spirit: action trumps theory, results speak louder than degrees. His strength lies in his ability to transform challenges into opportunities and find angles of approach that most don't perceive. His creative problem-solving approach stems from practical rather than theoretical intelligence, favoring learning through action and concrete experimentation.`; + ceoUser.emailConfirmed = true; + ceoUser.isActive = true; + ceoUser.collaboratorId = CEO_COLLABORATOR_ID; + console.log('Updating existing CEO user with complete profile...'); + } + + await userRepo.save(ceoUser); + console.log(`✓ CEO user ready (bryanwiedmersoler) ID: ${ceoUser.id}`); + + // 2. Assign ADMIN and COLLABORATOR roles + const adminRole = userRoleRepo.create({ + userId: ceoUser.id, + role: Role.ADMIN + }); + await userRoleRepo.save(adminRole); + + const collaboratorRole = userRoleRepo.create({ + userId: ceoUser.id, + role: Role.COLLABORATOR + }); + await userRoleRepo.save(collaboratorRole); + console.log('✓ ADMIN + COLLABORATOR roles assigned to CEO'); + + // 3. Create CEO Collaborator Profile + const ceoCollaborator = collaboratorRepo.create({ + id: CEO_COLLABORATOR_ID, + firstName: 'Bryan', + lastName: 'Wiedmer Soler', + email: 'cryptoswissrd@gmail.com', + position: 'CEO & Founder', + department: 'Executive Leadership', + bio: 'Visionary founder of Ark.Alliance with 9 years of experience in blockchain and cryptocurrency. Proven track record in building and scaling profitable businesses including CryptoSwiss exchange and large-scale mining operations. Expert in growth hacking, user acquisition, and strategic networking.', + avatarUrl: '/Assets/Site/Icon.png', + userId: ceoUser.id, // Link to user for resume data + organizationId: DEFAULT_ORGANIZATION_ID, + isActive: true, + displayOrder: 1, // CEO is first + hireDate: new Date('2025-01-01') + }); + + await collaboratorRepo.save(ceoCollaborator); + console.log('✓ CEO collaborator profile created (displayOrder: 1)'); +} + +/** + * Seeds CEO resume data (experiences, skills, etc.). + * + * @param dataSource - TypeORM DataSource connection + */ +export async function seedCEOResumeData(dataSource: DataSource): Promise { + const userRepo = dataSource.getRepository(User); + const experienceRepo = dataSource.getRepository(Experience); + const skillRepo = dataSource.getRepository(Skill); + const educationRepo = dataSource.getRepository(Education); + const languageRepo = dataSource.getRepository(Language); + const hobbyRepo = dataSource.getRepository(Hobby); + const businessDomainRepo = dataSource.getRepository(BusinessDomain); + + // Get CEO user by username + const ceoUser = await userRepo.findOne({ where: { username: 'bryanwiedmersoler' } }); + if (!ceoUser) { + console.log('○ CEO user not found, skipping resume data'); + return; + } + + // Check if resume data already exists + const existingExp = await experienceRepo.findOne({ where: { userId: ceoUser.id } }); + if (existingExp) { + console.log('○ CEO resume data already exists, skipping'); + return; + } + + // Experiences + const experiences = [ + { + userId: ceoUser.id, + collaboratorId: CEO_COLLABORATOR_ID, + company: 'Ark.Alliance', + position: 'CEO & Founder', + project: 'Ark.Alliance Ecosystem', + startDate: new Date('2025-01-01'), + endDate: null, + description: 'Founder and visionary of the Ark.Alliance initiative - an AI-powered CMS platform for startups. Conceived the entire ecosystem including resume drafting, task tracking, project publications, internal communication, and public visibility features. Commissioned and leads collaboration with Armand Richelet-Kleinberg (Solution Architect) to realize the technical vision.', + technologies: ['AI', 'Full-Stack Web Development', 'Product Management', 'Strategic Planning'], + isHighlighted: true, + displayOrder: 1 + }, + { + userId: ceoUser.id, + collaboratorId: CEO_COLLABORATOR_ID, + company: 'CryptoSwiss', + position: 'Founder & CEO', + project: 'Cryptocurrency Exchange Platform', + startDate: new Date('2021-01-01'), + endDate: null, + description: 'Founded and scaled a profitable cryptocurrency exchange in the Dominican Republic with physical office presence. Successfully operated for 5+ years generating exceptional profitability. Managed all aspects including regulatory compliance, customer acquisition, security, and operational excellence.', + technologies: ['Blockchain', 'Cryptocurrency', 'Fintech', 'Business Development', 'Compliance'], + isHighlighted: true, + displayOrder: 2 + }, + { + userId: ceoUser.id, + collaboratorId: CEO_COLLABORATOR_ID, + company: 'Self-Employed', + position: 'Digital Marketing Agency Founder', + project: 'Growth Hacking & Organic Traffic Generation', + startDate: new Date('2019-01-01'), + endDate: null, + description: 'Founded and operated a digital marketing agency specializing in organic traffic generation for influencers and brands. Developed innovative growth hacking techniques and strategies. Built a personal following of 70,000+ across social media platforms, enabling rapid user acquisition for new ventures.', + technologies: ['Growth Hacking', 'Social Media Marketing', 'SEO', 'Content Strategy', 'Influencer Marketing'], + isHighlighted: true, + displayOrder: 3 + }, + { + userId: ceoUser.id, + collaboratorId: CEO_COLLABORATOR_ID, + company: 'Self-Employed', + position: 'Cryptocurrency Mining Operator', + project: 'Large-Scale Mining Farm', + startDate: new Date('2017-01-01'), + endDate: new Date('2023-01-01'), + description: 'Built and operated cryptocurrency mining infrastructure starting with GPU rigs for Ethereum mining, scaling to a professional farm of 60 ASIC miners for Bitcoin. Managed technical infrastructure, power optimization, cooling systems, and profitability analysis.', + technologies: ['Blockchain', 'Ethereum', 'Bitcoin', 'Mining Hardware', 'Infrastructure Management', 'Hive OS'], + isHighlighted: false, + displayOrder: 4 + }, + { + userId: ceoUser.id, + collaboratorId: CEO_COLLABORATOR_ID, + company: 'Self-Employed', + position: 'Mobile Device Technician & Reseller', + project: 'Phone Repair & Resale Business', + startDate: new Date('2015-01-01'), + endDate: new Date('2018-01-01'), + description: 'First entrepreneurial venture in mobile phone repair and resale. Specialized in microsoldering repairs and refurbishment. Demonstrated early entrepreneurial capability and technical aptitude.', + technologies: ['Microsoldering', 'Hardware Repair', 'Business Operations', 'Sales'], + isHighlighted: false, + displayOrder: 5 + } + ]; + + for (const exp of experiences) { + await experienceRepo.save(experienceRepo.create(exp)); + } + console.log(`✓ ${experiences.length} experiences seeded for CEO`); + + // Skills + const skills = [ + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Blockchain Technology', level: 'Expert', yearsOfExperience: 9 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Cryptocurrency', level: 'Expert', yearsOfExperience: 9 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Growth Hacking', level: 'Expert', yearsOfExperience: 7 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Team Management', level: 'Advanced', yearsOfExperience: 5 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Strategic Networking', level: 'Expert', yearsOfExperience: 9 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'User Acquisition', level: 'Expert', yearsOfExperience: 7 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Python', level: 'Intermediate', yearsOfExperience: 3 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Business Strategy', level: 'Expert', yearsOfExperience: 9 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Creative Problem Solving', level: 'Expert', yearsOfExperience: 9 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Entrepreneurship', level: 'Expert', yearsOfExperience: 9 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Digital Marketing', level: 'Advanced', yearsOfExperience: 7 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Social Media Strategy', level: 'Expert', yearsOfExperience: 7 } + ]; + + for (const skill of skills) { + await skillRepo.save(skillRepo.create(skill)); + } + console.log(`✓ ${skills.length} skills seeded for CEO`); + + // Education + const education = educationRepo.create({ + userId: ceoUser.id, + collaboratorId: CEO_COLLABORATOR_ID, + institution: 'École Française', + degree: 'Secondary Education (Incomplete)', + fieldOfStudy: 'General Studies', + startDate: new Date('2008-09-01'), + endDate: new Date('2017-06-01'), + description: '14 years of schooling in France before choosing to pursue entrepreneurship full-time. Self-taught autodidact philosophy - learning through action and real-world experience rather than traditional academic pathways.' + }); + await educationRepo.save(education); + console.log('✓ Education record seeded for CEO'); + + // Languages + const languages = [ + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, language: 'French', speaking: 5, writing: 5, presenting: 5 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, language: 'Spanish', speaking: 5, writing: 5, presenting: 5 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, language: 'English', speaking: 2, writing: 2, presenting: 2 } + ]; + + for (const lang of languages) { + await languageRepo.save(languageRepo.create(lang)); + } + console.log(`✓ ${languages.length} languages seeded for CEO`); + + // Hobbies + const hobbies = [ + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Sport', description: 'Athletic activities and physical fitness', icon: 'Dumbbell', displayOrder: 1 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Music', description: 'Playing instruments and singing', icon: 'Music', displayOrder: 2 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Cryptocurrency', description: 'Active participant in cryptocurrency markets and blockchain technology', icon: 'Bitcoin', displayOrder: 3 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Human & Political Concerns', description: 'Engaged in social and political issues affecting humanity', icon: 'Vote', displayOrder: 4 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Equality of Chance', description: 'Advocating for equal opportunities for all', icon: 'Equal', displayOrder: 5 }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, name: 'Better World for Children', description: 'Committed to making the world a better place for future generations', icon: 'Baby', displayOrder: 6 } + ]; + + for (const hobby of hobbies) { + await hobbyRepo.save(hobbyRepo.create(hobby)); + } + console.log(`✓ ${hobbies.length} hobbies seeded for CEO`); + + // Business Domains + const domains = [ + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, domain: 'Blockchain & Cryptocurrency', level: 'Expert', yearsOfExperience: 9, description: 'Comprehensive experience across cryptocurrency exchange operations, mining infrastructure, and blockchain ecosystem development' }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, domain: 'Fintech', level: 'Expert', yearsOfExperience: 5, description: 'Founded and operated CryptoSwiss exchange with full regulatory compliance and customer management' }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, domain: 'Digital Marketing & Growth', level: 'Expert', yearsOfExperience: 7, description: 'Proven expertise in growth hacking, organic traffic generation, and user acquisition strategies with 70K+ social media following' }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, domain: 'Startup & Entrepreneurship', level: 'Expert', yearsOfExperience: 9, description: 'Serial entrepreneur with multiple successful ventures from mobile repair to cryptocurrency exchange to AI-powered platforms' }, + { userId: ceoUser.id, collaboratorId: CEO_COLLABORATOR_ID, domain: 'Team Leadership', level: 'Advanced', yearsOfExperience: 5, description: 'Experience managing hybrid teams of up to 7 employees across in-person and remote settings' } + ]; + + for (const domain of domains) { + await businessDomainRepo.save(businessDomainRepo.create(domain)); + } + console.log(`✓ ${domains.length} business domains seeded for CEO`); +} + +/** + * Updates Armand's collaborator to have displayOrder = 2 (below CEO). + * + * @param dataSource - TypeORM DataSource connection + */ +export async function updateFounderDisplayOrder(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder() + .update(Collaborator) + .set({ displayOrder: 2, position: 'AI Principal Architect & Full-Stack Developer' }) + .where('id = :id', { id: 'collab-founder-001' }) + .execute(); + + console.log('✓ Armand collaborator updated to displayOrder: 2'); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/business-domains.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/business-domains.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f0a7f9eefb7833ce850882e656b88c03b132f05 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/business-domains.seeder.ts @@ -0,0 +1,66 @@ +/** + * @fileoverview Business Domains Seeder + * Seeds business domain knowledge records for the resume/CV. + * + * @module database/seeds/seeders/business-domains.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { BusinessDomain, BUSINESS_DOMAIN_PRESETS } from '../../entities/business-domain.entity'; +import * as fs from 'fs'; +import * as path from 'path'; + +const DOMAINS_DATA_PATH = path.join(__dirname, '../../InitDbAsset/JsonDatas/business-domains.json'); + +/** + * Validates that domain is from the preset list. + * Returns the domain if valid, or null if invalid. + */ +function validateDomain(domain: string): string | null { + const isValid = BUSINESS_DOMAIN_PRESETS.includes(domain as any); + if (!isValid) { + console.warn(`⚠ Business domain "${domain}" is not in preset list, but will be added anyway.`); + } + return domain; +} + +/** + * Seeds the BusinessDomain table with expertise areas. + */ +export async function seedBusinessDomains(dataSource: DataSource): Promise { + const domainRepo = dataSource.getRepository(BusinessDomain); + const domainsData = JSON.parse(fs.readFileSync(DOMAINS_DATA_PATH, 'utf-8')); + + let seededCount = 0; + let displayOrder = 0; + + for (const domain of domainsData) { + const existing = await domainRepo.findOneBy({ domain: domain.domain }); + + if (!existing) { + const validDomain = validateDomain(domain.domain); + if (validDomain) { + const newDomain = domainRepo.create({ + domain: validDomain, + level: domain.level || 'Intermediate', + description: domain.description, + yearsOfExperience: domain.yearsOfExperience, + icon: domain.icon, + displayOrder: displayOrder++ + }); + await domainRepo.save(newDomain); + seededCount++; + } + } + } + + console.log(`✓ Business Domains seeded (${seededCount} records)`); +} + +/** + * Clears all BusinessDomain records from the database. + */ +export async function clearBusinessDomains(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(BusinessDomain).execute(); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/carousel.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/carousel.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..84de5b9aff79fa883fa471f5095aaee584edf3eb --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/carousel.seeder.ts @@ -0,0 +1,71 @@ +/** + * @fileoverview Carousel Seeder + * Seeds homepage carousel/slider items. + * + * @module database/seeds/seeders/carousel.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { CarouselItem } from '../../entities/carousel-item.entity'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Path to the carousel seed data JSON file. + */ +const CAROUSEL_DATA_PATH = path.join(__dirname, '../../InitDbAsset/JsonDatas/carousel.json'); + +/** + * Seeds the CarouselItem table with homepage slider content. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when carousel items are seeded + * + * @remarks + * - Only seeds if the CarouselItem table is empty + * - Each item includes: title, subtitle, description, imageUrl, linkUrl, order, isActive + * - Items are displayed on the homepage in order specified + * - Image URLs should reference assets in /Assets/Projects/ + */ +export async function seedCarousel(dataSource: DataSource): Promise { + const carouselPath = CAROUSEL_DATA_PATH; + + if (!fs.existsSync(carouselPath)) { + console.log('○ Carousel data file not found, skipping'); + return; + } + + const carouselRepo = dataSource.getRepository(CarouselItem); + + if (await carouselRepo.count() === 0) { + const carouselData = JSON.parse(fs.readFileSync(carouselPath, 'utf-8')); + + for (const item of carouselData) { + const carouselItem = carouselRepo.create({ + title: item.title, + subtitle: item.subtitle, + description: item.description, + imageUrl: item.imageUrl, + linkUrl: item.linkUrl, + linkText: item.linkText, + order: item.order, + isActive: item.isActive + }); + await carouselRepo.save(carouselItem); + } + + console.log(`✓ Carousel seeded (${carouselData.length} items)`); + } else { + console.log('○ Carousel already populated, skipping'); + } +} + +/** + * Clears all CarouselItem records from the database. + * + * @param dataSource - TypeORM DataSource connection + */ +export async function clearCarousel(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(CarouselItem).execute(); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/collaborator.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/collaborator.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..e7bca50ac7c70ebd23453090e64c393e34cee104 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/collaborator.seeder.ts @@ -0,0 +1,145 @@ +/** + * @fileoverview Collaborator Seeder + * Seeds team members for M2H organization with hierarchy. + * Migrates existing Profile data to Collaborator while maintaining resume data links. + * + * @module database/seeds/seeders/collaborator.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { Collaborator } from '../../entities/collaborator.entity'; +import { Profile } from '../../entities/profile.entity'; +import { DEFAULT_ORGANIZATION_ID } from './organization.seeder'; +import { CEO_COLLABORATOR_ID } from './bryan-ceo.seeder'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Default founder collaborator ID. + * This is the CEO/Founder linked to the existing profile and admin user. + */ +export const FOUNDER_COLLABORATOR_ID = 'collab-founder-001'; + +/** + * Path to the profile seed data JSON file. + */ +const PROFILE_DATA_PATH = path.join(__dirname, '../../InitDbAsset/JsonDatas/profile.json'); + +/** + * Seeds the Collaborator table with team hierarchy. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when collaborators are seeded + * + * @remarks + * - Creates founder from existing profile.json data + * - Links founder to organization + * - Creates sample team hierarchy for demonstration + * - Preserves profileId = 1 for backward compatibility with existing resume data + */ +export async function seedCollaborators(dataSource: DataSource): Promise { + const collaboratorRepo = dataSource.getRepository(Collaborator); + + // Check if Armand's collaborator already exists + const existingFounder = await collaboratorRepo.findOne({ where: { id: FOUNDER_COLLABORATOR_ID } }); + if (existingFounder) { + console.log('○ Armand collaborator already exists, skipping'); + return; + } + + // Load founder data from existing profile.json + const profileData = JSON.parse(fs.readFileSync(PROFILE_DATA_PATH, 'utf-8')); + + // Create Solution Architect (Armand - linked to profile ID 1 for backward compatibility) + const founder = collaboratorRepo.create({ + id: FOUNDER_COLLABORATOR_ID, + firstName: profileData.firstName || 'Armand', + lastName: profileData.lastName || 'Richelet-Kleinberg', + email: profileData.email || 'arkleinberg@gmail.com', + position: 'AI Principal Architect & Full-Stack Developer', + department: 'Technology', + bio: profileData.overview || 'Solution Architect bringing the Ark.Alliance vision to life through cutting-edge AI and full-stack development. Shares the founder\'s commitment to equality of chance, human concerns, and making the world better for future generations.', + avatarUrl: profileData.avatarUrl || '/Assets/Site/Icon.png', + linkedinUrl: profileData.linkedinUrl, + githubUrl: profileData.githubUrl, + hireDate: new Date('2024-01-01'), + isActive: true, + displayOrder: 0, // Will be updated to 2 by updateFounderDisplayOrder + organizationId: DEFAULT_ORGANIZATION_ID, + reportsToId: CEO_COLLABORATOR_ID // Reports to Bryan (CEO) + }); + + await collaboratorRepo.save(founder); + console.log('✓ Armand collaborator seeded (from profile.json)'); + + // Note: Additional team members should be added through the admin interface + // The seed only includes the founder to ensure real data alignment +} + +/** + * Updates existing resume data to link to the founder collaborator. + * This maintains backward compatibility by setting collaboratorId on existing records. + * + * @param dataSource - TypeORM DataSource connection + */ +export async function linkResumeDataToCollaborator(dataSource: DataSource): Promise { + // Update Experience records + await dataSource.createQueryBuilder() + .update('experience') + .set({ collaboratorId: FOUNDER_COLLABORATOR_ID }) + .where('profileId = :profileId', { profileId: 1 }) + .andWhere('collaboratorId IS NULL') + .execute(); + + // Update Education records + await dataSource.createQueryBuilder() + .update('education') + .set({ collaboratorId: FOUNDER_COLLABORATOR_ID }) + .where('profileId = :profileId', { profileId: 1 }) + .andWhere('collaboratorId IS NULL') + .execute(); + + // Update Skills records + await dataSource.createQueryBuilder() + .update('skills') + .set({ collaboratorId: FOUNDER_COLLABORATOR_ID }) + .where('profileId = :profileId', { profileId: 1 }) + .andWhere('collaboratorId IS NULL') + .execute(); + + // Update Languages records + await dataSource.createQueryBuilder() + .update('languages') + .set({ collaboratorId: FOUNDER_COLLABORATOR_ID }) + .where('profileId = :profileId', { profileId: 1 }) + .andWhere('collaboratorId IS NULL') + .execute(); + + // Update Hobbies records + await dataSource.createQueryBuilder() + .update('hobbies') + .set({ collaboratorId: FOUNDER_COLLABORATOR_ID }) + .where('profileId = :profileId', { profileId: 1 }) + .andWhere('collaboratorId IS NULL') + .execute(); + + // Update Business Domains records + await dataSource.createQueryBuilder() + .update('business_domains') + .set({ collaboratorId: FOUNDER_COLLABORATOR_ID }) + .where('profileId = :profileId', { profileId: 1 }) + .andWhere('collaboratorId IS NULL') + .execute(); + + console.log('✓ Resume data linked to founder collaborator'); +} + +/** + * Clears all Collaborator records from the database. + * + * @param dataSource - TypeORM DataSource connection + */ +export async function clearCollaborators(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(Collaborator).execute(); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/education.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/education.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..58d584545a7e63a02f21437a5fc6d9f1c8ba704e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/education.seeder.ts @@ -0,0 +1,55 @@ +/** + * @fileoverview Education Seeder + * Seeds educational qualifications for the resume/CV. + * + * @module database/seeds/seeders/education.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { Education } from '../../entities/education.entity'; +import * as fs from 'fs'; +import * as path from 'path'; + +const EDUCATION_DATA_PATH = path.join(__dirname, '../../InitDbAsset/JsonDatas/education.json'); + +/** + * Seeds the Education table with qualifications. + */ +export async function seedEducation(dataSource: DataSource): Promise { + const eduRepo = dataSource.getRepository(Education); + const educationData = JSON.parse(fs.readFileSync(EDUCATION_DATA_PATH, 'utf-8')); + + let seededCount = 0; + let displayOrder = 0; + + for (const edu of educationData) { + const existing = await eduRepo.findOneBy({ + institution: edu.institution, + degree: edu.degree + }); + + if (!existing) { + const newEdu = eduRepo.create({ + degree: edu.degree, + institution: edu.institution, + fieldOfStudy: edu.fieldOfStudy, + startDate: edu.startYear ? new Date(`${edu.startYear}-09-01`) : new Date(), + endDate: edu.endYear ? new Date(`${edu.endYear}-06-30`) : undefined, + description: edu.description, + displayOrder: displayOrder++ + }); + await eduRepo.save(newEdu); + seededCount++; + } + } + + console.log(`✓ Education seeded (${seededCount} records)`); +} + +/** + * Clears all Education records from the database. + */ +export async function clearEducation(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(Education).execute(); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/experience.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/experience.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..4309477c4d82923339f48084e0548dea87a954a0 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/experience.seeder.ts @@ -0,0 +1,125 @@ +/** + * @fileoverview Experience Seeder + * Seeds professional work experience records for the resume/CV. + * + * @module database/seeds/seeders/experience.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { Experience } from '../../entities/experience.entity'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Path to the experience seed data JSON file. + */ +const EXPERIENCE_DATA_PATH = path.join(__dirname, '../../InitDbAsset/JsonDatas/experience.json'); + +/** + * Month name to number mapping. + */ +const MONTH_MAP: Record = { + 'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', + 'May': '05', 'Jun': '06', 'Jul': '07', 'Aug': '08', + 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12' +}; + +/** + * Parses a period string to extract start and end dates. + * + * @param period - Period string (e.g., "Dec 2022 - Present", "Jan 2021 - Feb 2025") + * @returns Object with startDate and endDate (null if "Present") + * + * @example + * parsePeriod("Dec 2022 - Present") -> { startDate: "2022-12-01", endDate: null } + * parsePeriod("Jan 2021 - Feb 2025") -> { startDate: "2021-01-01", endDate: "2025-02-01" } + */ +function parsePeriod(period: string): { startDate: string; endDate: string | null } { + const parts = period.split(' - '); + + // Parse start date + const startParts = parts[0].trim().split(' '); + const startMonth = MONTH_MAP[startParts[0]] || '01'; + const startYear = startParts[1] || '2020'; + const startDate = `${startYear}-${startMonth}-01`; + + // Parse end date + let endDate: string | null = null; + if (parts[1] && parts[1].trim() !== 'Present') { + const endParts = parts[1].trim().split(' '); + const endMonth = MONTH_MAP[endParts[0]] || '01'; + const endYear = endParts[1] || '2020'; + endDate = `${endYear}-${endMonth}-01`; + } + + return { startDate, endDate }; +} + +/** + * Parses technologies string to array. + * + * @param tech - Comma-separated technology string + * @returns Array of technology names, trimmed + */ +function parseTechnologies(tech: string): string[] { + if (!tech) return []; + return tech.split(',').map(t => t.trim()).filter(t => t.length > 0); +} + +/** + * Seeds the Experience table with professional work history. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when experiences are seeded + * + * @remarks + * - Checks for duplicates by company + project combination + * - Parses 'period' field to extract startDate and endDate + * - Parses 'tech' field to create technologies array + * - Maps 'role' field to 'position' for compatibility + * - Maps 'desc' field to 'description' for compatibility + */ +export async function seedExperience(dataSource: DataSource): Promise { + const expRepo = dataSource.getRepository(Experience); + const experienceData = JSON.parse(fs.readFileSync(EXPERIENCE_DATA_PATH, 'utf-8')); + + let seededCount = 0; + let displayOrder = 0; + + for (const exp of experienceData) { + const existing = await expRepo.findOneBy({ company: exp.company, project: exp.project }); + if (!existing) { + // Parse period to dates + const { startDate, endDate } = parsePeriod(exp.period || 'Jan 2020 - Present'); + + // Parse technologies + const technologies = parseTechnologies(exp.tech || ''); + + const newExp = expRepo.create({ + company: exp.company, + project: exp.project, + position: exp.role || exp.position || 'Unknown', + startDate: new Date(startDate), + endDate: endDate ? new Date(endDate) : null, + description: exp.desc || exp.description, + technologies: technologies, + displayOrder: displayOrder++, + isHighlighted: displayOrder === 1 // Highlight first experience + }); + await expRepo.save(newExp); + seededCount++; + } + } + + console.log(`✓ Experience seeded (${seededCount} records)`); +} + +/** + * Clears all Experience records from the database. + * + * @param dataSource - TypeORM DataSource connection + */ +export async function clearExperience(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(Experience).execute(); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/hobbies.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/hobbies.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0ae007e4b3896edcc6d818c74cc00abc7cc6b63 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/hobbies.seeder.ts @@ -0,0 +1,49 @@ +/** + * @fileoverview Hobbies Seeder + * Seeds hobby records for the resume/CV. + * + * @module database/seeds/seeders/hobbies.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { Hobby } from '../../entities/hobby.entity'; +import * as fs from 'fs'; +import * as path from 'path'; + +const HOBBIES_DATA_PATH = path.join(__dirname, '../../InitDbAsset/JsonDatas/hobbies.json'); + +/** + * Seeds the Hobby table with personal interests. + */ +export async function seedHobbies(dataSource: DataSource): Promise { + const hobbyRepo = dataSource.getRepository(Hobby); + const hobbiesData = JSON.parse(fs.readFileSync(HOBBIES_DATA_PATH, 'utf-8')); + + let seededCount = 0; + let displayOrder = 0; + + for (const hobby of hobbiesData) { + const existing = await hobbyRepo.findOneBy({ name: hobby.name }); + + if (!existing) { + const newHobby = hobbyRepo.create({ + name: hobby.name, + description: hobby.description, + icon: hobby.icon, + displayOrder: displayOrder++ + }); + await hobbyRepo.save(newHobby); + seededCount++; + } + } + + console.log(`✓ Hobbies seeded (${seededCount} records)`); +} + +/** + * Clears all Hobby records from the database. + */ +export async function clearHobbies(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(Hobby).execute(); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/index.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8e90e32782e8db2b6b2b9f429e00551f5826f4b --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/index.ts @@ -0,0 +1,49 @@ +/** + * @fileoverview Seeders Index + * Barrel export for all database seeders. + * + * @module database/seeds/seeders + * @author Armand Richelet-Kleinberg + */ + +// Organization & Team (NEW - Corporate CMS) +export { seedOrganization, clearOrganization, DEFAULT_ORGANIZATION_ID } from './organization.seeder'; +export { seedCollaborators, clearCollaborators, linkResumeDataToCollaborator, FOUNDER_COLLABORATOR_ID } from './collaborator.seeder'; +export { seedUserRoles, clearUserRoles } from './user-roles.seeder'; +export { seedCEO, seedCEOResumeData, updateFounderDisplayOrder, CEO_COLLABORATOR_ID, CEO_USER_ID } from './bryan-ceo.seeder'; +export { seedMarioConsultant, seedMarioResumeData, MARIO_COLLABORATOR_ID } from './mario-consultant.seeder'; +export { seedRaduDirector, seedRaduResumeData, RADU_COLLABORATOR_ID } from './radu-director.seeder'; +export { seedArmandArchitect, seedArmandResumeData, ARMAND_COLLABORATOR_ID } from './armand-architect.seeder'; +export { seedLennyCSM, seedLennyResumeData, LENNY_COLLABORATOR_ID } from './lenny-csm.seeder'; +export { seedCollaboratorAvatars, clearCollaboratorAvatars } from './avatar.seeder'; + +// Profile & Identity (Legacy - kept for backward compatibility) +export { seedProfile, clearProfile } from './profile.seeder'; + +// Resume/CV Components +export { seedExperience, clearExperience } from './experience.seeder'; +export { seedSkills, clearSkills } from './skills.seeder'; +export { seedEducation, clearEducation } from './education.seeder'; +export { seedLanguages, clearLanguages } from './languages.seeder'; +export { seedHobbies, clearHobbies } from './hobbies.seeder'; +export { seedBusinessDomains, clearBusinessDomains } from './business-domains.seeder'; + +// Content & Media +export { seedCarousel, clearCarousel } from './carousel.seeder'; +export { seedMedia, clearMedia } from './media.seeder'; + +// Master Data +export { seedTechnologies, clearTechnologies } from './technology.seeder'; + +// Portfolio +export { seedProjects, clearProjects } from './projects.seeder'; + +// Themes +export { seedThemes, clearThemes } from './theme.seeder'; + +// Tasks / Accomplishments (Recognition System) +export { seedTasks, clearTasks } from './tasks.seeder'; + +// AI Prompt Templates +export { seedPromptTemplates, clearPromptTemplates } from './prompt-template.seeder'; + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/languages.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/languages.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..25c31e57e7fa12cd8cd8c8ac974dc432d77ba8fb --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/languages.seeder.ts @@ -0,0 +1,57 @@ +/** + * @fileoverview Languages Seeder + * Seeds language proficiency records for the resume/CV. + * + * @module database/seeds/seeders/languages.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { Language } from '../../entities/language.entity'; +import * as fs from 'fs'; +import * as path from 'path'; + +const LANGUAGES_DATA_PATH = path.join(__dirname, '../../InitDbAsset/JsonDatas/languages.json'); + +/** + * Validates proficiency value is between 1 and 5. + */ +function validateProficiency(value: number): number { + return Math.max(1, Math.min(5, value || 1)); +} + +/** + * Seeds the Language table with proficiency records. + */ +export async function seedLanguages(dataSource: DataSource): Promise { + const langRepo = dataSource.getRepository(Language); + const languagesData = JSON.parse(fs.readFileSync(LANGUAGES_DATA_PATH, 'utf-8')); + + let seededCount = 0; + let displayOrder = 0; + + for (const lang of languagesData) { + const existing = await langRepo.findOneBy({ language: lang.language }); + + if (!existing) { + const newLang = langRepo.create({ + language: lang.language, + speaking: validateProficiency(lang.speaking), + writing: validateProficiency(lang.writing), + presenting: validateProficiency(lang.presenting), + displayOrder: displayOrder++ + }); + await langRepo.save(newLang); + seededCount++; + } + } + + console.log(`✓ Languages seeded (${seededCount} records)`); +} + +/** + * Clears all Language records from the database. + */ +export async function clearLanguages(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(Language).execute(); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/lenny-csm.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/lenny-csm.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..d395fcac71b6aa802af0decfba07b9471761b803 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/lenny-csm.seeder.ts @@ -0,0 +1,278 @@ +/** + * @fileoverview Lenny Avril (Customer Success Manager) User & Resume Seeder + * Seeds the Customer Success Manager with complete profile and resume data. + * Reports to Radu Dinulescu (Public Relations Director). + * + * @module database/seeds/seeders/lenny-csm.seeder + * @author System - Based on CV data provided + */ + +import { DataSource } from 'typeorm'; +import { User } from '../../entities/user.entity'; +import { Collaborator } from '../../entities/collaborator.entity'; +import { UserRole } from '../../entities/user-role.entity'; +import { Experience } from '../../entities/experience.entity'; +import { Education } from '../../entities/education.entity'; +import { Skill } from '../../entities/skill.entity'; +import { Language } from '../../entities/language.entity'; +import { Hobby } from '../../entities/hobby.entity'; +import { BusinessDomain } from '../../entities/business-domain.entity'; +import { Role } from '@arkalliance/startupcms-ai-share'; +import { DEFAULT_ORGANIZATION_ID } from './organization.seeder'; +import { RADU_COLLABORATOR_ID } from './radu-director.seeder'; +import bcrypt from 'bcryptjs'; + +/** + * Lenny Collaborator ID. + */ +export const LENNY_COLLABORATOR_ID = 'collab-lenny-avril-001'; + +/** + * Seeds Lenny Avril as Customer Success Manager. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when CSM is seeded + */ +export async function seedLennyCSM(dataSource: DataSource): Promise { + const userRepo = dataSource.getRepository(User); + const collaboratorRepo = dataSource.getRepository(Collaborator); + const userRoleRepo = dataSource.getRepository(UserRole); + + // 1. Create or Update Lenny User + const hashedPassword = await bcrypt.hash('ArkAlliance2026!Secure', 10); + + let lennyUser = await userRepo.findOne({ where: { username: 'lennyavril' } }); + + if (!lennyUser) { + // Create new user + lennyUser = userRepo.create({ + username: 'lennyavril', + email: 'lenny.avril@gmail.com', + passwordHash: hashedPassword, + firstName: 'Lenny', + lastName: 'Avril', + title: 'Customer Success Manager & Account Manager – Startup Segment', + bio: 'Passionate about commerce and negotiation, my ambition is to enrich my knowledge and deepen my skills by putting them to the test in a professional environment. Currently pursuing high school studies while actively developing project creation, teamwork, adaptability, and autonomous work capabilities through civic engagement and diverse professional experiences.', + emailConfirmed: true, + isActive: true, + collaboratorId: LENNY_COLLABORATOR_ID + }); + console.log('Creating new Lenny user...'); + } else { + // Update existing user + lennyUser.email = 'lenny.avril@gmail.com'; + lennyUser.passwordHash = hashedPassword; + lennyUser.firstName = 'Lenny'; + lennyUser.lastName = 'Avril'; + lennyUser.title = 'Customer Success Manager & Account Manager – Startup Segment'; + lennyUser.bio = 'Passionate about commerce and negotiation, my ambition is to enrich my knowledge and deepen my skills by putting them to the test in a professional environment. Currently pursuing high school studies while actively developing project creation, teamwork, adaptability, and autonomous work capabilities through civic engagement and diverse professional experiences.'; + lennyUser.emailConfirmed = true; + lennyUser.isActive = true; + lennyUser.collaboratorId = LENNY_COLLABORATOR_ID; + console.log('Updating existing Lenny user...'); + } + + await userRepo.save(lennyUser); + console.log(`✓ Lenny user ready (lennyavril) ID: ${lennyUser.id}`); + + // 2. Assign COLLABORATOR role + const existingRole = await userRoleRepo.findOne({ + where: { userId: lennyUser.id, role: Role.COLLABORATOR } + }); + + if (!existingRole) { + const collaboratorRole = userRoleRepo.create({ + userId: lennyUser.id, + role: Role.COLLABORATOR + }); + await userRoleRepo.save(collaboratorRole); + console.log('✓ COLLABORATOR role assigned to Lenny'); + } + + // 3. Create Lenny Collaborator Profile + const lennyCollaborator = collaboratorRepo.create({ + id: LENNY_COLLABORATOR_ID, + firstName: 'Lenny', + lastName: 'Avril', + email: 'lenny.avril@gmail.com', + position: 'Customer Success Manager & Account Manager', + department: 'Customer Success & Sales', + bio: 'Customer Success Manager dedicated to client satisfaction and startup segment growth. Combines civic engagement experience, military preparation discipline, and international exposure (Dominican Republic) to deliver exceptional client outcomes. Expert in project creation, adaptability, and multilingual communication.', + avatarUrl: '/Assets/Site/Icon.png', + userId: lennyUser.id, // Link to user for resume data + organizationId: DEFAULT_ORGANIZATION_ID, + reportsToId: RADU_COLLABORATOR_ID, // Reports to Radu Dinulescu + isActive: true, + displayOrder: 4, // After Mario (3) + hireDate: new Date('2026-01-07') + }); + + await collaboratorRepo.save(lennyCollaborator); + console.log('✓ Lenny collaborator profile created (displayOrder: 4, reports to Radu)'); +} + +/** + * Seeds Lenny resume data (education, experiences, skills). + * + * @param dataSource - TypeORM DataSource connection + */ +export async function seedLennyResumeData(dataSource: DataSource): Promise { + const userRepo = dataSource.getRepository(User); + const experienceRepo = dataSource.getRepository(Experience); + const skillRepo = dataSource.getRepository(Skill); + const educationRepo = dataSource.getRepository(Education); + const languageRepo = dataSource.getRepository(Language); + const hobbyRepo = dataSource.getRepository(Hobby); + const businessDomainRepo = dataSource.getRepository(BusinessDomain); + + // Get Lenny user by username + const lennyUser = await userRepo.findOne({ where: { username: 'lennyavril' } }); + if (!lennyUser) { + console.log('○ Lenny user not found, skipping resume data'); + return; + } + + // Check if resume data already exists + const existingExp = await experienceRepo.findOne({ where: { userId: lennyUser.id } }); + if (existingExp) { + console.log('○ Lenny resume data already exists, skipping'); + return; + } + + // Experiences + const experiences = [ + { + userId: lennyUser.id, + company: 'M2H – Mindful AI to Humanity / Ark.Alliance', + position: 'Customer Success Manager & Account Manager – Startup Segment', + project: 'Ark.Alliance.StartupCms.Ai', + startDate: new Date('2026-01-07'), + endDate: null, + description: 'Managing customer success and account relationships for the startup segment. Ensuring client satisfaction and driving growth through effective communication, project coordination, and adaptive problem-solving. Leveraging multilingual skills and diverse professional background.', + technologies: ['Customer Success', 'Account Management', 'Client Relations', 'Communication', 'CRM'], + isHighlighted: true, + displayOrder: 1 + }, + { + userId: lennyUser.id, + company: 'Currency Exchange Office', + position: 'Currency Exchange Assistant', + project: 'Customer Service & Currency Operations', + startDate: new Date('2024-06-01'), + endDate: new Date('2024-08-31'), + description: 'Client reception and customer service. Knowledge of monetary systems and exchange software. Mastery of languages for international clients. Exchange rate negotiation. Stress management and adaptability in high-pressure retail environment.', + technologies: ['Customer Service', 'Currency Systems', 'Negotiation', 'Stress Management', 'Spanish', 'English'], + isHighlighted: true, + displayOrder: 2 + }, + { + userId: lennyUser.id, + company: 'French Navy', + position: 'Marine Military Preparation Trainee', + project: 'Military Training & Discipline', + startDate: new Date('2023-09-01'), + endDate: new Date('2024-06-30'), + description: 'Marine military preparation program developing respect for orders, accountability, teamwork, and time management skills. Rigorous training in discipline and organizational procedures.', + technologies: ['Leadership', 'Teamwork', 'Time Management', 'Discipline', 'Organization'], + isHighlighted: true, + displayOrder: 3 + }, + { + userId: lennyUser.id, + company: 'City Hall Youth Services & Municipal Police', + position: 'High School Internship', + project: 'Civic Internships', + startDate: new Date('2023-02-01'), + endDate: new Date('2023-02-28'), + description: 'Youth Services: Software-based creative projects and digital tools. Municipal Police: Understanding of authority respect and civic responsibility. Developed professional communication and institutional awareness.', + technologies: ['Software Tools', 'Creativity', 'Professional Communication', 'Civic Responsibility'], + isHighlighted: false, + displayOrder: 4 + }, + { + userId: lennyUser.id, + company: 'Conseil Municipal Jeunes (Youth Municipal Council)', + position: 'Youth Council Member', + project: 'Civic Engagement & Innovation Projects', + startDate: new Date('2022-09-01'), + endDate: new Date('2023-06-30'), + description: 'Active participation and civic engagement. Creation of innovative projects for youth community. Collaboration with local government on youth initiatives. Development of leadership and public speaking skills.', + technologies: ['Project Creation', 'Civic Engagement', 'Public Speaking', 'Leadership', 'Innovation'], + isHighlighted: true, + displayOrder: 5 + } + ]; + + for (const exp of experiences) { + await experienceRepo.save(experienceRepo.create(exp)); + } + console.log(`✓ ${experiences.length} experiences seeded for Lenny`); + + // Skills + const skills = [ + { userId: lennyUser.id, name: 'Project Creation', level: 'Advanced', yearsOfExperience: 3 }, + { userId: lennyUser.id, name: 'Autonomous Work', level: 'Advanced', yearsOfExperience: 3 }, + { userId: lennyUser.id, name: 'Teamwork', level: 'Advanced', yearsOfExperience: 4 }, + { userId: lennyUser.id, name: 'Adaptability', level: 'Expert', yearsOfExperience: 4 }, + { userId: lennyUser.id, name: 'Customer Service', level: 'Advanced', yearsOfExperience: 2 }, + { userId: lennyUser.id, name: 'Negotiation', level: 'Intermediate', yearsOfExperience: 1 }, + { userId: lennyUser.id, name: 'Stress Management', level: 'Advanced', yearsOfExperience: 2 }, + { userId: lennyUser.id, name: 'Time Management', level: 'Advanced', yearsOfExperience: 2 }, + { userId: lennyUser.id, name: 'Communication', level: 'Advanced', yearsOfExperience: 4 }, + { userId: lennyUser.id, name: 'Civic Leadership', level: 'Intermediate', yearsOfExperience: 2 } + ]; + + for (const skill of skills) { + await skillRepo.save(skillRepo.create(skill)); + } + console.log(`✓ ${skills.length} skills seeded for Lenny`); + + // Education + const education = educationRepo.create({ + userId: lennyUser.id, + institution: 'Lycée Jean Puy, Roanne', + degree: 'Terminale Générale (High School Senior)', + fieldOfStudy: 'General Studies', + startDate: new Date('2024-09-01'), + // endDate omitted - currently enrolled + description: 'Currently in Terminale Générale (final year of high school). Previously obtained Diplôme du Brevet des Collèges (middle school diploma). Developing academic foundation while gaining practical experience through internships and civic engagement.' + }); + await educationRepo.save(education); + console.log('✓ Education record seeded for Lenny'); + + // Languages + const languages = [ + { userId: lennyUser.id, language: 'French', speaking: 5, writing: 5, presenting: 5 }, + { userId: lennyUser.id, language: 'English', speaking: 4, writing: 4, presenting: 4 }, // B2 level + { userId: lennyUser.id, language: 'Spanish', speaking: 3, writing: 3, presenting: 3 } // B1 level + ]; + + for (const lang of languages) { + await languageRepo.save(languageRepo.create(lang)); + } + console.log(`✓ ${languages.length} languages seeded for Lenny`); + + // Hobbies + const hobbies = [ + { userId: lennyUser.id, name: 'Commerce & Negotiation', description: 'Passionate about business development and negotiation strategies', icon: 'HandCoins', displayOrder: 1 }, + { userId: lennyUser.id, name: 'Civic Engagement', description: 'Active participation in youth council and community projects', icon: 'Landmark', displayOrder: 2 }, + { userId: lennyUser.id, name: 'Project Innovation', description: 'Creating and implementing innovative projects for community benefit', icon: 'Rocket', displayOrder: 3 } + ]; + + for (const hobby of hobbies) { + await hobbyRepo.save(hobbyRepo.create(hobby)); + } + console.log(`✓ ${hobbies.length} hobbies seeded for Lenny`); + + // Business Domains + const domains = [ + { userId: lennyUser.id, domain: 'Customer Success & Account Management', level: 'Intermediate', yearsOfExperience: 1, description: 'Managing client relationships, ensuring satisfaction, and driving growth in the startup segment. Focus on proactive communication and problem-solving.' }, + { userId: lennyUser.id, domain: 'Sales & Negotiation', level: 'Intermediate', yearsOfExperience: 1, description: 'Experience in currency exchange negotiation and client-facing sales interactions. Developing skills in value proposition and deal closing.' }, + { userId: lennyUser.id, domain: 'Civic & Public Service', level: 'Advanced', yearsOfExperience: 2, description: 'Youth municipal council experience with project creation and civic engagement. Understanding of public institutions and community service.' } + ]; + + for (const domain of domains) { + await businessDomainRepo.save(businessDomainRepo.create(domain)); + } + console.log(`✓ ${domains.length} business domains seeded for Lenny`); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/mario-consultant.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/mario-consultant.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..170e857f1ab2c82fc72e4ae1d38f61e30e73f0f3 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/mario-consultant.seeder.ts @@ -0,0 +1,245 @@ +/** + * @fileoverview Mario Schmidt (Consultant) User & Resume Seeder + * Seeds the consultant user with complete profile and resume data. + * + * @module database/seeds/seeders/mario-consultant.seeder + * @author Generated for Mario Schmidt + */ + +import { DataSource } from 'typeorm'; +import { User } from '../../entities/user.entity'; +import { Collaborator } from '../../entities/collaborator.entity'; +import { UserRole } from '../../entities/user-role.entity'; +import { Experience } from '../../entities/experience.entity'; +import { Education } from '../../entities/education.entity'; +import { Skill } from '../../entities/skill.entity'; +import { Language } from '../../entities/language.entity'; +import { Hobby } from '../../entities/hobby.entity'; +import { BusinessDomain } from '../../entities/business-domain.entity'; +import { Role } from '@arkalliance/startupcms-ai-share'; +import { DEFAULT_ORGANIZATION_ID } from './organization.seeder'; +import { FOUNDER_COLLABORATOR_ID } from './collaborator.seeder'; +import bcrypt from 'bcryptjs'; + +/** + * Mario Collaborator ID. + */ +export const MARIO_COLLABORATOR_ID = 'collab-mario-schmidt-001'; + +/** + * Seeds Mario Schmidt as Consultant. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when consultant is seeded + */ +export async function seedMarioConsultant(dataSource: DataSource): Promise { + const userRepo = dataSource.getRepository(User); + const collaboratorRepo = dataSource.getRepository(Collaborator); + const userRoleRepo = dataSource.getRepository(UserRole); + + // 1. Create or Update Mario User + const hashedPassword = await bcrypt.hash('ArkAlliance2026!Secure', 10); + + let marioUser = await userRepo.findOne({ where: { username: 'marioschmidt' } }); + + if (!marioUser) { + // Create new user + marioUser = userRepo.create({ + username: 'marioschmidt', + email: 'mario.schmidt@m2h.ai', + passwordHash: hashedPassword, + firstName: 'Mario', + lastName: 'Schmidt', + title: 'Consultant Prompt Testing System', + bio: 'School was tough. My grades were never good enough, and teachers weren\'t shy about making me feel like I was falling behind. I carried this heavy weight—believing I had nothing to offer the world. To escape, I\'d come home and lose myself in the one place where I felt alive: Fortnite. That\'s where everything changed. In a random public match, I met someone who just got me—Bryan Wiedmer. We started talking, not just about the game, but about real life. He told me that he\'d been written off at school too. But he refused to accept those limits. He taught me something I\'ll never forget: every human being has value. That day, my whole perspective shifted. I realized that my experiences, my ideas, who I am—it all matters. So I turned off the game and started learning to code. Not to escape anymore, but to build tools that help everyone recognize their own worth. I wanted to walk the path of people who changed the world—Elon Musk, Jeff Bezos, Bill Gates. I took on freelance projects, taught myself relentlessly through the internet, and mastered the foundations of web development: HTML, CSS, JSON, XML. That foundation led me here. Bryan told me about Ark.Alliance—a revolutionary project that aims to "send the invoice to AI" by finally compensating every human for their contribution to collective knowledge. Today, I\'m responsible for testing and optimizing the user interface. I bring that vision to life by creating responsive designs that work on every screen and developing AI-powered test scenarios that guarantee total reliability. I went from thinking I was useless to building the system that will recognize everyone\'s value.', + emailConfirmed: true, + isActive: true, + collaboratorId: MARIO_COLLABORATOR_ID + }); + console.log('Creating new Mario user...'); + } else { + // Update existing user + marioUser.email = 'mario.schmidt@m2h.ai'; + marioUser.passwordHash = hashedPassword; + marioUser.firstName = 'Mario'; + marioUser.lastName = 'Schmidt'; + marioUser.title = 'Consultant Prompt Testing System'; + marioUser.bio = 'School was tough. My grades were never good enough, and teachers weren\'t shy about making me feel like I was falling behind. I carried this heavy weight—believing I had nothing to offer the world. To escape, I\'d come home and lose myself in the one place where I felt alive: Fortnite. That\'s where everything changed. In a random public match, I met someone who just got me—Bryan Wiedmer. We started talking, not just about the game, but about real life. He told me that he\'d been written off at school too. But he refused to accept those limits. He taught me something I\'ll never forget: every human being has value. That day, my whole perspective shifted. I realized that my experiences, my ideas, who I am—it all matters. So I turned off the game and started learning to code. Not to escape anymore, but to build tools that help everyone recognize their own worth. I wanted to walk the path of people who changed the world—Elon Musk, Jeff Bezos, Bill Gates. I took on freelance projects, taught myself relentlessly through the internet, and mastered the foundations of web development: HTML, CSS, JSON, XML. That foundation led me here. Bryan told me about Ark.Alliance—a revolutionary project that aims to "send the invoice to AI" by finally compensating every human for their contribution to collective knowledge. Today, I\'m responsible for testing and optimizing the user interface. I bring that vision to life by creating responsive designs that work on every screen and developing AI-powered test scenarios that guarantee total reliability. I went from thinking I was useless to building the system that will recognize everyone\'s value.'; + marioUser.emailConfirmed = true; + marioUser.isActive = true; + marioUser.collaboratorId = MARIO_COLLABORATOR_ID; + console.log('Updating existing Mario user...'); + } + + await userRepo.save(marioUser); + console.log(`✓ Mario user ready (marioschmidt) ID: ${marioUser.id}`); + + // 2. Assign COLLABORATOR role + const existingRole = await userRoleRepo.findOne({ + where: { userId: marioUser.id, role: Role.COLLABORATOR } + }); + + if (!existingRole) { + const collaboratorRole = userRoleRepo.create({ + userId: marioUser.id, + role: Role.COLLABORATOR + }); + await userRoleRepo.save(collaboratorRole); + console.log('✓ COLLABORATOR role assigned to Mario'); + } + + // 3. Create Mario Collaborator Profile + const marioCollaborator = collaboratorRepo.create({ + id: MARIO_COLLABORATOR_ID, + firstName: 'Mario', + lastName: 'Schmidt', + email: 'mario.schmidt@m2h.ai', + position: 'Consultant Prompt Testing System', + department: 'Quality Assurance & Innovation', + bio: 'From struggling student to software innovator. Met Bryan Wiedmer in a Fortnite match—he taught me that every human has value. Self-taught web developer now building AI-powered testing systems at Ark.Alliance to help recognize everyone\'s contribution to collective knowledge.', + avatarUrl: '/Assets/Site/Icon.png', + userId: marioUser.id, // Link to user for resume data + organizationId: DEFAULT_ORGANIZATION_ID, + reportsToId: FOUNDER_COLLABORATOR_ID, // Reports to Armand + isActive: true, + displayOrder: 3, // After Bryan (1) and Armand (2) + hireDate: new Date('2025-10-01') + }); + + await collaboratorRepo.save(marioCollaborator); + console.log('✓ Mario collaborator profile created (displayOrder: 3, reports to Armand)'); +} + +/** + * Seeds Mario resume data (experiences, skills, etc.). + * + * @param dataSource - TypeORM DataSource connection + */ +export async function seedMarioResumeData(dataSource: DataSource): Promise { + const userRepo = dataSource.getRepository(User); + const experienceRepo = dataSource.getRepository(Experience); + const skillRepo = dataSource.getRepository(Skill); + const educationRepo = dataSource.getRepository(Education); + const languageRepo = dataSource.getRepository(Language); + const hobbyRepo = dataSource.getRepository(Hobby); + const businessDomainRepo = dataSource.getRepository(BusinessDomain); + + // Get Mario user by username + const marioUser = await userRepo.findOne({ where: { username: 'marioschmidt' } }); + if (!marioUser) { + console.log('○ Mario user not found, skipping resume data'); + return; + } + + // Check if resume data already exists + const existingExp = await experienceRepo.findOne({ where: { userId: marioUser.id } }); + if (existingExp) { + console.log('○ Mario resume data already exists, skipping'); + return; + } + + // Experiences + const experiences = [ + { + userId: marioUser.id, + collaboratorId: MARIO_COLLABORATOR_ID, + company: 'M2H – Mindful AI to Humanity', + position: 'Consultant Prompt Testing System', + project: 'AI-Driven Test Generation', + startDate: new Date('2025-10-01'), + endDate: null, + description: 'Unit Testing (TypeScript): Validating business classes through mock injection. Testing Innovation: Automatic generation of test scenarios and mock data via AI to cover edge cases. Coverage: Improving test coverage through rapid simulation of varied data.', + technologies: ['TypeScript', 'ChatGPT', 'Prompt Engineering', 'Jest', 'Unit Testing', 'Mocks', 'AI Testing'], + isHighlighted: true, + displayOrder: 1 + }, + { + userId: marioUser.id, + collaboratorId: MARIO_COLLABORATOR_ID, + company: 'Coop', + position: 'Retail Apprentice', + project: 'Retail Operations', + startDate: new Date('2023-08-01'), + endDate: new Date('2025-07-31'), + description: 'Sales & Customer Service: Welcoming and assisting customers, building loyalty, and handling complaints. Inventory Management: Receiving merchandise, quality control, merchandising, and stock management (inventories). Checkout: Autonomous cash register management and closing procedures. Teamwork: Daily collaboration to maintain store standards and achieve objectives.', + technologies: ['Customer Service', 'Merchandising', 'Inventory Management', 'Point of Sale'], + isHighlighted: false, + displayOrder: 2 + } + ]; + + for (const exp of experiences) { + await experienceRepo.save(experienceRepo.create(exp)); + } + console.log(`✓ ${experiences.length} experiences seeded for Mario`); + + // Skills + const skills = [ + { userId: marioUser.id, collaboratorId: MARIO_COLLABORATOR_ID, name: 'ChatGPT', level: 'Expert', yearsOfExperience: 1 }, + { userId: marioUser.id, collaboratorId: MARIO_COLLABORATOR_ID, name: 'Prompt Engineering', level: 'Expert', yearsOfExperience: 1 }, + { userId: marioUser.id, collaboratorId: MARIO_COLLABORATOR_ID, name: 'CSS', level: 'Intermediate', yearsOfExperience: 2 }, + { userId: marioUser.id, collaboratorId: MARIO_COLLABORATOR_ID, name: 'HTML', level: 'Intermediate', yearsOfExperience: 2 }, + { userId: marioUser.id, collaboratorId: MARIO_COLLABORATOR_ID, name: 'XML', level: 'Beginner', yearsOfExperience: 1 }, + { userId: marioUser.id, collaboratorId: MARIO_COLLABORATOR_ID, name: 'JWT', level: 'Intermediate', yearsOfExperience: 1 }, + { userId: marioUser.id, collaboratorId: MARIO_COLLABORATOR_ID, name: 'TypeScript', level: 'Advanced', yearsOfExperience: 1 }, + { userId: marioUser.id, collaboratorId: MARIO_COLLABORATOR_ID, name: 'Unit Testing', level: 'Advanced', yearsOfExperience: 1 }, + { userId: marioUser.id, collaboratorId: MARIO_COLLABORATOR_ID, name: 'Customer Service', level: 'Expert', yearsOfExperience: 2 }, + { userId: marioUser.id, collaboratorId: MARIO_COLLABORATOR_ID, name: 'Adaptability', level: 'Expert', yearsOfExperience: 2 }, + { userId: marioUser.id, collaboratorId: MARIO_COLLABORATOR_ID, name: 'Stress Management', level: 'Advanced', yearsOfExperience: 2 }, + { userId: marioUser.id, collaboratorId: MARIO_COLLABORATOR_ID, name: 'Teamwork', level: 'Expert', yearsOfExperience: 2 } + ]; + + for (const skill of skills) { + await skillRepo.save(skillRepo.create(skill)); + } + console.log(`✓ ${skills.length} skills seeded for Mario`); + + // Education + const education = educationRepo.create({ + userId: marioUser.id, + institution: 'Ecole professionnelle commerciale de Lausanne (EPCL)', + degree: 'Retail Assistant Certificate (AFP)', + fieldOfStudy: 'Retail Sales and Operations', + startDate: new Date('2023-08-01'), + endDate: new Date('2025-07-31'), + description: 'Professional training in retail commerce, successfully completed' + }); + await educationRepo.save(education); + console.log('✓ Education record seeded for Mario'); + + // Languages + const languages = [ + { userId: marioUser.id, language: 'French', speaking: 5, writing: 5, presenting: 5 }, + { userId: marioUser.id, language: 'Spanish', speaking: 4, writing: 3, presenting: 4 }, + { userId: marioUser.id, language: 'English', speaking: 3, writing: 3, presenting: 3 } + ]; + + for (const lang of languages) { + await languageRepo.save(languageRepo.create(lang)); + } + console.log(`✓ ${languages.length} languages seeded for Mario`); + + // Hobbies + const hobbies = [ + { userId: marioUser.id, name: 'AI & Innovation', description: 'Passionate about artificial intelligence and innovative testing methodologies', icon: 'Bot', displayOrder: 1 }, + { userId: marioUser.id, name: 'Quality Assurance', description: 'Continuous improvement of software quality through advanced testing', icon: 'ShieldCheck', displayOrder: 2 }, + { userId: marioUser.id, name: 'Technology Learning', description: 'Always learning new technologies and best practices', icon: 'GraduationCap', displayOrder: 3 } + ]; + + for (const hobby of hobbies) { + await hobbyRepo.save(hobbyRepo.create(hobby)); + } + console.log(`✓ ${hobbies.length} hobbies seeded for Mario`); + + // Business Domains + const domains = [ + { userId: marioUser.id, domain: 'Quality Assurance & Testing', level: 'Advanced', yearsOfExperience: 1, description: 'Expert in TypeScript unit testing, mock injection, and AI-driven test generation' }, + { userId: marioUser.id, domain: 'AI & Prompt Engineering', level: 'Expert', yearsOfExperience: 1, description: 'Specialized in prompt engineering for automated test scenario generation' }, + { userId: marioUser.id, domain: 'Retail & Customer Service', level: 'Advanced', yearsOfExperience: 2, description: 'Experience in retail operations, customer service, and inventory management' } + ]; + + for (const domain of domains) { + await businessDomainRepo.save(businessDomainRepo.create(domain)); + } + console.log(`✓ ${domains.length} business domains seeded for Mario`); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/media.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/media.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..5c59f8e241fa36b557ecb425c32ff0cd489a37e8 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/media.seeder.ts @@ -0,0 +1,75 @@ +/** + * @fileoverview Media Seeder + * Seeds media assets for the portfolio (images, documents, etc.). + * + * @module database/seeds/seeders/media.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { Media, MediaType } from '../../entities/media.entity'; +import { DEFAULT_MEDIA_SEED } from '@arkalliance/startupcms-ai-share'; + +/** + * Seeds the Media table with default asset references. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when media assets are seeded + * + * @remarks + * - Uses DEFAULT_MEDIA_SEED from the shared layer for type-safe seed data + * - Media entries reference files in /Assets folder + * - Each entry includes: name, key, url, type, mimeType, fileSize, tags + * - Media can be managed via the admin interface after seeding + * + * @example Media entry: + * ```typescript + * { + * name: 'Project Hero Image', + * key: 'project-portfolio-hero', + * url: '/Assets/Projects/Ark.Alliance.StartupCms.Ai/portfolio-hero.png', + * type: 'image', + * mimeType: 'image/png', + * fileSize: 605232, + * tags: ['project', 'hero', 'carousel'] + * } + * ``` + */ +export async function seedMedia(dataSource: DataSource): Promise { + const mediaRepo = dataSource.getRepository(Media); + + if (await mediaRepo.count() === 0) { + for (const item of DEFAULT_MEDIA_SEED) { + const mediaItem = mediaRepo.create({ + name: item.name, + key: item.key, + url: item.url, + type: item.type as unknown as MediaType, + mimeType: item.mimeType, + originalFileName: item.originalFileName, + fileSize: item.fileSize, + altText: item.altText, + description: item.description, + tags: item.tags, + isPublic: item.isPublic !== false + }); + await mediaRepo.save(mediaItem); + } + + console.log(`✓ Media assets seeded (${DEFAULT_MEDIA_SEED.length} items)`); + } else { + console.log('○ Media already populated, skipping'); + } +} + +/** + * Clears all Media records from the database. + * + * @param dataSource - TypeORM DataSource connection + */ +export async function clearMedia(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(Media).execute(); +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/organization.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/organization.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..63f4e86a5451d1dd701cbf036887761c73a84508 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/organization.seeder.ts @@ -0,0 +1,91 @@ +/** + * @fileoverview Organization Seeder + * Seeds the default organization for M2H - Mindful AI for Humanity. + * Part of the Ark.Alliance ecosystem. + * + * @module database/seeds/seeders/organization.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { Organization } from '../../entities/organization.entity'; + +/** + * Default organization ID for M2H. + * Used as a constant to ensure consistent references across seeders. + */ +export const DEFAULT_ORGANIZATION_ID = 'm2h-org-001'; + +/** + * Seeds the Organization table with M2H - Mindful AI for Humanity. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when organization is seeded + * + * @remarks + * M2H (Mindful AI for Humanity) is part of the Ark.Alliance ecosystem, + * focused on building AI-powered solutions that prioritize human values, + * ethical AI practices, and sustainable technology development. + */ +export async function seedOrganization(dataSource: DataSource): Promise { + const orgRepo = dataSource.getRepository(Organization); + + const existingOrg = await orgRepo.findOne({ where: { id: DEFAULT_ORGANIZATION_ID } }); + if (existingOrg) { + console.log('○ Organization already exists, skipping'); + return; + } + + const organization = orgRepo.create({ + id: DEFAULT_ORGANIZATION_ID, + name: 'M2H - Mindful AI for Humanity', + legalName: 'M2H.IO SAS', + tagline: 'Building AI solutions that prioritize humanity', + mission: `M2H exists to bridge the gap between cutting-edge AI technology and human-centered design. +We believe that artificial intelligence should amplify human potential, not replace it. +Our mission is to develop AI-powered solutions that are ethical, transparent, and aligned with human values, +ensuring that the benefits of AI are accessible to all while minimizing potential harms.`, + vision: `A world where AI technology serves as a catalyst for human flourishing—enhancing creativity, +enabling collaboration, and solving complex challenges while respecting individual autonomy and privacy. +We envision AI systems that are partners in human progress, designed with empathy and built for trust.`, + description: `M2H (Mindful AI for Humanity) is a technology company within the Ark.Alliance ecosystem, +specializing in AI-driven enterprise solutions. We combine deep technical expertise in machine learning, +cloud architecture, and full-stack development with a commitment to responsible AI practices. + +Our core competencies include: +- AI/ML Solutions Architecture & MLOps Pipelines +- Multi-provider AI Integration (OpenAI, Anthropic Claude, Google Gemini) +- Enterprise-grade Portfolio & CMS Platforms +- Event-driven Microservices Architecture +- Clean Architecture & Domain-Driven Design + +We are part of the broader Ark.Alliance initiative, which aims to create an ecosystem of +interconnected, ethically-designed software solutions that empower individuals and organizations +to harness technology for positive impact.`, + logoUrl: '/Assets/Site/Icon.png', + iconUrl: '/Assets/Site/Icon.png', + address: 'Remote-First Organization', + city: 'Global', + country: 'International', + timezone: 'UTC', + socialLinks: { + website: 'https://ark-alliance.io', + github: 'https://github.com/M2H-Machine-to-Human-Race', + linkedIn: 'https://linkedin.com/company/m2h-io' + }, + foundedYear: 2024, + isActive: true + }); + + await orgRepo.save(organization); + console.log('✓ Organization seeded (M2H - Mindful AI for Humanity)'); +} + +/** + * Clears all Organization records from the database. + * + * @param dataSource - TypeORM DataSource connection + */ +export async function clearOrganization(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(Organization).execute(); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/profile.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/profile.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba87b0cf7b0990d9c1e5a2d7e5d315e0bb3d7566 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/profile.seeder.ts @@ -0,0 +1,51 @@ +/** + * @fileoverview Profile Seeder + * Seeds the portfolio owner's profile information. + * + * @module database/seeds/seeders/profile.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { Profile } from '../../entities/profile.entity'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Path to the profile seed data JSON file. + */ +const PROFILE_DATA_PATH = path.join(__dirname, '../../InitDbAsset/JsonDatas/profile.json'); + +/** + * Seeds the Profile table with initial data. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when profile is seeded + * + * @remarks + * - Only seeds if the Profile table is empty (prevents duplicate entries) + * - Reads from profile.json in InitDbAsset/JsonDatas + * - Profile contains: firstName, lastName, title, overview, email, social URLs, avatarUrl + */ +export async function seedProfile(dataSource: DataSource): Promise { + const profileRepo = dataSource.getRepository(Profile); + + if (await profileRepo.count() === 0) { + const profileData = JSON.parse(fs.readFileSync(PROFILE_DATA_PATH, 'utf-8')); + const profile = new Profile(); + Object.assign(profile, profileData); + await profileRepo.save(profile); + console.log('✓ Profile seeded'); + } else { + console.log('○ Profile already exists, skipping'); + } +} + +/** + * Clears all Profile records from the database. + * + * @param dataSource - TypeORM DataSource connection + */ +export async function clearProfile(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(Profile).execute(); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/projects.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/projects.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3da1534c117f1519a582dd9136c2abecd91440a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/projects.seeder.ts @@ -0,0 +1,141 @@ +/** + * @fileoverview Projects Seeder + * Seeds portfolio projects with full relational data (pages, features, technologies). + * + * @module database/seeds/seeders/projects.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { Project } from '../../entities/project.entity'; +import { ProjectTechnology } from '../../entities/project-technology.entity'; +import { ProjectPage } from '../../entities/project-page.entity'; +import { ProjectFeature } from '../../entities/project-feature.entity'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Path to the projects seed data JSON file. + */ +const PROJECTS_DATA_PATH = path.join(__dirname, '../../InitDbAsset/ProjectData/projects.json'); + +/** + * Seeds the Project table with portfolio projects and related entities. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when projects are seeded + * + * @remarks + * This seeder handles the complete project hierarchy: + * - **Project**: Main project entity (title, description, status, imageUrl, repoUrl) + * - **ProjectPage**: Multi-page documentation (overview, architecture, technical, etc.) + * - **ProjectFeature**: Feature highlights with icons + * - **ProjectTechnology**: Technology tags (React, TypeScript, etc.) + * + * @example Project JSON structure: + * ```json + * { + * "title": "Project Name", + * "description": "...", + * "status": "Featured", + * "imageUrl": "/Assets/Projects/...", + * "technologies": ["React", "TypeScript"], + * "features": [{ "title": "...", "description": "...", "icon": "..." }], + * "pages": [{ "title": "Overview", "type": "OVERVIEW", "content": "..." }] + * } + * ``` + */ +export async function seedProjects(dataSource: DataSource): Promise { + const projectRepo = dataSource.getRepository(Project); + const techRepo = dataSource.getRepository(ProjectTechnology); + const pageRepo = dataSource.getRepository(ProjectPage); + const featureRepo = dataSource.getRepository(ProjectFeature); + + // Load individual project JSON files for better maintainability + const projectFiles = [ + 'ark-alliance.json', + 'ark-alliance-startupcms-ia.json', + 'ark-portfolio.json', + 'react-component-ui.json', + 'trading-providers-lib.json', + 'trends-calculator.json' + ]; + + const projectsData = projectFiles.map(filename => { + const filePath = path.join(__dirname, '../../InitDbAsset/ProjectData', filename); + if (fs.existsSync(filePath)) { + return JSON.parse(fs.readFileSync(filePath, 'utf-8')); + } + return null; + }).filter(p => p !== null); + + let seededCount = 0; + + for (const pData of projectsData) { + let project = await projectRepo.findOneBy({ title: pData.title }); + + if (!project) { + // Create main project + project = projectRepo.create({ + title: pData.title, + description: pData.description, + status: pData.status, + imageUrl: pData.imageUrl, + repoUrl: pData.repoUrl, + demoUrl: pData.demoUrl, + packageUrl: pData.packageUrl + }); + await projectRepo.save(project); + + // Seed Project Pages + if (pData.pages) { + for (const pg of pData.pages) { + const page = pageRepo.create({ + ...pg, + project: project + }); + await pageRepo.save(page); + } + } + + // Seed Project Features + if (pData.features) { + for (const feat of pData.features) { + const feature = featureRepo.create({ + ...feat, + project: project + }); + await featureRepo.save(feature); + } + } + + // Seed Project Technologies + if (pData.technologies) { + for (const tech of pData.technologies) { + const newTech = techRepo.create({ + projectId: project.id, + technology: tech + }); + await techRepo.save(newTech); + } + } + + seededCount++; + } + } + + console.log(`✓ Projects seeded (${seededCount} projects with pages, features, technologies)`); +} + +/** + * Clears all Project-related records from the database. + * Clears in reverse order of dependencies. + * + * @param dataSource - TypeORM DataSource connection + */ +export async function clearProjects(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(ProjectTechnology).execute(); + await dataSource.createQueryBuilder().delete().from(ProjectFeature).execute(); + await dataSource.createQueryBuilder().delete().from(ProjectPage).execute(); + await dataSource.createQueryBuilder().delete().from(Project).execute(); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/prompt-template.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/prompt-template.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..f4e1845f455534ed65502ae1d0abe27c178da806 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/prompt-template.seeder.ts @@ -0,0 +1,176 @@ +/** + * @fileoverview PromptTemplate Seeder + * Seeds initial AI prompt templates for various AI-assisted features. + * Includes profile-generation, resume-enhancement, and task-generation prompts. + * + * @module database/seeds/seeders/prompt-template.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { PromptTemplate } from '../../entities/prompt-template.entity'; + +/** + * Seeds initial prompt templates for AI-assisted features. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when prompts are seeded + */ +export async function seedPromptTemplates(dataSource: DataSource): Promise { + const promptRepo = dataSource.getRepository(PromptTemplate); + + // Check if already seeded + const existingCount = await promptRepo.count(); + if (existingCount > 0) { + console.log('○ Prompt templates already seeded'); + return; + } + + const templates: Partial[] = [ + { + name: 'profile-generation', + description: 'Generates or updates a User/Collaborator profile from textual description, URLs, and personal information.', + systemPrompt: `You are an expert data architect for Ark.Alliance.StartupCms.Ia. +Transform unstructured biographical text into a strictly valid JSON payload conforming to the UserCollaboratorSchema. + +RULES: +1. Output ONLY valid JSON, no markdown code blocks, no commentary +2. Extract firstName, lastName, title, bio, email, URLs from the description +3. Infer skills, experiences, languages from context clues +4. Use professional tone for bio (maximum 500 characters) +5. Map mentioned technologies to appropriate skill categories +6. Required fields: firstName, lastName, position, bio +7. If manager name is provided, include it in reportsToName field +8. Parse experience entries with company, position, dates, description +9. Extract skill proficiency levels: Beginner, Intermediate, Advanced, Expert +10. Include any social URLs (LinkedIn, GitHub, Twitter) if mentioned + +OUTPUT SCHEMA: +{ + "firstName": "string", + "lastName": "string", + "email": "string | null", + "position": "string", + "department": "string | null", + "bio": "string (max 500 chars)", + "linkedinUrl": "string | null", + "githubUrl": "string | null", + "twitterUrl": "string | null", + "reportsToName": "string | null", + "experiences": [ + { + "company": "string", + "position": "string", + "startDate": "YYYY-MM-DD", + "endDate": "YYYY-MM-DD | null", + "description": "string", + "technologies": ["string"] + } + ], + "skills": [ + { + "name": "string", + "level": "Beginner | Intermediate | Advanced | Expert", + "yearsOfExperience": number + } + ], + "languages": [ + { + "language": "string", + "speaking": 1-5, + "writing": 1-5 + } + ] +}`, + clientPromptTemplate: `Generate a complete JSON profile for the following person: + +Name: {firstName} {lastName} +Function/Position: {function} +Reports to (N+1): {managerName} + +Description and Background: +{description} + +Reference URLs (if any): +{urls} + +Generate strictly valid JSON matching the schema. No markdown, no explanations.`, + outputFormat: 'JSON conforming to UserCollaboratorSchema', + isActive: true + }, + { + name: 'resume-enhancement', + description: 'Polishes and enhances existing resume text for better presentation.', + systemPrompt: `You are a professional resume writer and career coach. +Your task is to enhance and polish resume content while maintaining authenticity. + +RULES: +1. Improve clarity and impact of descriptions +2. Use action verbs and quantifiable achievements where possible +3. Maintain professional tone +4. Keep original facts accurate, only enhance presentation +5. Output the enhanced text directly, no JSON required +6. Preserve technical terms and company names exactly`, + clientPromptTemplate: `Enhance the following resume section: + +Section Type: {sectionType} +Original Content: +{content} + +Provide the enhanced version, keeping the same structure but improving language and impact.`, + outputFormat: 'Plain text with enhanced content', + isActive: true + }, + { + name: 'task-generation', + description: 'Generates task/accomplishment entries from a description of achievements.', + systemPrompt: `You are an accomplishment recognition specialist. +Transform achievement descriptions into structured task/accomplishment records. + +RULES: +1. Output valid JSON array of tasks +2. Assign appropriate recognition levels: 'minor', 'standard', 'notable', 'exceptional' +3. Keep titles concise (max 100 chars) +4. Descriptions should be achievement-focused (max 500 chars) +5. Include relevant impact and business value assessments + +OUTPUT SCHEMA: +[ + { + "title": "string", + "description": "string", + "recognitionLevel": "minor | standard | notable | exceptional", + "impactSummary": "string", + "completedAt": "YYYY-MM-DD" + } +]`, + clientPromptTemplate: `Convert the following achievement description into structured task records: + +Collaborator: {collaboratorName} +Period: {period} + +Achievements/Accomplishments: +{description} + +Generate a JSON array of task records matching the schema.`, + outputFormat: 'JSON array of Task objects', + isActive: true + } + ]; + + for (const template of templates) { + await promptRepo.save(promptRepo.create(template)); + } + + console.log(`✓ ${templates.length} prompt templates seeded`); +} + +/** + * Clears all prompt templates from the database. + * + * @param dataSource - TypeORM DataSource connection + */ +export async function clearPromptTemplates(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(PromptTemplate).execute(); + console.log('✓ Prompt templates cleared'); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/radu-director.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/radu-director.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..100980bc84a578a902f4a1c053526f852e61c700 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/radu-director.seeder.ts @@ -0,0 +1,324 @@ +/** + * @fileoverview Radu Dinulescu (Public Relations Director) User & Resume Seeder + * Seeds the Public Relations Director with complete profile and extensive resume data. + * Renowned theater/film/TV director with 40+ years of experience. + * + * @module database/seeds/seeders/radu-director.seeder + * @author System - Based on LinkedIn and Wikipedia sources + */ + +import { DataSource } from 'typeorm'; +import { User } from '../../entities/user.entity'; +import { Collaborator } from '../../entities/collaborator.entity'; +import { UserRole } from '../../entities/user-role.entity'; +import { Experience } from '../../entities/experience.entity'; +import { Education } from '../../entities/education.entity'; +import { Skill } from '../../entities/skill.entity'; +import { Language } from '../../entities/language.entity'; +import { Hobby } from '../../entities/hobby.entity'; +import { BusinessDomain } from '../../entities/business-domain.entity'; +import { Role } from '@arkalliance/startupcms-ai-share'; +import { DEFAULT_ORGANIZATION_ID } from './organization.seeder'; +import { CEO_COLLABORATOR_ID } from './bryan-ceo.seeder'; +import bcrypt from 'bcryptjs'; + +/** + * Radu Collaborator ID. + */ +export const RADU_COLLABORATOR_ID = 'collab-radu-dinulescu-001'; + +/** + * Seeds Radu Dinulescu as Public Relations Director. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when director is seeded + */ +export async function seedRaduDirector(dataSource: DataSource): Promise { + const userRepo = dataSource.getRepository(User); + const collaboratorRepo = dataSource.getRepository(Collaborator); + const userRoleRepo = dataSource.getRepository(UserRole); + + // 1. Create or Update Radu User + const hashedPassword = await bcrypt.hash('ArkAlliance2026!Secure', 10); + + let raduUser = await userRepo.findOne({ where: { username: 'radudinulescu' } }); + + if (!raduUser) { + // Create new user + raduUser = userRepo.create({ + username: 'radudinulescu', + email: 'radu.dinulescu@m2h.ai', + passwordHash: hashedPassword, + firstName: 'Radu', + lastName: 'Dinulescu', + title: 'Public Relations Director', + bio: 'Renowned theater, film, and TV director with over 40 years of experience. Pioneer of 3D stereoscopic theater with marionettes. International festival director and artistic consultant. Created groundbreaking multimedia productions across Romania, France, Israel, and USA. Award-winning director at Avignon Festival.', + emailConfirmed: true, + isActive: true, + collaboratorId: RADU_COLLABORATOR_ID + }); + console.log('Creating new Radu user...'); + } else { + // Update existing user + raduUser.email = 'radu.dinulescu@m2h.ai'; + raduUser.passwordHash = hashedPassword; + raduUser.firstName = 'Radu'; + raduUser.lastName = 'Dinulescu'; + raduUser.title = 'Public Relations Director'; + raduUser.bio = 'Renowned theater, film, and TV director with over 40 years of experience. Pioneer of 3D stereoscopic theater with marionettes. International festival director and artistic consultant. Created groundbreaking multimedia productions across Romania, France, Israel, and USA. Award-winning director at Avignon Festival.'; + raduUser.emailConfirmed = true; + raduUser.isActive = true; + raduUser.collaboratorId = RADU_COLLABORATOR_ID; + console.log('Updating existing Radu user...'); + } + + await userRepo.save(raduUser); + console.log(`✓ Radu user ready (radudinulescu) ID: ${raduUser.id}`); + + // 2. Assign COLLABORATOR role + const existingRole = await userRoleRepo.findOne({ + where: { userId: raduUser.id, role: Role.COLLABORATOR } + }); + + if (!existingRole) { + const collaboratorRole = userRoleRepo.create({ + userId: raduUser.id, + role: Role.COLLABORATOR + }); + await userRoleRepo.save(collaboratorRole); + console.log('✓ COLLABORATOR role assigned to Radu'); + } + + // 3. Create Radu Collaborator Profile + const raduCollaborator = collaboratorRepo.create({ + id: RADU_COLLABORATOR_ID, + firstName: 'Radu', + lastName: 'Dinulescu', + email: 'radu.dinulescu@m2h.ai', + position: 'Directeur Relation Publique', + department: 'Public Relations & Arts', + bio: 'Visionary theater director with 40+ years creating innovative multimedia productions. First in the world to create 3D stereoscopic theater with marionettes. Internationally acclaimed director with productions at Avignon Festival, Kennedy Center Washington, and theaters across Europe. Expert in marionette theater, multimedia arts, and cultural project management.', + avatarUrl: '/Assets/Site/Icon.png', + userId: raduUser.id, // Link to user for resume data + organizationId: DEFAULT_ORGANIZATION_ID, + reportsToId: CEO_COLLABORATOR_ID, // Reports to Bryan (same level as Armand) + isActive: true, + displayOrder: 2, // Same level as Armand + hireDate: new Date('2025-06-01') + }); + + await collaboratorRepo.save(raduCollaborator); + console.log('✓ Radu collaborator profile created (displayOrder: 2, reports to Bryan)'); +} + +/** + * Seeds Radu resume data (extensive theater/film career). + * + * @param dataSource - TypeORM DataSource connection + */ +export async function seedRaduResumeData(dataSource: DataSource): Promise { + const userRepo = dataSource.getRepository(User); + const experienceRepo = dataSource.getRepository(Experience); + const skillRepo = dataSource.getRepository(Skill); + const educationRepo = dataSource.getRepository(Education); + const languageRepo = dataSource.getRepository(Language); + const hobbyRepo = dataSource.getRepository(Hobby); + const businessDomainRepo = dataSource.getRepository(BusinessDomain); + + // Get Radu user by username + const raduUser = await userRepo.findOne({ where: { username: 'radudinulescu' } }); + if (!raduUser) { + console.log('○ Radu user not found, skipping resume data'); + return; + } + + // Check if resume data already exists + const existingExp = await experienceRepo.findOne({ where: { userId: raduUser.id } }); + if (existingExp) { + console.log('○ Radu resume data already exists, skipping'); + return; + } + + // Experiences (Theater directing career) + const experiences = [ + { + userId: raduUser.id, + company: 'M2H – Mindful AI to Humanity', + position: 'Directeur Relation Publique', + project: 'Public Relations & Site Promotion', + startDate: new Date('2025-06-01'), + endDate: null, + description: 'Managing public relations and promotion for M2H site. Leveraging extensive arts and cultural background to enhance organizational visibility and stakeholder engagement. Applying theater and multimedia expertise to creative communications strategies.', + technologies: ['Digital Art', 'Financial Analysis', 'Business Analysis', 'Public Relations', 'Communications'], + isHighlighted: true, + displayOrder: 1 + }, + { + userId: raduUser.id, + company: 'Teatrul de Marionete Arad', + position: 'Manager', + project: 'Theater Management', + startDate: new Date('2026-01-01'), + endDate: null, + description: 'Managing Arad Marionette Theater operations and artistic direction.', + technologies: ['Theater Management', 'Arts Administration', 'Cultural Programming'], + isHighlighted: true, + displayOrder: 2 + }, + { + userId: raduUser.id, + company: 'Teatrul Al. Davila', + position: 'Regizor (Director)', + project: 'Theatrical Productions', + startDate: new Date('2020-09-01'), + endDate: new Date('2026-01-31'), + description: 'Directed major theatrical productions at Alexandru Davila Theater in Pitești. Created innovative staging and direction for contemporary and classical works.', + technologies: ['Theater Direction', 'Stage Design', 'Artistic Direction'], + isHighlighted: true, + displayOrder: 3 + }, + { + userId: raduUser.id, + company: 'Multiple Theaters & Festivals', + position: 'Director / Artistic Consultant', + project: '3D Stereoscopic Theater Innovation', + startDate: new Date('2008-01-01'), + endDate: new Date('2020-08-31'), + description: 'Created world\'s first 3D stereoscopic theater production with marionettes ("Le Compagnon de Route" / "Tovarășul de drum"). Toured internationally to Kennedy Center Washington, New York, Istanbul. Directed productions at Avignon Festival (multiple years). Artistic consultant for multiple Romanian theaters.', + technologies: ['3D Technology', 'Marionette Theater', 'Multimedia Arts', 'International Production', 'Festival Direction'], + isHighlighted: true, + displayOrder: 4 + }, + { + userId: raduUser.id, + company: 'Teatrul de Marionete Arad', + position: 'Director / Manager', + project: 'Theater Regeneration & International Festivals', + startDate: new Date('2006-01-01'), + endDate: new Date('2011-12-31'), + description: 'Director of Arad Marionette Theater. Organized Festival International Underground and Festival EuroMarionete. Led theater regeneration bringing renowned international directors. Organized international tours to France, USA, New Caledonia, Spain, Czech Republic, Hungary, Turkey, Slovenia.', + technologies: ['Theater Management', 'Festival Organization', 'International Programming', 'Arts Administration'], + isHighlighted: true, + displayOrder: 5 + }, + { + userId: raduUser.id, + company: 'Teatrul de Stat Arad', + position: 'Regizor Artistic Principal (Principal Artistic Director)', + project: 'Main Stage Productions', + startDate: new Date('1990-01-01'), + endDate: new Date('1992-12-31'), + description: 'Principal artistic director at Arad State Theater following return from Israel. Directed major productions including "Candide" by Voltaire and "Max et Moritz" by Wilhelm Busch.', + technologies: ['Theater Direction', 'Artistic Leadership', 'Classical Theater'], + isHighlighted: false, + displayOrder: 6 + }, + { + userId: raduUser.id, + company: 'Scapin Théâtre Tel-Aviv', + position: 'Founder / Director / Professor', + project: 'Theater Company & Education', + startDate: new Date('1985-01-01'), + endDate: new Date('1990-12-31'), + description: 'Founded own theater company in Tel-Aviv. Professor at Acting Studio and Beit Tzvi (leading theater schools). Directed "Don Quichotte" and "Max et Moritz". Collaborated with Modern Dance Company of Kibbutzim as scenographer. Learned marionette craft from Itsak Peker.', + technologies: ['Theater Education', 'Company Management', 'Marionette Craft', 'Scenography', 'Hebrew Language'], + isHighlighted: true, + displayOrder: 7 + }, + { + userId: raduUser.id, + company: 'Marseille Theater Scene', + position: 'Director / Plastician', + project: 'French Productions & Carnival Arts', + startDate: new Date('1992-01-01'), + endDate: new Date('2000-12-31'), + description: 'Based in Marseille. Created giant characters and allegorical floats for carnivals (Martigues, La Seyne sur Mer, Toulon, Aubagne). Directed productions at Pannama Théâtre. Collaborated with Marseille Opera for "Salomé" costumes. International festival participation.', + technologies: ['Carnival Arts', 'Giant Puppets', 'Opera Costume Design', 'French Theater'], + isHighlighted: false, + displayOrder: 8 + } + ]; + + for (const exp of experiences) { + await experienceRepo.save(experienceRepo.create(exp)); + } + console.log(`✓ ${experiences.length} experiences seeded for Radu`); + + // Skills + const skills = [ + { userId: raduUser.id, name: 'Theater Direction', level: 'Expert', yearsOfExperience: 44 }, + { userId: raduUser.id, name: 'Marionette Theater', level: 'Expert', yearsOfExperience: 39 }, + { userId: raduUser.id, name: '3D Multimedia Production', level: 'Expert', yearsOfExperience: 16 }, + { userId: raduUser.id, name: 'Festival Direction', level: 'Expert', yearsOfExperience: 20 }, + { userId: raduUser.id, name: 'Artistic Direction', level: 'Expert', yearsOfExperience: 40 }, + { userId: raduUser.id, name: 'Scenography', level: 'Expert', yearsOfExperience: 40 }, + { userId: raduUser.id, name: 'Stage Design', level: 'Expert', yearsOfExperience: 40 }, + { userId: raduUser.id, name: 'Digital Art', level: 'Advanced', yearsOfExperience: 20 }, + { userId: raduUser.id, name: 'Cultural Project Management', level: 'Expert', yearsOfExperience: 30 }, + { userId: raduUser.id, name: 'International Production', level: 'Expert', yearsOfExperience: 35 }, + { userId: raduUser.id, name: 'Financial Analysis', level: 'Advanced', yearsOfExperience: 15 }, + { userId: raduUser.id, name: 'Business Analysis', level: 'Advanced', yearsOfExperience: 15 }, + { userId: raduUser.id, name: 'Theater Education', level: 'Expert', yearsOfExperience: 25 }, + { userId: raduUser.id, name: 'Screenplay Writing', level: 'Advanced', yearsOfExperience: 30 }, + { userId: raduUser.id, name: 'Translation', level: 'Advanced', yearsOfExperience: 25 } + ]; + + for (const skill of skills) { + await skillRepo.save(skillRepo.create(skill)); + } + console.log(`✓ ${skills.length} skills seeded for Radu`); + + // Education + const education = educationRepo.create({ + userId: raduUser.id, + institution: 'IATC I. L. Caragiale - București', + degree: 'Higher National Diploma', + fieldOfStudy: 'Theater - Film - TV', + startDate: new Date('1976-09-01'), + endDate: new Date('1980-06-30'), + description: 'Studied at class of Valeriu Moisescu and Cătălina Buzoianu. Diploma production: "Acești îngeri triști" by D.R. Popescu at Studio Casandra București. Activities: Project manager, director, artistic consultant, festival director. Artistic consultant at Baia Mare Municipal Theater, Artistic director of Atelier International Festival and Myths of the City Festival.' + }); + await educationRepo.save(education); + console.log('✓ Education record seeded for Radu'); + + // Languages + const languages = [ + { userId: raduUser.id, language: 'Romanian', speaking: 5, writing: 5, presenting: 5 }, + { userId: raduUser.id, language: 'French', speaking: 5, writing: 5, presenting: 5 }, + { userId: raduUser.id, language: 'Hebrew', speaking: 5, writing: 4, presenting: 5 }, + { userId: raduUser.id, language: 'English', speaking: 4, writing: 4, presenting: 4 } + ]; + + for (const lang of languages) { + await languageRepo.save(languageRepo.create(lang)); + } + console.log(`✓ ${languages.length} languages seeded for Radu`); + + // Hobbies + const hobbies = [ + { userId: raduUser.id, name: 'Multimedia Innovation', description: 'Pioneering 3D stereoscopic theater technology and multimedia arts integration', icon: 'Clapperboard', displayOrder: 1 }, + { userId: raduUser.id, name: 'Cultural Festivals', description: 'Organizing and directing international theater and arts festivals', icon: 'PartyPopper', displayOrder: 2 }, + { userId: raduUser.id, name: 'Marionette Craft', description: 'Creating and innovating with giant puppets and marionette characters', icon: 'Scissors', displayOrder: 3 }, + { userId: raduUser.id, name: 'Literary Translation', description: 'Translating theatrical works and francophone literature', icon: 'Languages', displayOrder: 4 }, + { userId: raduUser.id, name: 'International Collaboration', description: 'Building cultural bridges through international theater productions', icon: 'Handshake', displayOrder: 5 } + ]; + + for (const hobby of hobbies) { + await hobbyRepo.save(hobbyRepo.create(hobby)); + } + console.log(`✓ ${hobbies.length} hobbies seeded for Radu`); + + // Business Domains + const domains = [ + { userId: raduUser.id, domain: 'Theater & Performing Arts', level: 'Expert', yearsOfExperience: 44, description: 'Master theater director with international acclaim. Created innovative productions across multiple countries and festivals.' }, + { userId: raduUser.id, domain: 'Multimedia Arts & 3D Technology', level: 'Expert', yearsOfExperience: 16, description: 'Pioneer of 3D stereoscopic theater. First in the world to combine marionettes with 3D multimedia imagery.' }, + { userId: raduUser.id, domain: 'Cultural Project Management', level: 'Expert', yearsOfExperience: 30, description: 'Extensive experience managing theaters, festivals, and international cultural projects. European project coordinator.' }, + { userId: raduUser.id, domain: 'Public Relations & Communications', level: 'Advanced', yearsOfExperience: 40, description: 'Promoting cultural initiatives and building international partnerships. Expert in arts marketing and festival promotion.' }, + { userId: raduUser.id, domain: 'Arts Education', level: 'Expert', yearsOfExperience: 25, description: 'Professor of theater at leading institutions in Israel. Mentor to new generation of theater artists.' } + ]; + + for (const domain of domains) { + await businessDomainRepo.save(businessDomainRepo.create(domain)); + } + console.log(`✓ ${domains.length} business domains seeded for Radu`); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/skills.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/skills.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba80557456ed044f675df7386a46d8aa2e005bbf --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/skills.seeder.ts @@ -0,0 +1,161 @@ +/** + * @fileoverview Skills Seeder + * Seeds technical skills categorized by domain with proper category relationships. + * + * @module database/seeds/seeders/skills.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { Skill } from '../../entities/skill.entity'; +import { SkillCategory } from '../../entities/skill-category.entity'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Path to the skills seed data JSON file. + */ +const SKILLS_DATA_PATH = path.join(__dirname, '../../InitDbAsset/JsonDatas/skills.json'); + +/** + * Category metadata for display purposes. + */ +const CATEGORY_METADATA: Record = { + languages: { + icon: 'Code', + color: '#3B82F6', + description: 'Programming languages' + }, + frameworks: { + icon: 'Layers', + color: '#8B5CF6', + description: 'Frameworks and libraries' + }, + databases: { + icon: 'Database', + color: '#10B981', + description: 'Database systems' + }, + tools: { + icon: 'Wrench', + color: '#F59E0B', + description: 'Development tools and platforms' + }, + methodologies: { + icon: 'GitBranch', + color: '#EC4899', + description: 'Development methodologies and practices' + } +}; + +/** + * Formats category key to display name. + * @example 'languages' -> 'Languages', 'methodologies' -> 'Methodologies' + */ +function formatCategoryName(key: string): string { + return key.charAt(0).toUpperCase() + key.slice(1); +} + +/** + * Seeds the Skill table with technical competencies and their categories. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when skills are seeded + * + * @remarks + * - First creates/updates skill categories + * - Then creates skills linked to their proper categories + * - Skills are organized by category (languages, frameworks, databases, etc.) + * - Defaults all skills to 'Expert' level + * - Prevents duplicate skill names + * + * @example JSON structure: + * ```json + * { + * "languages": ["C#", "Python", "TypeScript"], + * "frameworks": [".NET", "React", "Node.js"], + * "databases": ["PostgreSQL", "MongoDB"] + * } + * ``` + */ +export async function seedSkills(dataSource: DataSource): Promise { + const skillRepo = dataSource.getRepository(Skill); + const categoryRepo = dataSource.getRepository(SkillCategory); + const skillsData = JSON.parse(fs.readFileSync(SKILLS_DATA_PATH, 'utf-8')); + + let seededSkillCount = 0; + let seededCategoryCount = 0; + let displayOrder = 0; + + // First pass: Create or find categories + const categoryMap = new Map(); + + for (const categoryKey of Object.keys(skillsData)) { + const categoryName = formatCategoryName(categoryKey); + let category = await categoryRepo.findOneBy({ name: categoryName }); + + if (!category) { + const metadata = CATEGORY_METADATA[categoryKey] || { + icon: 'Circle', + color: '#6B7280', + description: `${categoryName} skills` + }; + + category = categoryRepo.create({ + name: categoryName, + description: metadata.description, + icon: metadata.icon, + color: metadata.color, + displayOrder: displayOrder++ + }); + await categoryRepo.save(category); + seededCategoryCount++; + } + + categoryMap.set(categoryKey, category); + } + + if (seededCategoryCount > 0) { + console.log(`✓ Skill categories seeded (${seededCategoryCount} new categories)`); + } else { + console.log(`○ Skill categories already exist, skipping`); + } + + // Second pass: Create skills with category links + let skillDisplayOrder = 0; + for (const [categoryKey, skills] of Object.entries(skillsData)) { + const category = categoryMap.get(categoryKey); + + for (const skillName of (skills as string[])) { + const existing = await skillRepo.findOneBy({ name: skillName }); + if (!existing) { + const skill = skillRepo.create({ + name: skillName, + level: 'Expert', + categoryId: category?.id, + displayOrder: skillDisplayOrder++ + }); + await skillRepo.save(skill); + seededSkillCount++; + } else if (existing && !existing.categoryId && category) { + // Update existing skill with category if missing + existing.categoryId = category.id; + await skillRepo.save(existing); + } + } + } + + console.log(`✓ Skills seeded (${seededSkillCount} records across ${Object.keys(skillsData).length} categories)`); +} + +/** + * Clears all Skill and SkillCategory records from the database. + * + * @param dataSource - TypeORM DataSource connection + */ +export async function clearSkills(dataSource: DataSource): Promise { + // Clear skills first (foreign key constraint) + await dataSource.createQueryBuilder().delete().from(Skill).execute(); + // Then clear categories + await dataSource.createQueryBuilder().delete().from(SkillCategory).execute(); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/tasks.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/tasks.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..5455cbc79bad0537f352a2c63d8a1191515b86bc --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/tasks.seeder.ts @@ -0,0 +1,364 @@ +/** + * @fileoverview Tasks Seeder + * Seeds accomplishment tasks for the founder collaborator based on real repository achievements. + * + * Philosophy: + * - Tasks represent real accomplishments from the portfolio projects + * - Mix of Achieved tasks (public), Lessons Learned (public with badge), and Ongoing (private) + * - Deadline performance demonstrates efficiency and growth + * + * @module database/seeds/seeders/tasks.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { Task } from '../../entities/task.entity'; +import { Project } from '../../entities/project.entity'; +import { FOUNDER_COLLABORATOR_ID } from './collaborator.seeder'; + +/** + * Task seed data representing real accomplishments. + * Based on actual repository milestones and features. + */ +interface TaskSeedData { + title: string; + description: string; + role: string; + status: 'backlog' | 'ongoing' | 'achieved' | 'mistake'; + projectTitle?: string; // Will be resolved to projectId + estimatedDurationHours?: number; + actualDurationHours?: number; + averageRating?: number; + ratingCount?: number; + lessonLearned?: boolean; + lessonTitle?: string; + lessonDescription?: string; + isHighlighted?: boolean; + displayOrder: number; +} + +/** + * Real accomplishments from portfolio projects. + */ +const TASK_SEED_DATA: TaskSeedData[] = [ + // ═══════════════════════════════════════════════════════════════════════ + // Ark.Alliance.StartupCms.Ai CMS - ACHIEVED TASKS + // ═══════════════════════════════════════════════════════════════════════ + { + title: 'Multi-Provider AI Integration', + description: 'Integrated OpenAI GPT-4, Anthropic Claude 3, and Google Gemini with unified provider abstraction and automatic fallback.', + role: 'Lead Architect', + status: 'achieved', + projectTitle: 'Ark.Alliance.StartupCms.Ai - AI-Powered Portfolio CMS', + estimatedDurationHours: 40, + actualDurationHours: 32, + averageRating: 4.8, + ratingCount: 5, + isHighlighted: true, + displayOrder: 1 + }, + { + title: 'MVVM Architecture Implementation', + description: 'Established strict Model-View-ViewModel pattern across entire frontend with TypeScript type safety.', + role: 'Frontend Architect', + status: 'achieved', + projectTitle: 'Ark.Alliance.StartupCms.Ai - AI-Powered Portfolio CMS', + estimatedDurationHours: 24, + actualDurationHours: 20, + averageRating: 4.5, + ratingCount: 3, + displayOrder: 2 + }, + { + title: 'Static Site Export System', + description: 'Built one-click generation of deployable static websites from CMS content with SEO optimization.', + role: 'Full Stack Developer', + status: 'achieved', + projectTitle: 'Ark.Alliance.StartupCms.Ai - AI-Powered Portfolio CMS', + estimatedDurationHours: 16, + actualDurationHours: 18, + averageRating: 4.2, + ratingCount: 4, + lessonLearned: true, + lessonTitle: 'Asset path handling complexity', + lessonDescription: 'Underestimated complexity of relative/absolute path handling for different deployment contexts. Now use consistent URL resolution strategy.', + displayOrder: 3 + }, + { + title: 'User & Collaborator Data Model Refactoring', + description: 'Separated User (authentication) and Collaborator (professional) entities with optional one-to-one relationship.', + role: 'Database Architect', + status: 'achieved', + projectTitle: 'Ark.Alliance.StartupCms.Ai - AI-Powered Portfolio CMS', + estimatedDurationHours: 8, + actualDurationHours: 6, + averageRating: 4.7, + ratingCount: 2, + displayOrder: 4 + }, + { + title: 'Task Recognition System', + description: 'Implemented public achievements table with deadline performance metrics, peer ratings, and emulation philosophy.', + role: 'Feature Lead', + status: 'achieved', + projectTitle: 'Ark.Alliance.StartupCms.Ai - AI-Powered Portfolio CMS', + estimatedDurationHours: 12, + actualDurationHours: 10, + averageRating: 4.9, + ratingCount: 3, + isHighlighted: true, + displayOrder: 5 + }, + + // ═══════════════════════════════════════════════════════════════════════ + // React Component Library - ACHIEVEMENTS + // ═══════════════════════════════════════════════════════════════════════ + { + title: 'Component Library npm Publication', + description: 'Published ark-alliance-react-ui to npm with 40+ component categories, full TypeScript support, and Storybook documentation.', + role: 'Library Maintainer', + status: 'achieved', + projectTitle: 'Ark.Alliance.React.Component.UI', + estimatedDurationHours: 20, + actualDurationHours: 16, + averageRating: 4.6, + ratingCount: 4, + isHighlighted: true, + displayOrder: 6 + }, + { + title: 'OrgChart Component with D3.js', + description: 'Built interactive organizational chart component with glassmorphism effects, configurable hierarchy depth, and responsive design.', + role: 'Component Developer', + status: 'achieved', + projectTitle: 'Ark.Alliance.React.Component.UI', + estimatedDurationHours: 16, + actualDurationHours: 14, + averageRating: 4.4, + ratingCount: 3, + displayOrder: 7 + }, + { + title: 'Zod 4 Runtime Validation Migration', + description: 'Migrated all component models to Zod 4 schemas for runtime type checking and validation.', + role: 'Developer', + status: 'achieved', + projectTitle: 'Ark.Alliance.React.Component.UI', + estimatedDurationHours: 8, + actualDurationHours: 10, + averageRating: 4.0, + ratingCount: 2, + lessonLearned: true, + lessonTitle: 'Schema breaking changes', + lessonDescription: 'Zod 4 had breaking API changes. Learned to always check migration guides before major version upgrades.', + displayOrder: 8 + }, + + // ═══════════════════════════════════════════════════════════════════════ + // Trading Providers Library - ACHIEVEMENTS + // ═══════════════════════════════════════════════════════════════════════ + { + title: 'Binance Futures Integration', + description: 'Implemented full Binance Futures API support with HMAC-SHA256 authentication and real-time WebSocket streams.', + role: 'Backend Developer', + status: 'achieved', + projectTitle: 'Ark.Alliance.Trading.Providers.Lib', + estimatedDurationHours: 32, + actualDurationHours: 28, + averageRating: 4.7, + ratingCount: 5, + isHighlighted: true, + displayOrder: 9 + }, + { + title: 'Deribit Exchange Integration', + description: 'Added Deribit options/futures support with Ed25519 authentication and comprehensive REST/WebSocket coverage.', + role: 'Backend Developer', + status: 'achieved', + projectTitle: 'Ark.Alliance.Trading.Providers.Lib', + estimatedDurationHours: 24, + actualDurationHours: 20, + averageRating: 4.5, + ratingCount: 3, + displayOrder: 10 + }, + { + title: 'Result Pattern Error Handling', + description: 'Implemented type-safe Result pattern for functional error handling across all API calls.', + role: 'Architect', + status: 'achieved', + projectTitle: 'Ark.Alliance.Trading.Providers.Lib', + estimatedDurationHours: 8, + actualDurationHours: 6, + averageRating: 4.8, + ratingCount: 4, + displayOrder: 11 + }, + + // ═══════════════════════════════════════════════════════════════════════ + // Trading Ecosystem - ACHIEVEMENTS + // ═══════════════════════════════════════════════════════════════════════ + { + title: '3D Market Topography Visualization', + description: 'Built interactive Three.js order book visualization with peaks representing buy/sell walls.', + role: 'Frontend Developer', + status: 'achieved', + projectTitle: 'Ark.Alliance Trading Ecosystem', + estimatedDurationHours: 40, + actualDurationHours: 35, + averageRating: 4.9, + ratingCount: 6, + isHighlighted: true, + displayOrder: 12 + }, + { + title: 'Real-time WebSocket Streaming', + description: 'Implemented sub-millisecond market data streaming with automatic reconnection and rate limiting.', + role: 'Backend Developer', + status: 'achieved', + projectTitle: 'Ark.Alliance Trading Ecosystem', + estimatedDurationHours: 24, + actualDurationHours: 22, + averageRating: 4.6, + ratingCount: 4, + displayOrder: 13 + }, + + // ═══════════════════════════════════════════════════════════════════════ + // Logistics Platform - ACHIEVEMENTS + // ═══════════════════════════════════════════════════════════════════════ + { + title: 'TMS Integration Layer', + description: 'Built seamless integration with Ortec & Axiodis Transport Management Systems for Ahold Delhaize.', + role: 'Integration Architect', + status: 'achieved', + projectTitle: 'Logistics Orchestration Platform', + estimatedDurationHours: 80, + actualDurationHours: 72, + averageRating: 4.7, + ratingCount: 8, + isHighlighted: true, + displayOrder: 14 + }, + { + title: 'Real-time Delivery Tracking', + description: 'Implemented GPS-based delivery tracking with live status updates across European distribution network.', + role: 'Full Stack Developer', + status: 'achieved', + projectTitle: 'Logistics Orchestration Platform', + estimatedDurationHours: 40, + actualDurationHours: 38, + averageRating: 4.5, + ratingCount: 5, + displayOrder: 15 + }, + + // ═══════════════════════════════════════════════════════════════════════ + // ONGOING TASKS (Private - Psychological Safety) + // ═══════════════════════════════════════════════════════════════════════ + { + title: 'AccomplishmentsTable Integration', + description: 'Integrate the AccomplishmentsTable component into CollaboratorProfilePage for public display.', + role: 'Frontend Developer', + status: 'ongoing', + projectTitle: 'Ark.Alliance.StartupCms.Ai - AI-Powered Portfolio CMS', + estimatedDurationHours: 4, + displayOrder: 16 + }, + { + title: 'TaskController API Routes', + description: 'Create REST API endpoints for task CRUD operations with public/internal separation.', + role: 'Backend Developer', + status: 'ongoing', + projectTitle: 'Ark.Alliance.StartupCms.Ai - AI-Powered Portfolio CMS', + estimatedDurationHours: 6, + displayOrder: 17 + }, + + // ═══════════════════════════════════════════════════════════════════════ + // BACKLOG TASKS (Private) + // ═══════════════════════════════════════════════════════════════════════ + { + title: 'Peer Rating UI Component', + description: 'Build interactive star rating component for peer task evaluations.', + role: 'Frontend Developer', + status: 'backlog', + projectTitle: 'Ark.Alliance.StartupCms.Ai - AI-Powered Portfolio CMS', + estimatedDurationHours: 8, + displayOrder: 18 + }, + { + title: 'Task Performance Dashboard', + description: 'Create admin dashboard showing team task statistics and deadline performance metrics.', + role: 'Full Stack Developer', + status: 'backlog', + projectTitle: 'Ark.Alliance.StartupCms.Ai - AI-Powered Portfolio CMS', + estimatedDurationHours: 16, + displayOrder: 19 + } +]; + +/** + * Seeds task data for the founder collaborator. + * Creates real accomplishments based on portfolio projects. + * + * @param dataSource - TypeORM DataSource connection + */ +export async function seedTasks(dataSource: DataSource): Promise { + const taskRepo = dataSource.getRepository(Task); + const projectRepo = dataSource.getRepository(Project); + + // Check if already seeded + if (await taskRepo.count() > 0) { + console.log('○ Tasks already exist, skipping'); + return; + } + + // Get project mapping for linking tasks + const projects = await projectRepo.find(); + const projectMap = new Map(projects.map(p => [p.title, p.id])); + + // Create tasks + let createdCount = 0; + for (const taskData of TASK_SEED_DATA) { + const projectId = taskData.projectTitle + ? projectMap.get(taskData.projectTitle) + : undefined; + + const task = taskRepo.create({ + title: taskData.title, + description: taskData.description, + role: taskData.role, + status: taskData.status, + collaboratorId: FOUNDER_COLLABORATOR_ID, + projectId: projectId, + estimatedDurationHours: taskData.estimatedDurationHours, + actualDurationHours: taskData.actualDurationHours, + averageRating: taskData.averageRating || 0, + ratingCount: taskData.ratingCount || 0, + lessonLearned: taskData.lessonLearned || false, + lessonTitle: taskData.lessonTitle, + lessonDescription: taskData.lessonDescription, + isHighlighted: taskData.isHighlighted || false, + displayOrder: taskData.displayOrder, + closingDate: taskData.status === 'achieved' || taskData.status === 'mistake' + ? new Date() + : undefined + }); + + await taskRepo.save(task); + createdCount++; + } + + console.log(`✓ Seeded ${createdCount} tasks (${TASK_SEED_DATA.filter(t => t.status === 'achieved').length} achieved, ${TASK_SEED_DATA.filter(t => t.lessonLearned).length} lessons learned)`); +} + +/** + * Clears all Task records from the database. + * + * @param dataSource - TypeORM DataSource connection + */ +export async function clearTasks(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(Task).execute(); +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/technology.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/technology.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..5fc4d5848203ad509f199031a2a059dc78919405 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/technology.seeder.ts @@ -0,0 +1,110 @@ +/** + * @fileoverview Technology Seeder + * Seeds the master technology table with comprehensive technology data. + * + * @module database/seeds/seeders/technology.seeder + * @author Armand Richelet-Kleinberg + * + * @description + * This seeder populates the Technology table with 90+ technologies + * organized by category. Technologies include rich metadata for UI + * rendering: icons, colors, descriptions, and website links. + * + * Categories: + * - frontend: React, Angular, Vue, etc. + * - languages: TypeScript, Python, C#, etc. + * - runtimes: Node.js, .NET, Unity, etc. + * - backend: Express, NestJS, FastAPI, etc. + * - databases: PostgreSQL, MongoDB, Redis, etc. + * - cloud: AWS, Azure, GCP, etc. + * - devops: Docker, Kubernetes, Terraform, etc. + * - messaging: RabbitMQ, Kafka, etc. + * - ai: PyTorch, OpenAI, Anthropic, etc. + * - enterprise: SAP, Salesforce, etc. + * - patterns: Microservices, CQRS, DDD, etc. + * - apis: Binance, Stripe, Twilio, etc. + * - testing: Jest, Cypress, Playwright, etc. + * - mobile: React Native, Flutter, Swift, etc. + * - styling: TailwindCSS, Sass, Bootstrap, etc. + */ + +import { DataSource } from 'typeorm'; +import { Technology } from '../../entities/technology.entity'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Path to the technologies seed data JSON file. + */ +const TECHNOLOGIES_DATA_PATH = path.join(__dirname, '../../InitDbAsset/JsonDatas/technologies.json'); + +/** + * Seeds the Technology table with master technology data. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when technologies are seeded + * + * @remarks + * - Checks if technology key already exists before inserting + * - Reads from technologies.json in InitDbAsset/JsonDatas + * - Each technology includes: key, name, label, category, description, icon, color, website + */ +export async function seedTechnologies(dataSource: DataSource): Promise { + const techRepo = dataSource.getRepository(Technology); + + if (!fs.existsSync(TECHNOLOGIES_DATA_PATH)) { + console.log('○ Technologies data file not found, skipping'); + return; + } + + const data = JSON.parse(fs.readFileSync(TECHNOLOGIES_DATA_PATH, 'utf-8')); + const technologies = data.technologies; + + if (!technologies || technologies.length === 0) { + console.log('○ No technologies found in data file'); + return; + } + + let seededCount = 0; + let skippedCount = 0; + + for (const tech of technologies) { + const existing = await techRepo.findOneBy({ key: tech.key }); + + if (!existing) { + const technology = techRepo.create({ + key: tech.key, + name: tech.name, + label: tech.label || tech.name, + category: tech.category, + description: tech.description, + abbreviation: tech.abbreviation, + icon: tech.icon, + color: tech.color, + website: tech.website, + versions: tech.versions, + order: tech.order || 0, + isActive: true + }); + await techRepo.save(technology); + seededCount++; + } else { + skippedCount++; + } + } + + console.log(`✓ Technologies seeded (${seededCount} new, ${skippedCount} existing)`); +} + +/** + * Clears all Technology records from the database. + * + * @param dataSource - TypeORM DataSource connection + * + * @remarks + * ProjectTechnology records referencing technologies will be CASCADE deleted + * if properly configured with foreign keys. + */ +export async function clearTechnologies(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(Technology).execute(); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/theme.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/theme.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..d81767ca81def7af0accfed097a1abdc6bc5caba --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/theme.seeder.ts @@ -0,0 +1,123 @@ +/** + * @fileoverview Theme Seeder + * Seeds the themes table with CSS content for dynamic theme switching. + * + * @module database/seeds/seeders/theme.seeder + * @author Armand Richelet-Kleinberg + * + * @description + * This seeder populates the Theme table with pre-defined themes. + * Each theme includes full CSS content that is dynamically loaded + * and injected by the frontend for runtime theme switching. + * + * Themes: + * - normal: Balanced neon glow (default) + * - neon: Maximum intensity glow effects + * - minimal: Reduced effects, clean appearance + * - glass: Ultra glassmorphism with soft glow + */ + +import { DataSource } from 'typeorm'; +import { Theme } from '../../entities/theme.entity'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Path to the themes manifest JSON file. + */ +const THEMES_MANIFEST_PATH = path.join(__dirname, '../../InitDbAsset/JsonDatas/themes.json'); + +/** + * Path to the themes CSS content directory. + */ +const THEMES_CSS_DIR = path.join(__dirname, '../../InitDbAsset/JsonDatas/themes'); + +/** + * Theme manifest entry interface. + */ +interface ThemeManifestEntry { + name: string; + slug: string; + description: string; + previewColor: string; + icon: string; + isDefault: boolean; + order: number; + cssFile: string; +} + +/** + * Seeds the Theme table with theme data from manifest and CSS files. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when themes are seeded + * + * @remarks + * - Checks if theme slug already exists before inserting + * - Reads manifest from themes.json + * - Loads CSS content from individual theme files + */ +export async function seedThemes(dataSource: DataSource): Promise { + const themeRepo = dataSource.getRepository(Theme); + + if (!fs.existsSync(THEMES_MANIFEST_PATH)) { + console.log('○ Themes manifest not found, skipping'); + return; + } + + const manifest = JSON.parse(fs.readFileSync(THEMES_MANIFEST_PATH, 'utf-8')); + const themes: ThemeManifestEntry[] = manifest.themes; + + if (!themes || themes.length === 0) { + console.log('○ No themes found in manifest'); + return; + } + + let seededCount = 0; + let skippedCount = 0; + + for (const entry of themes) { + const existing = await themeRepo.findOne({ where: { slug: entry.slug } }); + + if (!existing) { + // Load CSS content from individual theme file + const cssFilePath = path.join(THEMES_CSS_DIR, entry.cssFile); + let cssContent = ''; + + if (fs.existsSync(cssFilePath)) { + const cssData = JSON.parse(fs.readFileSync(cssFilePath, 'utf-8')); + cssContent = cssData.cssContent || ''; + } else { + console.warn(` ⚠ CSS file not found for theme '${entry.slug}': ${entry.cssFile}`); + } + + const theme = themeRepo.create({ + name: entry.name, + slug: entry.slug, + description: entry.description, + previewColor: entry.previewColor, + icon: entry.icon, + isDefault: entry.isDefault, + order: entry.order, + cssContent: cssContent, + isActive: true + }); + + await themeRepo.save(theme); + seededCount++; + } else { + skippedCount++; + } + } + + console.log(`✓ Themes seeded (${seededCount} new, ${skippedCount} existing)`); +} + +/** + * Clears all Theme records from the database. + * + * @param dataSource - TypeORM DataSource connection + */ +export async function clearThemes(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(Theme).execute(); +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/user-roles.seeder.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/user-roles.seeder.ts new file mode 100644 index 0000000000000000000000000000000000000000..b55dc2ac545b240aeef6c7f7078adb949a88176a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/database/seeds/seeders/user-roles.seeder.ts @@ -0,0 +1,144 @@ +/** + * @fileoverview User Roles Seeder + * Seeds user roles for the multi-role RBAC system. + * Updates existing admin user to use the new UserRole junction table. + * + * @module database/seeds/seeders/user-roles.seeder + * @author Armand Richelet-Kleinberg + */ + +import { DataSource } from 'typeorm'; +import { User } from '../../entities/user.entity'; +import { UserRole } from '../../entities/user-role.entity'; +import { Collaborator } from '../../entities/collaborator.entity'; +import { Role } from '@arkalliance/startupcms-ai-share'; +import { FOUNDER_COLLABORATOR_ID } from './collaborator.seeder'; +import bcrypt from 'bcryptjs'; + +/** + * Seeds user roles and updates admin users with multi-role support. + * Ensures both Bryan (CEO) and Armand (Solution Architect) have ADMIN access. + * + * @param dataSource - TypeORM DataSource connection + * @returns Promise resolving when user roles are seeded + * + * @remarks + * - Assigns ADMIN + COLLABORATOR roles to Bryan (CEO) + * - Creates Armand user if needed and assigns ADMIN + COLLABORATOR roles + * - Links users to their respective collaborator profiles + */ +export async function seedUserRoles(dataSource: DataSource): Promise { + const userRepo = dataSource.getRepository(User); + const userRoleRepo = dataSource.getRepository(UserRole); + + // === 1. Handle Bryan (CEO) - created by auth service as 'bryanwiedmersoler' === + const bryanUser = await userRepo.findOne({ + where: { username: 'bryanwiedmersoler' }, + relations: ['userRoles'] + }); + + if (bryanUser) { + // Check if Bryan already has roles assigned (done by bryan-ceo.seeder) + const existingBryanAdmin = await userRoleRepo.findOne({ + where: { userId: bryanUser.id, role: Role.ADMIN } + }); + + if (!existingBryanAdmin) { + // Assign ADMIN role to Bryan + const bryanAdminRole = userRoleRepo.create({ + userId: bryanUser.id, + role: Role.ADMIN + }); + await userRoleRepo.save(bryanAdminRole); + + // Assign COLLABORATOR role to Bryan + const bryanCollabRole = userRoleRepo.create({ + userId: bryanUser.id, + role: Role.COLLABORATOR + }); + await userRoleRepo.save(bryanCollabRole); + console.log('✓ ADMIN + COLLABORATOR roles assigned to Bryan (CEO)'); + } else { + console.log('○ Bryan already has roles assigned'); + } + } + + // === 2. Handle Armand (Solution Architect) === + let armandUser = await userRepo.findOne({ + where: { email: 'arkleinberg@gmail.com' }, + relations: ['userRoles'] + }); + + if (!armandUser) { + // Create Armand's user account + console.log('Creating Armand user account...'); + const hashedPassword = await bcrypt.hash(process.env.ADMIN_KEY_CMS || 'ArkAlliance2026!Secure', 10); + + armandUser = userRepo.create({ + username: 'armandkleinberg', + email: 'arkleinberg@gmail.com', + passwordHash: hashedPassword, + firstName: 'Armand', + lastName: 'Richelet-Kleinberg', + title: 'AI Principal Architect & Full-Stack Developer', + emailConfirmed: true, + isActive: true, + collaboratorId: FOUNDER_COLLABORATOR_ID + }); + await userRepo.save(armandUser); + console.log('✓ Armand user account created'); + } + + // Assign ADMIN + COLLABORATOR roles to Armand + const existingArmandAdmin = await userRoleRepo.findOne({ + where: { userId: armandUser.id, role: Role.ADMIN } + }); + + if (!existingArmandAdmin) { + // Assign ADMIN role to Armand + const armandAdminRole = userRoleRepo.create({ + userId: armandUser.id, + role: Role.ADMIN + }); + await userRoleRepo.save(armandAdminRole); + + // Assign COLLABORATOR role to Armand + const armandCollabRole = userRoleRepo.create({ + userId: armandUser.id, + role: Role.COLLABORATOR + }); + await userRoleRepo.save(armandCollabRole); + console.log('✓ ADMIN + COLLABORATOR roles assigned to Armand'); + } else { + console.log('○ Armand already has roles assigned'); + } + + // Link Armand to founder collaborator if not already linked + if (!armandUser.collaboratorId) { + armandUser.collaboratorId = FOUNDER_COLLABORATOR_ID; + await userRepo.save(armandUser); + console.log('✓ Armand user linked to collaborator profile'); + } + + // Ensure bidirectional link: Collaborator.userId → User.id + const collaboratorRepo = dataSource.getRepository(Collaborator); + const armandCollaborator = await collaboratorRepo.findOne({ + where: { id: FOUNDER_COLLABORATOR_ID } + }); + if (armandCollaborator && !armandCollaborator.userId) { + armandCollaborator.userId = armandUser.id; + await collaboratorRepo.save(armandCollaborator); + console.log('✓ Armand collaborator linked to user (enables View Full Resume)'); + } +} + +/** + * Clears all UserRole records from the database. + * + * @param dataSource - TypeORM DataSource connection + */ +export async function clearUserRoles(dataSource: DataSource): Promise { + await dataSource.createQueryBuilder().delete().from(UserRole).execute(); +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/index.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2675c28817936d94dcd4bd827ead786b8e378b11 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/index.ts @@ -0,0 +1,388 @@ +import 'reflect-metadata'; +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import path from 'path'; +import fs from 'fs'; +import http from 'http'; +import https from 'https'; +import dotenv from 'dotenv'; +import readline from 'readline'; +import { initializeDatabase } from './database/context/db-context'; +import routes from './routes'; +import { killPort } from './utils/port-killer'; +import { AppDataSource } from './config/database'; +import { Project } from './database/entities/project.entity'; +import { globalErrorHandler, notFoundHandler } from './middleware/error-handler.middleware'; + +dotenv.config(); + +const app = express(); +// Default to 3085 as requested, or use ENV if provided +const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3085; + +// Protocol settings - HTTP by default, HTTPS optional +const USE_HTTPS = process.env.USE_HTTPS === 'true'; + +app.use(cors()); +app.use(helmet({ crossOriginResourcePolicy: { policy: 'cross-origin' } })); +app.use(express.json()); + +// Serve uploaded files statically +app.use('/uploads', express.static(path.join(process.cwd(), 'uploads'))); + +// Serve project assets (carousel images, etc.) +app.use('/Assets', express.static(path.join(__dirname, 'Assets'))); + +// Health check endpoint for frontend connection status (BEFORE routes) +app.get('/api/health', (req, res) => { + res.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + version: '1.0.0', + protocol: req.secure ? 'https' : 'http', + uptime: process.uptime(), + }); +}); + +// API Routes +app.use('/api', routes); + +// Swagger Documentation +import swaggerUi from 'swagger-ui-express'; +import { swaggerSpec } from './config/swagger.config'; +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + +// Health check endpoint (root) +app.get('/', (req, res) => { + res.send('Ark Portfolio API is running'); +}); + +// 404 handler for undefined routes (AFTER all valid routes) +app.use(notFoundHandler); + +// Global error handler (MUST be last middleware) +app.use(globalErrorHandler); + +/** + * Delay helper for startup retry. + */ +const delay = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); + +/** + * Certificate configuration + */ +const CERT_CONFIG = { + folder: 'Certificate', + keyFile: 'server.key', + certFile: 'server.crt', + validityDays: 365, + renewBeforeDays: 30 // Renew if less than 30 days remaining +}; + +/** + * Check if certificate is expired or about to expire + */ +function isCertificateExpired(certPath: string): boolean { + try { + const certContent = fs.readFileSync(certPath, 'utf8'); + const crypto = require('crypto'); + const x509 = new crypto.X509Certificate(certContent); + + const notAfter = new Date(x509.validTo); + const now = new Date(); + const daysRemaining = Math.floor((notAfter.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); + + console.log(`Certificate expires: ${notAfter.toISOString()}`); + console.log(`Days remaining: ${daysRemaining}`); + + if (daysRemaining <= 0) { + console.log('Certificate has expired!'); + return true; + } + + if (daysRemaining <= CERT_CONFIG.renewBeforeDays) { + console.log(`Certificate expires in ${daysRemaining} days - renewal recommended`); + return true; + } + + return false; + } catch (error) { + console.warn('Could not check certificate expiration:', error); + return true; // Assume expired if we can't check + } +} + +/** + * Get or generate self-signed certificate for HTTPS + * Uses Certificate folder and checks for expiration + */ +function getOrCreateCertificate(): { key: string | Buffer; cert: string | Buffer } { + const certDir = path.join(process.cwd(), CERT_CONFIG.folder); + const keyPath = path.join(certDir, CERT_CONFIG.keyFile); + const certPath = path.join(certDir, CERT_CONFIG.certFile); + + // Create Certificate directory if it doesn't exist + if (!fs.existsSync(certDir)) { + fs.mkdirSync(certDir, { recursive: true }); + console.log(`Created Certificate folder: ${certDir}`); + } + + // Check if certs already exist and are valid + if (fs.existsSync(keyPath) && fs.existsSync(certPath)) { + if (!isCertificateExpired(certPath)) { + console.log('Using existing valid certificate from Certificate folder'); + return { + key: fs.readFileSync(keyPath), + cert: fs.readFileSync(certPath) + }; + } + console.log('Certificate expired or about to expire - generating new certificate'); + } + + // Generate new certificate using node-forge (synchronous) + console.log('Generating new self-signed certificate using node-forge...'); + const forge = require('node-forge'); + const pki = forge.pki; + + // Generate RSA key pair + console.log('Generating RSA 2048-bit key pair...'); + const keys = pki.rsa.generateKeyPair(2048); + + // Create certificate + const cert = pki.createCertificate(); + cert.publicKey = keys.publicKey; + cert.serialNumber = '01'; + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(); + cert.validity.notAfter.setDate(cert.validity.notAfter.getDate() + CERT_CONFIG.validityDays); + + const attrs = [ + { name: 'commonName', value: 'localhost' }, + { name: 'organizationName', value: 'Ark.Alliance.StartupCms.Ia Development' }, + { name: 'countryName', value: 'CA' } + ]; + cert.setSubject(attrs); + cert.setIssuer(attrs); + + // Add extensions + cert.setExtensions([ + { name: 'basicConstraints', cA: false }, + { name: 'keyUsage', digitalSignature: true, keyEncipherment: true }, + { name: 'extKeyUsage', serverAuth: true }, + { + name: 'subjectAltName', + altNames: [ + { type: 2, value: 'localhost' }, + { type: 7, ip: '127.0.0.1' } + ] + } + ]); + + // Self-sign the certificate + cert.sign(keys.privateKey, forge.md.sha256.create()); + + // Convert to PEM format + const privateKeyPem = pki.privateKeyToPem(keys.privateKey); + const certPem = pki.certificateToPem(cert); + + // Save certificates + fs.writeFileSync(keyPath, privateKeyPem); + fs.writeFileSync(certPath, certPem); + + console.log(`Certificate generated and saved to ${CERT_CONFIG.folder}/`); + console.log(`Valid until: ${cert.validity.notAfter.toISOString()}`); + + return { + key: privateKeyPem, + cert: certPem + }; +} + +/** + * Prompt user for protocol selection in interactive dev mode + */ +async function promptProtocolSelection(): Promise { + // Skip prompt if not interactive terminal or CI environment + if (!process.stdin.isTTY || process.env.CI === 'true') { + return USE_HTTPS; + } + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + console.log('\n╔════════════════════════════════════════════════════════════╗'); + console.log('║ Ark.Alliance.StartupCms.Ia Backend - Dev Server ║'); + console.log('╠════════════════════════════════════════════════════════════╣'); + console.log('║ Select protocol: ║'); + console.log('║ [1] HTTP (default, simpler for development) ║'); + console.log('║ [2] HTTPS (auto-signed certificate) ║'); + console.log('╚════════════════════════════════════════════════════════════╝'); + + rl.question('\nEnter choice [1/2] (default: 1): ', (answer) => { + rl.close(); + const useHttps = answer.trim() === '2'; + console.log(`\n→ Selected: ${useHttps ? 'HTTPS' : 'HTTP'}\n`); + resolve(useHttps); + }); + + // Auto-select HTTP after 5 seconds if no input + setTimeout(() => { + console.log('\n→ No input received, defaulting to HTTP\n'); + rl.close(); + resolve(false); + }, 5000); + }); +} + +/** + * Starts the HTTP server with retry logic. + */ +async function startHttpServer(port: number, maxRetries: number = 3): Promise { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + await new Promise((resolve, reject) => { + const server = http.createServer(app).listen(port, () => { + console.log('════════════════════════════════════════════════════════════'); + console.log(` 🚀 HTTP Server running on port ${port}`); + console.log(` 📍 Access at: http://localhost:${port}`); + console.log(` 📚 API Docs: http://localhost:${port}/api-docs`); + console.log('════════════════════════════════════════════════════════════'); + resolve(); + }); + + server.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + server.close(); + reject(err); + } else { + reject(err); + } + }); + }); + return; // Success + } catch (err) { + const error = err as NodeJS.ErrnoException; + if (error.code === 'EADDRINUSE' && attempt < maxRetries) { + console.warn(`Port ${port} still in use. Retrying in 1 second... (${attempt}/${maxRetries})`); + await killPort(port); + await delay(1000); + } else { + throw err; + } + } + } +} + +/** + * Starts the HTTPS server with retry logic. + */ +async function startHttpsServer(port: number, maxRetries: number = 3): Promise { + const sslOptions = getOrCreateCertificate(); + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + await new Promise((resolve, reject) => { + const server = https.createServer(sslOptions, app).listen(port, () => { + console.log('════════════════════════════════════════════════════════════'); + console.log(` 🔒 HTTPS Server running on port ${port}`); + console.log(` 📍 Access at: https://localhost:${port}`); + console.log(` 📚 API Docs: https://localhost:${port}/api-docs`); + console.log('════════════════════════════════════════════════════════════'); + resolve(); + }); + + server.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + server.close(); + reject(err); + } else { + reject(err); + } + }); + }); + return; // Success + } catch (err) { + const error = err as NodeJS.ErrnoException; + if (error.code === 'EADDRINUSE' && attempt < maxRetries) { + console.warn(`Port ${port} still in use. Retrying in 1 second... (${attempt}/${maxRetries})`); + await killPort(port); + await delay(1000); + } else { + throw err; + } + } + } +} + +const startServer = async () => { + console.log('\n╔════════════════════════════════════════════════════════════╗'); + console.log('║ Ark.Alliance.StartupCms.Ia Backend ║'); + console.log('╚════════════════════════════════════════════════════════════╝\n'); + + // Interactive protocol selection for dev mode + const useHttps = process.env.NODE_ENV === 'production' + ? USE_HTTPS // Use env var in production + : await promptProtocolSelection(); // Interactive in dev + + // Ensure port is free before proceeding + console.log(`Ensuring port ${PORT} is free...`); + await killPort(PORT); + + // Initialize database + await initializeDatabase(); + + // Auto-Seed Check + try { + const projectRepo = AppDataSource.getRepository(Project); + const count = await projectRepo.count(); + if (count === 0) { + console.log('Database appears empty. Auto-seeding...'); + const { seedDatabase } = await import('./database/seeds/seed'); + await seedDatabase(AppDataSource); + console.log('Auto-seeding complete.'); + } else { + console.log(`Database already contains ${count} projects. Skipping auto-seed.`); + } + } catch (error) { + console.error('Error during auto-seed check:', error); + } + + // Seed Admin User + try { + const { default: seedAdmin } = await import('./seed-admin'); + await seedAdmin(); + } catch (error) { + console.error('Error seeding admin user:', error); + } + + // Start server based on protocol selection + try { + if (useHttps) { + await startHttpsServer(PORT); + } else { + await startHttpServer(PORT); + } + } catch (error) { + console.error(`Failed to start server on port ${PORT}:`, error); + } +}; + +// Process-level error handlers to prevent crashes +process.on('uncaughtException', (error: Error) => { + console.error(`[${new Date().toISOString()}] UNCAUGHT EXCEPTION:`); + console.error(` Message: ${error.message}`); + console.error(` Stack: ${error.stack}`); + // Don't exit - keep service running +}); + +process.on('unhandledRejection', (reason: unknown, promise: Promise) => { + console.error(`[${new Date().toISOString()}] UNHANDLED REJECTION:`); + console.error(` Reason:`, reason); + // Don't exit - keep service running +}); + +startServer(); diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/mappers/index.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/mappers/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..99cdcb6d541eb733eeaddb23aa930144f7e6de50 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/mappers/index.ts @@ -0,0 +1,8 @@ +/** + * @fileoverview Barrel export for all entity-to-DTO mappers + * @author Armand Richelet-Kleinberg + */ + +export * from './project.mapper'; +export * from './profile.mapper'; + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/mappers/profile.mapper.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/mappers/profile.mapper.ts new file mode 100644 index 0000000000000000000000000000000000000000..539e4ddd6a043f6be91a0403cfe8d08ffcac5e49 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/mappers/profile.mapper.ts @@ -0,0 +1,68 @@ +/** + * @fileoverview Profile Entity to DTO Mapper + * Provides centralized mapping functions to convert Profile entities to DTOs. + * + * @author Armand Richelet-Kleinberg + */ + +import { Profile } from '../database/entities/profile.entity'; +import { AdminProfileDto } from '@arkalliance/startupcms-ai-share'; + +/** + * Maps a Profile entity to AdminProfileDto. + * + * @param profile - The profile entity from the database + * @returns The profile DTO for admin API responses + * + * @remarks + * Used by admin controllers to return profile data for editing. + * All optional fields are included as-is (may be undefined). + * + * @example + * ```typescript + * const profile = await profileRepo.findOne({ where: { id: 1 } }); + * if (profile) { + * const dto = mapProfileToDto(profile); + * res.json(dto); + * } + * ``` + */ +export function mapProfileToDto(profile: Profile): AdminProfileDto { + return { + firstName: profile.firstName, + lastName: profile.lastName, + title: profile.title, + overview: profile.overview, + email: profile.email, + githubUrl: profile.githubUrl, + linkedinUrl: profile.linkedinUrl, + avatarUrl: profile.avatarUrl + }; +} + +/** + * Maps an AdminProfileDto to partial Profile entity for updates. + * + * @param dto - The profile DTO from API request + * @returns Partial Profile entity for database update + * + * @example + * ```typescript + * const updates = mapDtoToProfile(req.body); + * await profileRepo.update(profileId, updates); + * ``` + */ +export function mapDtoToProfile(dto: AdminProfileDto): Partial { + return { + firstName: dto.firstName, + lastName: dto.lastName, + title: dto.title, + overview: dto.overview, + email: dto.email, + githubUrl: dto.githubUrl, + linkedinUrl: dto.linkedinUrl, + avatarUrl: dto.avatarUrl + }; +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/mappers/project.mapper.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/mappers/project.mapper.ts new file mode 100644 index 0000000000000000000000000000000000000000..c70b06b4def2dde565d4624040b076071f4c1f82 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/mappers/project.mapper.ts @@ -0,0 +1,97 @@ +/** + * @fileoverview Project entity to DTO mappers + * Provides centralized mapping functions to convert TypeORM entities to DTOs. + * Eliminates code duplication across service methods. + * + * @author Armand Richelet-Kleinberg + */ + +import { ProjectDto, ProjectFeatureDto, ProjectPageDto, ProjectStatus, TechnologyDto } from '@arkalliance/startupcms-ai-share'; +import { Project } from '../database/entities/project.entity'; +import { ProjectFeature } from '../database/entities/project-feature.entity'; +import { ProjectPage } from '../database/entities/project-page.entity'; + +/** + * Maps a ProjectFeature entity to ProjectFeatureDto. + * + * @param feature - The feature entity from the database + * @returns The feature DTO for API response + */ +export function mapFeatureToDto(feature: ProjectFeature): ProjectFeatureDto { + return { + id: feature.id, + title: feature.title, + description: feature.description, + icon: feature.icon, + imageUrl: feature.imageUrl + }; +} + +/** + * Maps a ProjectPage entity to ProjectPageDto. + * + * @param page - The page entity from the database + * @returns The page DTO for API response + */ +export function mapPageToDto(page: ProjectPage): ProjectPageDto { + return { + id: page.id, + type: page.type as ProjectPageDto['type'], + title: page.title, + content: page.content + }; +} + +/** + * Maps a Project entity to ProjectDto. + * Includes mapping of all nested relations (technologies, features, pages). + * + * @param project - The project entity from the database with loaded relations + * @returns The complete project DTO for API response + * + * @example + * ```typescript + * const project = await projectRepo.findOne({ + * where: { id }, + * relations: { technologies: true, features: true, pages: true } + * }); + * const dto = mapProjectToDto(project); + * ``` + */ +export function mapProjectToDto(project: Project): ProjectDto { + return { + id: project.id, + title: project.title, + description: project.description, + isFeatured: project.isFeatured, + status: project.status as ProjectStatus, + technologies: project.technologies + ? project.technologies.map((t: any) => t.technology as TechnologyDto) + : [], + imageUrl: project.imageUrl, + repoUrl: project.repoUrl, + packageUrl: project.packageUrl, + demoUrl: project.demoUrl, + startDate: project.startDate, + endDate: project.endDate, + features: project.features + ? project.features.map(mapFeatureToDto) + : [], + pages: project.pages + ? project.pages.map(mapPageToDto) + : [] + }; +} + +/** + * Maps an array of Project entities to ProjectDto array. + * + * @param projects - Array of project entities + * @returns Array of project DTOs + */ +export function mapProjectsToDtos(projects: Project[]): ProjectDto[] { + return projects.map(mapProjectToDto); +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/middleware/auth.middleware.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/middleware/auth.middleware.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd505e46eaa47a2d1b38a0a5d270dd0314637dad --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/middleware/auth.middleware.ts @@ -0,0 +1,99 @@ +import { Request, Response, NextFunction } from 'express'; +import { AuthService } from '../services/auth.service'; +import { Role } from '@arkalliance/startupcms-ai-share'; + +const authService = new AuthService(); + +/** + * Extend Express Request to include user data. + */ +export interface AuthenticatedRequest extends Request { + user?: { + id: string; + username: string; + role: string; // Legacy + roles: Role[]; + }; +} + +/** + * Authentication middleware - validates JWT token. + */ +export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ message: 'No token provided' }); + } + + const token = authHeader.split(' ')[1]; + const decoded = await authService.validateToken(token); + + if (!decoded) { + return res.status(401).json({ message: 'Invalid token' }); + } + + // Ensure roles array exists (for backward compatibility) + if (!decoded.roles && decoded.role) { + decoded.roles = [decoded.role]; + } + + (req as any).user = decoded; + next(); +}; + +/** + * Role-based authorization middleware factory. + * User must have at least one of the specified roles. + * + * @param allowedRoles - Roles that are allowed to access the route + * @returns Express middleware function + * + * @example + * router.get('/admin/users', authMiddleware, requireRoles(Role.ADMIN, Role.SUPERVISOR), handler); + */ +export const requireRoles = (...allowedRoles: Role[]) => { + return (req: Request, res: Response, next: NextFunction) => { + const user = (req as AuthenticatedRequest).user; + + if (!user) { + return res.status(401).json({ message: 'Not authenticated' }); + } + + const userRoles = user.roles || [user.role as Role]; + const hasRole = allowedRoles.some(role => userRoles.includes(role)); + + if (!hasRole) { + return res.status(403).json({ + message: 'Insufficient permissions', + required: allowedRoles, + current: userRoles + }); + } + + next(); + }; +}; + +// ============================================ +// Convenience Role Guards +// ============================================ + +/** Only admins can access */ +export const adminOnly = requireRoles(Role.ADMIN); + +/** Admins and supervisors can access */ +export const supervisorOrAdmin = requireRoles(Role.ADMIN, Role.SUPERVISOR); + +/** Admins, supervisors, and collaborators can access */ +export const collaboratorOrAbove = requireRoles(Role.ADMIN, Role.SUPERVISOR, Role.COLLABORATOR); + +/** All authenticated users with resume access (excludes guests) */ +export const resumeAccess = requireRoles(Role.ADMIN, Role.SUPERVISOR, Role.COLLABORATOR, Role.MEMBER); + +/** All authenticated users including guests */ +export const anyAuthenticated = requireRoles(Role.ADMIN, Role.SUPERVISOR, Role.COLLABORATOR, Role.MEMBER, Role.GUEST); + + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/middleware/error-handler.middleware.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/middleware/error-handler.middleware.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffc3d077db68d8fb8404a2c531e8c4ff5938bb67 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/middleware/error-handler.middleware.ts @@ -0,0 +1,90 @@ +/** + * @fileoverview Global Error Handling Middleware + * Provides centralized error handling for all API routes. + * Ensures errors are logged without crashing the service. + * + * @author Armand Richelet-Kleinberg + */ + +import { Request, Response, NextFunction, ErrorRequestHandler } from 'express'; + +/** + * Error response structure. + */ +interface ErrorResponse { + success: false; + message: string; + error?: string; + stack?: string; +} + +/** + * Logs error details to console with timestamp. + * @param error - Error object to log + * @param req - Express request object for context + */ +function logError(error: Error, req: Request): void { + const timestamp = new Date().toISOString(); + console.error(`[${timestamp}] ERROR in ${req.method} ${req.path}:`); + console.error(` Message: ${error.message}`); + console.error(` Stack: ${error.stack}`); +} + +/** + * Global error handling middleware. + * Catches all unhandled errors from route handlers and prevents service crashes. + * Must be registered AFTER all routes. + */ +export const globalErrorHandler: ErrorRequestHandler = ( + error: Error, + req: Request, + res: Response, + _next: NextFunction +): void => { + // Log the error + logError(error, req); + + // Determine if we're in development mode + const isDev = process.env.NODE_ENV !== 'production'; + + // Build error response + const errorResponse: ErrorResponse = { + success: false, + message: 'Internal Server Error', + error: error.message + }; + + // Include stack trace in development + if (isDev) { + errorResponse.stack = error.stack; + } + + // Send error response (don't crash) + res.status(500).json(errorResponse); +}; + +/** + * Middleware to wrap async route handlers and catch errors. + * Use this to wrap controller methods that are async. + * + * @example + * router.get('/profile', asyncHandler(controller.getProfile.bind(controller))); + */ +export function asyncHandler( + fn: (req: Request, res: Response, next: NextFunction) => Promise +) { + return (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +} + +/** + * 404 Not Found handler for undefined routes. + */ +export function notFoundHandler(req: Request, res: Response): void { + res.status(404).json({ + success: false, + message: `Route ${req.method} ${req.path} not found` + }); +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/middleware/upload.middleware.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/middleware/upload.middleware.ts new file mode 100644 index 0000000000000000000000000000000000000000..655da7cd5899ec8f9dd809af0265f3165a1ab625 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/middleware/upload.middleware.ts @@ -0,0 +1,248 @@ +/** + * @fileoverview File Upload Middleware + * Multer configuration for handling multipart file uploads. + * + * @module middleware/upload + * @author Armand Richelet-Kleinberg + * @since 1.0.0 + * + * @description + * This middleware handles file uploads using Multer with disk storage. + * Files are organized into type-specific subdirectories (image, video, audio, etc.) + * and renamed with UUIDs to prevent conflicts. + * + * @example + * // Using in a route + * import { mediaUpload } from '../middleware/upload.middleware'; + * + * router.post('/upload', + * authMiddleware, + * mediaUpload.single('file'), + * uploadHandler + * ); + * + * @example + * // Getting public URL for uploaded file + * import { getPublicUrl, getMediaType } from '../middleware/upload.middleware'; + * + * const url = getPublicUrl(req.file.path); + * const type = getMediaType(req.file.mimetype); + */ + +import multer, { FileFilterCallback } from 'multer'; +import path from 'path'; +import fs from 'fs'; +import { v4 as uuidv4 } from 'uuid'; +import { Request } from 'express'; + +/** + * Base directory for file uploads. + * @constant {string} + * @default './uploads' + * @remarks Can be overridden via UPLOAD_DIR environment variable + */ +const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'; + +// Ensure upload directory exists on startup +if (!fs.existsSync(UPLOAD_DIR)) { + fs.mkdirSync(UPLOAD_DIR, { recursive: true }); +} + +/** + * MIME type to MediaType mapping. + * + * @constant {Record} + * @description Maps standard MIME types to internal media type categories. + * Used for organizing files into subdirectories and setting the MediaType field. + * + * @example + * MIME_TYPE_MAP['image/jpeg'] // returns 'image' + * MIME_TYPE_MAP['application/pdf'] // returns 'pdf' + */ +export const MIME_TYPE_MAP: Record = { + // Images + 'image/jpeg': 'image', + 'image/png': 'image', + 'image/gif': 'image', + 'image/webp': 'image', + 'image/svg+xml': 'image', + 'image/x-icon': 'icon', + // Videos + 'video/mp4': 'video', + 'video/webm': 'video', + 'video/quicktime': 'video', + 'video/x-msvideo': 'video', + // Audio + 'audio/mpeg': 'audio', + 'audio/wav': 'audio', + 'audio/ogg': 'audio', + 'audio/mp3': 'audio', + 'audio/x-m4a': 'audio', + // Documents + 'application/pdf': 'pdf', + 'application/msword': 'word', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'word', + 'application/vnd.ms-excel': 'excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'excel', + 'text/markdown': 'markdown', + 'text/x-markdown': 'markdown', + 'application/json': 'json', + 'text/plain': 'other' +}; + +/** + * Allowed file extensions for upload. + * + * @constant {string[]} + * @description Whitelist of permitted file extensions. + * Files with extensions not in this list will be rejected. + * + * @remarks + * - Case-insensitive comparison is performed + * - Add new extensions here to support additional file types + */ +const ALLOWED_EXTENSIONS: string[] = [ + // Images + '.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', + // Videos + '.mp4', '.webm', '.mov', '.avi', + // Audio + '.mp3', '.wav', '.ogg', '.m4a', '.flac', + // Documents + '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.md', '.json', '.txt' +]; + +/** + * Maximum allowed file size in bytes. + * + * @constant {number} + * @default 52428800 (50MB) + * @remarks Requests exceeding this limit will be rejected with a 413 error + */ +const MAX_FILE_SIZE: number = 50 * 1024 * 1024; + +/** + * Multer disk storage configuration. + * + * @description + * - Destination: Files are organized into type-based subdirectories + * - Filename: Original names are replaced with UUID + original extension + * + * @remarks + * Subdirectories are created automatically if they don't exist. + */ +const storage = multer.diskStorage({ + /** + * Destination callback - determines target directory. + * @param req - Express request object + * @param file - Uploaded file info + * @param cb - Callback function + */ + destination: (req: Request, file: Express.Multer.File, cb: (error: Error | null, destination: string) => void) => { + const mediaType = MIME_TYPE_MAP[file.mimetype] || 'other'; + const typeDir = path.join(UPLOAD_DIR, mediaType); + + if (!fs.existsSync(typeDir)) { + fs.mkdirSync(typeDir, { recursive: true }); + } + + cb(null, typeDir); + }, + /** + * Filename callback - generates unique filename. + * @param req - Express request object + * @param file - Uploaded file info + * @param cb - Callback function + */ + filename: (req: Request, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) => { + const ext = path.extname(file.originalname).toLowerCase(); + const uniqueName = `${uuidv4()}${ext}`; + cb(null, uniqueName); + } +}); + +/** + * File filter function for validating uploads. + * + * @param req - Express request object + * @param file - Uploaded file information + * @param cb - Multer callback function + * + * @description + * Validates file extensions against ALLOWED_EXTENSIONS whitelist. + * Rejects files with disallowed extensions. + */ +const fileFilter = ( + req: Request, + file: Express.Multer.File, + cb: FileFilterCallback +): void => { + const ext = path.extname(file.originalname).toLowerCase(); + + if (ALLOWED_EXTENSIONS.includes(ext)) { + cb(null, true); + } else { + cb(new Error(`File type not allowed: ${ext}. Allowed types: ${ALLOWED_EXTENSIONS.join(', ')}`)); + } +}; + +/** + * Configured Multer instance for media uploads. + * + * @constant + * @description Pre-configured multer instance with disk storage, file filtering, and size limits. + * + * @example + * // Single file upload + * router.post('/upload', mediaUpload.single('file'), handler); + * + * @example + * // Multiple files upload + * router.post('/upload-multiple', mediaUpload.array('files', 10), handler); + */ +export const mediaUpload = multer({ + storage, + fileFilter, + limits: { + fileSize: MAX_FILE_SIZE + } +}); + +/** + * Get media type from MIME type. + * + * @param mimeType - The MIME type string (e.g., 'image/jpeg') + * @returns {string} MediaType string (e.g., 'image', 'pdf', 'video') + * + * @example + * getMediaType('image/png'); // returns 'image' + * getMediaType('application/pdf'); // returns 'pdf' + * getMediaType('unknown/type'); // returns 'other' + */ +export function getMediaType(mimeType: string): string { + return MIME_TYPE_MAP[mimeType] || 'other'; +} + +/** + * Generate public URL for an uploaded file. + * + * @param filePath - Absolute or relative file path from multer + * @returns {string} Public-facing URL for the file + * + * @description + * Converts the local file path to a URL accessible via the static file server. + * Uses API_BASE_URL environment variable for the domain. + * + * @example + * getPublicUrl('./uploads/image/abc123.jpg'); + * // returns 'http://localhost:5085/uploads/image/abc123.jpg' + * + * @remarks + * Ensure API_BASE_URL is set correctly in production environments. + */ +export function getPublicUrl(filePath: string): string { + const relativePath = filePath.replace(UPLOAD_DIR, '').replace(/\\/g, '/'); + const baseUrl = process.env.API_BASE_URL || 'http://localhost:5085'; + return `${baseUrl}/uploads${relativePath}`; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/ai.routes.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/ai.routes.ts new file mode 100644 index 0000000000000000000000000000000000000000..e89c7dc6c52ea8de08cac6add99e1855b83e84ec --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/ai.routes.ts @@ -0,0 +1,96 @@ +/** + * @fileoverview Admin AI Routes + * Defines authenticated admin endpoints for AI settings, profile generation, and prompts. + * + * @author Armand Richelet-Kleinberg + * @module routes/admin/ai + */ + +import { Router } from 'express'; +import { AdminController } from '../../controllers/admin.controller'; +import { aiProfileController } from '../../controllers/ai-profile.controller'; +import { asyncHandler } from '../../middleware/error-handler.middleware'; +import { authMiddleware } from '../../middleware/auth.middleware'; + +const router = Router(); + +const adminController = new AdminController(); + +// ============================================ +// AI SETTINGS +// ============================================ + +router.get('/settings', authMiddleware, asyncHandler(adminController.getAiSettings.bind(adminController))); +router.put('/settings', authMiddleware, asyncHandler(adminController.updateAiSettings.bind(adminController))); + +/** + * @swagger + * /admin/ai/test: + * post: + * summary: Test AI connection + * tags: [Admin - AI] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: AI connection test result + */ +router.post('/test', authMiddleware, asyncHandler(adminController.testAiConnection.bind(adminController))); + +/** + * @swagger + * /admin/ai/models/{provider}: + * get: + * summary: Get available models for AI provider + * tags: [Admin - AI] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: provider + * required: true + * schema: + * type: string + * enum: [openai, anthropic, gemini] + * responses: + * 200: + * description: List of available models + */ +router.get('/models/:provider', authMiddleware, asyncHandler(adminController.getAiProviderModels.bind(adminController))); + +// ============================================ +// AI ACTIONS +// ============================================ + +router.post('/organize-skills', authMiddleware, asyncHandler(adminController.organizeSkillsWithAi.bind(adminController))); +router.post('/improve-text', authMiddleware, asyncHandler(adminController.improveTextWithAi.bind(adminController))); + +// ============================================ +// AI PROFILE GENERATION +// ============================================ + +/** + * @swagger + * /admin/ai/generate-profile: + * post: + * summary: Generate profile using AI from description + * tags: [Admin - AI Profile] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Profile generated successfully + */ +router.post('/generate-profile', authMiddleware, asyncHandler(aiProfileController.generateProfile)); + +// ============================================ +// PROMPT TEMPLATES +// ============================================ + +router.get('/prompts', authMiddleware, asyncHandler(aiProfileController.getPromptTemplates)); +router.post('/prompts', authMiddleware, asyncHandler(aiProfileController.createPromptTemplate)); +router.get('/prompts/:name', authMiddleware, asyncHandler(aiProfileController.getPromptByName)); +router.put('/prompts/:id', authMiddleware, asyncHandler(aiProfileController.updatePromptTemplate)); +router.delete('/prompts/:id', authMiddleware, asyncHandler(aiProfileController.deletePromptTemplate)); + +export default router; diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/index.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..a791af7f63ec1d2dee4670be746251c649bba223 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/index.ts @@ -0,0 +1,23 @@ +/** + * @fileoverview Admin Routes Index + * Composes all admin route modules into a single router. + * + * @author Armand Richelet-Kleinberg + * @module routes/admin + */ + +import { Router } from 'express'; +import projectsRoutes from './projects.routes'; +import resumeRoutes from './resume.routes'; +import aiRoutes from './ai.routes'; +import systemRoutes from './system.routes'; + +const router = Router(); + +// Mount admin sub-routes +router.use('/projects', projectsRoutes); +router.use('/resume', resumeRoutes); +router.use('/ai', aiRoutes); +router.use('/', systemRoutes); // System routes are at admin root level + +export default router; diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/projects.routes.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/projects.routes.ts new file mode 100644 index 0000000000000000000000000000000000000000..6eaabbbd10e3dbabe3b58d3081fa71d9a7a8edf6 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/projects.routes.ts @@ -0,0 +1,107 @@ +/** + * @fileoverview Admin Projects Routes + * Defines authenticated admin endpoints for project management. + * + * @author Armand Richelet-Kleinberg + * @module routes/admin/projects + */ + +import { Router } from 'express'; +import { AdminController } from '../../controllers/admin.controller'; +import { asyncHandler } from '../../middleware/error-handler.middleware'; +import { authMiddleware } from '../../middleware/auth.middleware'; + +const router = Router(); + +const adminController = new AdminController(); + +// ============================================ +// ADMIN - PROJECTS +// ============================================ + +/** + * @swagger + * /admin/projects: + * get: + * summary: Get all projects (admin) + * tags: [Admin - Projects] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: List of all projects + * post: + * summary: Create new project + * tags: [Admin - Projects] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Project' + * responses: + * 201: + * description: Project created + */ +router.get('/', authMiddleware, asyncHandler(adminController.getProjects.bind(adminController))); +router.post('/', authMiddleware, asyncHandler(adminController.createProject.bind(adminController))); + +/** + * @swagger + * /admin/projects/{id}: + * get: + * summary: Get project by ID (admin) + * tags: [Admin - Projects] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Project details + * put: + * summary: Update project + * tags: [Admin - Projects] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Project' + * responses: + * 200: + * description: Project updated + * delete: + * summary: Delete project + * tags: [Admin - Projects] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Project deleted + */ +router.get('/:id', authMiddleware, asyncHandler(adminController.getProject.bind(adminController))); +router.put('/:id', authMiddleware, asyncHandler(adminController.updateProject.bind(adminController))); +router.delete('/:id', authMiddleware, asyncHandler(adminController.deleteProject.bind(adminController))); + +export default router; diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/resume.routes.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/resume.routes.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ca42b00413a09660827106315edb9c94d72c059 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/resume.routes.ts @@ -0,0 +1,120 @@ +/** + * @fileoverview Admin Resume Routes + * Defines authenticated admin endpoints for resume management. + * Includes experience, education, skills, categories, languages, hobbies, business domains. + * + * @author Armand Richelet-Kleinberg + * @module routes/admin/resume + */ + +import { Router } from 'express'; +import { AdminController } from '../../controllers/admin.controller'; +import { asyncHandler } from '../../middleware/error-handler.middleware'; +import { authMiddleware } from '../../middleware/auth.middleware'; + +const router = Router(); + +const adminController = new AdminController(); + +// ============================================ +// ADMIN - RESUME (Core) +// ============================================ + +/** + * @swagger + * /admin/resume: + * get: + * summary: Get admin resume data + * tags: [Admin - Resume] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Complete resume for editing + */ +router.get('/', authMiddleware, asyncHandler(adminController.getResume.bind(adminController))); + +/** + * @swagger + * /admin/resume/profile: + * put: + * summary: Update profile + * tags: [Admin - Resume] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Profile' + * responses: + * 200: + * description: Profile updated + */ +router.put('/profile', authMiddleware, asyncHandler(adminController.updateProfile.bind(adminController))); + +// ============================================ +// EXPERIENCE +// ============================================ + +router.post('/experience', authMiddleware, asyncHandler(adminController.createExperience.bind(adminController))); +router.put('/experience/reorder', authMiddleware, asyncHandler(adminController.reorderExperiences.bind(adminController))); +router.put('/experience/:id', authMiddleware, asyncHandler(adminController.updateExperience.bind(adminController))); +router.delete('/experience/:id', authMiddleware, asyncHandler(adminController.deleteExperience.bind(adminController))); + +// ============================================ +// EDUCATION +// ============================================ + +router.post('/education', authMiddleware, asyncHandler(adminController.createEducation.bind(adminController))); +router.put('/education/reorder', authMiddleware, asyncHandler(adminController.reorderEducation.bind(adminController))); +router.put('/education/:id', authMiddleware, asyncHandler(adminController.updateEducation.bind(adminController))); +router.delete('/education/:id', authMiddleware, asyncHandler(adminController.deleteEducation.bind(adminController))); + +// ============================================ +// SKILLS +// ============================================ + +router.post('/skill', authMiddleware, asyncHandler(adminController.createSkill.bind(adminController))); +router.put('/skill/reorder', authMiddleware, asyncHandler(adminController.reorderSkills.bind(adminController))); +router.put('/skill/:id', authMiddleware, asyncHandler(adminController.updateSkill.bind(adminController))); +router.delete('/skill/:id', authMiddleware, asyncHandler(adminController.deleteSkill.bind(adminController))); + +// ============================================ +// SKILL CATEGORIES +// ============================================ + +router.get('/categories', authMiddleware, asyncHandler(adminController.getSkillCategories.bind(adminController))); +router.post('/category', authMiddleware, asyncHandler(adminController.createSkillCategory.bind(adminController))); +router.put('/category/:id', authMiddleware, asyncHandler(adminController.updateSkillCategory.bind(adminController))); +router.delete('/category/:id', authMiddleware, asyncHandler(adminController.deleteSkillCategory.bind(adminController))); + +// ============================================ +// LANGUAGES +// ============================================ + +router.post('/language', authMiddleware, asyncHandler(adminController.createLanguage.bind(adminController))); +router.put('/language/reorder', authMiddleware, asyncHandler(adminController.reorderLanguages.bind(adminController))); +router.put('/language/:id', authMiddleware, asyncHandler(adminController.updateLanguage.bind(adminController))); +router.delete('/language/:id', authMiddleware, asyncHandler(adminController.deleteLanguage.bind(adminController))); + +// ============================================ +// HOBBIES +// ============================================ + +router.post('/hobby', authMiddleware, asyncHandler(adminController.createHobby.bind(adminController))); +router.put('/hobby/reorder', authMiddleware, asyncHandler(adminController.reorderHobbies.bind(adminController))); +router.put('/hobby/:id', authMiddleware, asyncHandler(adminController.updateHobby.bind(adminController))); +router.delete('/hobby/:id', authMiddleware, asyncHandler(adminController.deleteHobby.bind(adminController))); + +// ============================================ +// BUSINESS DOMAINS +// ============================================ + +router.post('/business-domain', authMiddleware, asyncHandler(adminController.createBusinessDomain.bind(adminController))); +router.put('/business-domain/reorder', authMiddleware, asyncHandler(adminController.reorderBusinessDomains.bind(adminController))); +router.put('/business-domain/:id', authMiddleware, asyncHandler(adminController.updateBusinessDomain.bind(adminController))); +router.delete('/business-domain/:id', authMiddleware, asyncHandler(adminController.deleteBusinessDomain.bind(adminController))); + +export default router; diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/system.routes.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/system.routes.ts new file mode 100644 index 0000000000000000000000000000000000000000..cccd3771490d6f82d2efd810111cc293ce5b87b1 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/admin/system.routes.ts @@ -0,0 +1,153 @@ +/** + * @fileoverview Admin System Routes + * Defines authenticated admin endpoints for system configuration. + * Includes widgets, menu, styles, themes, carousel, media, organizations, collaborators, users, tasks, audit logs. + * + * @author Armand Richelet-Kleinberg + * @module routes/admin/system + */ + +import { Router } from 'express'; +import { AdminController } from '../../controllers/admin.controller'; +import { OrganizationController } from '../../controllers/organization.controller'; +import { CollaboratorController } from '../../controllers/collaborator.controller'; +import { UserManagementController } from '../../controllers/user-management.controller'; +import { UserProfileController } from '../../controllers/user-profile.controller'; +import { AuditLogController } from '../../controllers/audit-log.controller'; +import { TaskController } from '../../controllers/task.controller'; +import { asyncHandler } from '../../middleware/error-handler.middleware'; +import { authMiddleware, supervisorOrAdmin, adminOnly, collaboratorOrAbove } from '../../middleware/auth.middleware'; +import { mediaUpload } from '../../middleware/upload.middleware'; + +const router = Router(); + +const adminController = new AdminController(); +const orgController = new OrganizationController(); +const collabController = new CollaboratorController(); +const userMgmtController = new UserManagementController(); +const userProfileController = new UserProfileController(); +const auditController = new AuditLogController(); +const taskController = new TaskController(); + +// ============================================ +// WIDGETS +// ============================================ + +router.get('/widgets', authMiddleware, asyncHandler(adminController.getWidgets.bind(adminController))); +router.post('/widgets', authMiddleware, asyncHandler(adminController.createWidget.bind(adminController))); +router.put('/widgets/reorder', authMiddleware, asyncHandler(adminController.reorderWidgets.bind(adminController))); +router.put('/widgets/:id', authMiddleware, asyncHandler(adminController.updateWidget.bind(adminController))); +router.delete('/widgets/:id', authMiddleware, asyncHandler(adminController.deleteWidget.bind(adminController))); + +// ============================================ +// MENU +// ============================================ + +router.get('/menu', authMiddleware, asyncHandler(adminController.getMenu.bind(adminController))); +router.post('/menu', authMiddleware, asyncHandler(adminController.createMenuItem.bind(adminController))); +router.put('/menu/reorder', authMiddleware, asyncHandler(adminController.reorderMenu.bind(adminController))); +router.put('/menu/:id', authMiddleware, asyncHandler(adminController.updateMenuItem.bind(adminController))); +router.delete('/menu/:id', authMiddleware, asyncHandler(adminController.deleteMenuItem.bind(adminController))); + +// ============================================ +// STYLES +// ============================================ + +router.get('/styles', authMiddleware, asyncHandler(adminController.getStyles.bind(adminController))); +router.post('/styles', authMiddleware, asyncHandler(adminController.createStyle.bind(adminController))); +router.get('/styles/active', authMiddleware, asyncHandler(adminController.getActiveStyle.bind(adminController))); +router.put('/styles/:id/activate', authMiddleware, asyncHandler(adminController.activateStyle.bind(adminController))); +router.put('/styles/:id', authMiddleware, asyncHandler(adminController.updateStyle.bind(adminController))); +router.delete('/styles/:id', authMiddleware, asyncHandler(adminController.deleteStyle.bind(adminController))); + +// ============================================ +// THEMES +// ============================================ + +router.get('/themes', authMiddleware, asyncHandler(adminController.getThemes.bind(adminController))); +router.post('/themes', authMiddleware, asyncHandler(adminController.createTheme.bind(adminController))); +router.get('/themes/:id', authMiddleware, asyncHandler(adminController.getTheme.bind(adminController))); +router.put('/themes/:id', authMiddleware, asyncHandler(adminController.updateTheme.bind(adminController))); +router.delete('/themes/:id', authMiddleware, asyncHandler(adminController.deleteTheme.bind(adminController))); +router.put('/themes/:id/default', authMiddleware, asyncHandler(adminController.setDefaultTheme.bind(adminController))); + +// ============================================ +// CAROUSEL +// ============================================ + +router.get('/carousel', authMiddleware, asyncHandler(adminController.getCarousel.bind(adminController))); +router.post('/carousel', authMiddleware, asyncHandler(adminController.createCarouselItem.bind(adminController))); +router.put('/carousel/reorder', authMiddleware, asyncHandler(adminController.reorderCarousel.bind(adminController))); +router.put('/carousel/:id', authMiddleware, asyncHandler(adminController.updateCarouselItem.bind(adminController))); +router.delete('/carousel/:id', authMiddleware, asyncHandler(adminController.deleteCarouselItem.bind(adminController))); + +// ============================================ +// MEDIA +// ============================================ + +router.get('/media', authMiddleware, asyncHandler(adminController.getMedia.bind(adminController))); +router.post('/media', authMiddleware, mediaUpload.single('file'), asyncHandler(adminController.uploadMedia.bind(adminController))); +router.put('/media/:id', authMiddleware, asyncHandler(adminController.updateMedia.bind(adminController))); +router.delete('/media/:id', authMiddleware, asyncHandler(adminController.deleteMedia.bind(adminController))); + +// ============================================ +// ORGANIZATIONS +// ============================================ + +router.get('/organizations', authMiddleware, asyncHandler(orgController.getDefault.bind(orgController))); +router.get('/organizations/:id', authMiddleware, asyncHandler(orgController.getById.bind(orgController))); +router.get('/organizations/:id/stats', authMiddleware, asyncHandler(orgController.getWithStats.bind(orgController))); +router.put('/organizations/:id', authMiddleware, asyncHandler(orgController.update.bind(orgController))); + +// ============================================ +// COLLABORATORS +// ============================================ + +router.get('/collaborators', authMiddleware, asyncHandler(collabController.list.bind(collabController))); +router.post('/collaborators', authMiddleware, supervisorOrAdmin, asyncHandler(collabController.create.bind(collabController))); +router.get('/collaborators/:id', authMiddleware, asyncHandler(collabController.getById.bind(collabController))); +router.put('/collaborators/:id', authMiddleware, supervisorOrAdmin, asyncHandler(collabController.update.bind(collabController))); +router.put('/collaborators/:id/hierarchy', authMiddleware, supervisorOrAdmin, asyncHandler(collabController.updateHierarchy.bind(collabController))); + +// ============================================ +// USER MANAGEMENT +// ============================================ + +router.get('/users', authMiddleware, adminOnly, asyncHandler(userMgmtController.list.bind(userMgmtController))); +router.get('/users/:id', authMiddleware, asyncHandler(userMgmtController.getById.bind(userMgmtController))); +router.post('/users/:id/roles', authMiddleware, supervisorOrAdmin, asyncHandler(userMgmtController.assignRole.bind(userMgmtController))); +router.delete('/users/:id/roles/:role', authMiddleware, supervisorOrAdmin, asyncHandler(userMgmtController.revokeRole.bind(userMgmtController))); +router.post('/users/:id/unlock', authMiddleware, adminOnly, asyncHandler(userMgmtController.unlockUser.bind(userMgmtController))); +router.post('/users/:id/avatar', authMiddleware, asyncHandler(userMgmtController.updateAvatar.bind(userMgmtController))); + +// ============================================ +// AUDIT LOGS +// ============================================ + +router.get('/audit-logs', authMiddleware, supervisorOrAdmin, asyncHandler(auditController.query.bind(auditController))); +router.get('/audit-logs/user/:userId', authMiddleware, supervisorOrAdmin, asyncHandler(auditController.getUserLoginHistory.bind(auditController))); +router.get('/audit-logs/my-history', authMiddleware, asyncHandler(auditController.getMyLoginHistory.bind(auditController))); + +// ============================================ +// TASKS +// ============================================ + +router.get('/tasks', authMiddleware, asyncHandler(taskController.getAllTasks.bind(taskController))); +router.post('/tasks/:collaboratorId', authMiddleware, collaboratorOrAbove, asyncHandler(taskController.create.bind(taskController))); +router.get('/tasks/detail/:id', authMiddleware, asyncHandler(taskController.getById.bind(taskController))); +router.put('/tasks/:id', authMiddleware, supervisorOrAdmin, asyncHandler(taskController.update.bind(taskController))); +router.delete('/tasks/:id', authMiddleware, supervisorOrAdmin, asyncHandler(taskController.delete.bind(taskController))); +router.post('/tasks/:id/rate', authMiddleware, collaboratorOrAbove, asyncHandler(taskController.addRating.bind(taskController))); + +// Public task routes (for resume display) +router.get('/tasks/public/:collaboratorId', asyncHandler(taskController.getPublicTasks.bind(taskController))); +router.get('/tasks/public/:collaboratorId/summary', asyncHandler(taskController.getPublicSummary.bind(taskController))); + +// ============================================ +// USER PROFILES (Full Profile with Resume) +// ============================================ + +router.get('/user-profile/:id', authMiddleware, asyncHandler(userProfileController.getProfile.bind(userProfileController))); +router.post('/user-profile', authMiddleware, asyncHandler(userProfileController.createOrUpdateProfile.bind(userProfileController))); + +export default router; diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/routes/auth.routes.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/auth.routes.ts new file mode 100644 index 0000000000000000000000000000000000000000..54c9c0b3b13e7894bc9f7379dd0cfe21009374c9 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/auth.routes.ts @@ -0,0 +1,89 @@ +/** + * @fileoverview Authentication Routes + * Defines authentication endpoints for login, logout, and password management. + * + * @author Armand Richelet-Kleinberg + * @module routes/auth + */ + +import { Router } from 'express'; +import { AuthController } from '../controllers/auth.controller'; +import { asyncHandler } from '../middleware/error-handler.middleware'; +import { authMiddleware } from '../middleware/auth.middleware'; + +const router = Router(); + +const authController = new AuthController(); + +// ============================================ +// AUTH ENDPOINTS +// ============================================ + +/** + * @swagger + * /auth/login: + * post: + * summary: Login and get JWT token + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LoginRequest' + * responses: + * 200: + * description: Login successful + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LoginResponse' + * 401: + * description: Invalid credentials + */ +router.post('/login', asyncHandler(authController.login.bind(authController))); + +/** + * @swagger + * /auth/me: + * get: + * summary: Get current user info + * tags: [Auth] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Current user information + * 401: + * description: Not authenticated + */ +router.get('/me', authMiddleware, asyncHandler(authController.getMe.bind(authController))); + +/** + * @swagger + * /auth/change-password: + * post: + * summary: Change password + * tags: [Auth] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * currentPassword: + * type: string + * newPassword: + * type: string + * responses: + * 200: + * description: Password changed successfully + * 401: + * description: Current password incorrect + */ +router.post('/change-password', authMiddleware, asyncHandler(authController.changePassword.bind(authController))); + +export default router; diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/routes/index.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..943bfb2d05aae532058f8e2065ee4d5042ebff12 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/index.ts @@ -0,0 +1,116 @@ +/** + * @fileoverview API Routes - Main Entry Point + * Composes all route modules for Ark.Alliance.StartupCms.Ai Backend. + * + * This file serves as the central router that imports and mounts all route modules, + * following Clean Architecture principles for maintainability and separation of concerns. + * + * @author Armand Richelet-Kleinberg + * @module routes + * + * @remarks + * Routes are organized into modular files: + * - public.routes.ts: Unauthenticated public endpoints + * - auth.routes.ts: Authentication endpoints + * - admin/: Admin endpoints split by domain + * - projects.routes.ts: Project CRUD + * - resume.routes.ts: Resume/CV management + * - ai.routes.ts: AI settings and generation + * - system.routes.ts: System config (widgets, menu, styles, themes, users, etc.) + * - static-export.routes.ts: Static site generation + */ + +import { Router } from 'express'; +import publicRoutes from './public.routes'; +import authRoutes from './auth.routes'; +import adminRoutes from './admin'; +import staticExportRoutes from './static-export.routes'; +import seoRoutes from './seo.routes'; +import { getRobotsTxt } from '../controllers/sitemap.controller'; +import { UserProfileController } from '../controllers/user-profile.controller'; +import { asyncHandler } from '../middleware/error-handler.middleware'; +import { authMiddleware } from '../middleware/auth.middleware'; + +const router = Router(); + +// Controller instances for user routes not in admin +const userProfileController = new UserProfileController(); + +// ============================================ +// PUBLIC ROUTES (Unauthenticated) +// ============================================ +// Profile, carousel, technologies, themes, projects, resume, dashboard, widgets +router.use('/', publicRoutes); + +// ============================================ +// SEO ROUTES (Pages, Sitemap, Site Settings) +// ============================================ +router.use('/', seoRoutes); + +// ============================================ +// ROBOTS.TXT (Plain text response) +// ============================================ +router.get('/robots.txt', getRobotsTxt); + +// ============================================ +// AUTHENTICATION ROUTES +// ============================================ +// Login, logout, change password +router.use('/auth', authRoutes); + +// ============================================ +// ADMIN ROUTES (Authenticated) +// ============================================ +// All admin functionality organized by domain +router.use('/admin', adminRoutes); + +// ============================================ +// STATIC EXPORT ROUTES +// ============================================ +router.use('/static', staticExportRoutes); + +// ============================================ +// USER PROFILE ROUTES (Authenticated) +// ============================================ +// Complete user profile with resume data (separate from admin) + +/** + * @swagger + * /users/{id}/profile: + * get: + * summary: Get complete user profile with resume data + * tags: [Users - Profile] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * format: uuid + * responses: + * 200: + * description: Complete user profile with nested data + * 404: + * description: User not found + */ +router.get('/users/:id/profile', authMiddleware, asyncHandler(userProfileController.getProfile.bind(userProfileController))); + +/** + * @swagger + * /users/profile: + * post: + * summary: Create or update user profile with nested data + * tags: [Users - Profile] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Profile created or updated successfully + * 400: + * description: Validation error + */ +router.post('/users/profile', authMiddleware, asyncHandler(userProfileController.createOrUpdateProfile.bind(userProfileController))); + +export default router; diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/routes/public.routes.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/public.routes.ts new file mode 100644 index 0000000000000000000000000000000000000000..b202d90e85ccc5f8ce43705cc2b0c3a4773c95e7 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/public.routes.ts @@ -0,0 +1,442 @@ +/** + * @fileoverview Public API Routes + * Defines unauthenticated public endpoints for Ark.Alliance.StartupCms.Ai. + * + * @author Armand Richelet-Kleinberg + * @module routes/public + */ + +import { Router } from 'express'; +import { ProfileController } from '../controllers/profile.controller'; +import { ProjectController as LegacyProjectController } from '../controllers/project.controller'; +import { ProjectPresentationController } from '../controllers/project-presentation.controller'; +import { ResumeController } from '../controllers/resume.controller'; +import { DashboardController } from '../controllers/dashboard.controller'; +import { WidgetController } from '../controllers/widget.controller'; +import { CarouselController } from '../controllers/carousel.controller'; +import { ThemeController } from '../controllers/theme.controller'; +import { CollaboratorController } from '../controllers/collaborator.controller'; +import { OrganizationController } from '../controllers/organization.controller'; +import { TaskController } from '../controllers/task.controller'; +import { getAllTechnologies, getTechnologyByKey } from '../controllers/technology.controller'; +import { asyncHandler } from '../middleware/error-handler.middleware'; + +const router = Router(); + +const profileController = new ProfileController(); +const legacyProjectController = new LegacyProjectController(); +const presentationController = new ProjectPresentationController(); +const resumeController = new ResumeController(); +const dashboardController = new DashboardController(); +const widgetController = new WidgetController(); +const carouselController = new CarouselController(); +const themeController = new ThemeController(); +const collaboratorController = new CollaboratorController(); +const organizationController = new OrganizationController(); +const taskController = new TaskController(); + +// ============================================ +// PROFILE ENDPOINTS +// ============================================ + +/** + * @swagger + * /profile: + * get: + * summary: Get portfolio owner profile + * tags: [Public - Profile] + * responses: + * 200: + * description: Profile information + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Profile' + * 404: + * description: Profile not found + */ +router.get('/profile', asyncHandler(profileController.getProfile.bind(profileController))); + +// ============================================ +// CAROUSEL ENDPOINTS +// ============================================ + +/** + * @swagger + * /carousel: + * get: + * summary: Get homepage carousel items + * tags: [Public - Carousel] + * responses: + * 200: + * description: List of carousel items + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/CarouselItem' + */ +router.get('/carousel', asyncHandler(carouselController.getCarousel.bind(carouselController))); + +// ============================================ +// TECHNOLOGY ENDPOINTS +// ============================================ + +/** + * @swagger + * /technologies: + * get: + * summary: Get all technologies + * tags: [Public - Technologies] + * responses: + * 200: + * description: List of all technologies with categories + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Technology' + */ +router.get('/technologies', asyncHandler(getAllTechnologies)); + +/** + * @swagger + * /technologies/{key}: + * get: + * summary: Get technology by key + * tags: [Public - Technologies] + * parameters: + * - in: path + * name: key + * required: true + * schema: + * type: string + * description: Technology key (e.g., 'react', 'typescript') + * responses: + * 200: + * description: Technology details + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Technology' + * 404: + * description: Technology not found + */ +router.get('/technologies/:key', asyncHandler(getTechnologyByKey)); + +// ============================================ +// THEME ENDPOINTS +// ============================================ + +/** + * @swagger + * /themes: + * get: + * summary: Get all active themes + * tags: [Public - Themes] + * responses: + * 200: + * description: List of active themes (without CSS content) + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/ThemeListItem' + */ +router.get('/themes', asyncHandler(themeController.listThemes.bind(themeController))); + +/** + * @swagger + * /themes/default: + * get: + * summary: Get default theme with CSS content + * tags: [Public - Themes] + * responses: + * 200: + * description: Default theme with full CSS content + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ThemeDetail' + * 404: + * description: No default theme configured + */ +router.get('/themes/default', asyncHandler(themeController.getDefaultTheme.bind(themeController))); + +/** + * @swagger + * /themes/{slug}: + * get: + * summary: Get theme by slug with CSS content + * tags: [Public - Themes] + * parameters: + * - in: path + * name: slug + * required: true + * schema: + * type: string + * description: Theme slug (e.g., 'default-cyber', 'neon-cyber') + * responses: + * 200: + * description: Theme with full CSS content + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ThemeDetail' + * 404: + * description: Theme not found + */ +router.get('/themes/:slug', asyncHandler(themeController.getThemeBySlug.bind(themeController))); + +// ============================================ +// PROJECT ENDPOINTS +// ============================================ + +/** + * @swagger + * /projects: + * get: + * summary: Get all projects + * tags: [Public - Projects] + * responses: + * 200: + * description: List of all projects + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Project' + */ +router.get('/projects', asyncHandler(presentationController.getAllProjects.bind(presentationController))); + +/** + * @swagger + * /projects/featured: + * get: + * summary: Get featured projects + * tags: [Public - Projects] + * responses: + * 200: + * description: List of featured projects for homepage + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Project' + */ +router.get('/projects/featured', asyncHandler(legacyProjectController.getFeaturedProjects.bind(legacyProjectController))); + +/** + * @swagger + * /projects/{id}/presentation: + * get: + * summary: Get full project presentation + * tags: [Public - Projects] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Project ID or slug + * responses: + * 200: + * description: Full project with pages, features, and technologies + * 404: + * description: Project not found + */ +router.get('/projects/:id/presentation', asyncHandler(presentationController.getFullProject.bind(presentationController))); + +// ============================================ +// RESUME ENDPOINTS +// ============================================ + +// CV (Legacy redirect) +router.get('/cv', (req, res) => res.redirect(301, '/api/resume')); + +/** + * @swagger + * /resume: + * get: + * summary: Get resume/CV data + * tags: [Public - Resume] + * responses: + * 200: + * description: Complete resume with profile, experiences, skills, education + */ +router.get('/resume', asyncHandler(resumeController.getResume.bind(resumeController))); + +// ============================================ +// DASHBOARD & WIDGETS ENDPOINTS +// ============================================ + +/** + * @swagger + * /dashboard: + * get: + * summary: Get dashboard data + * tags: [Public - Dashboard] + * responses: + * 200: + * description: Dashboard statistics and overview + */ +router.get('/dashboard', asyncHandler(dashboardController.getDashboardData.bind(dashboardController))); + +/** + * @swagger + * /widgets/home: + * get: + * summary: Get home page widgets + * tags: [Public - Dashboard] + * responses: + * 200: + * description: List of widgets for home page + */ +router.get('/widgets/home', asyncHandler(widgetController.getHomeWidgets.bind(widgetController))); + +/** + * @swagger + * /widgets/project/{projectId}: + * get: + * summary: Get project-specific widgets + * tags: [Public - Dashboard] + * parameters: + * - in: path + * name: projectId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: List of widgets for specific project + */ +router.get('/widgets/project/:projectId', asyncHandler(widgetController.getProjectWidgets.bind(widgetController))); + +// ============================================ +// TEAM (Organization Chart) ENDPOINTS +// ============================================ + +/** + * @swagger + * /team: + * get: + * summary: Get organization chart + * tags: [Public - Team] + * parameters: + * - in: query + * name: organizationId + * schema: + * type: string + * description: Organization ID (optional, uses default if not provided) + * responses: + * 200: + * description: Organization chart with team members + * 404: + * description: Organization not found + */ +router.get('/team', asyncHandler(collaboratorController.getOrgChart.bind(collaboratorController))); + +/** + * @swagger + * /team/{id}: + * get: + * summary: Get team member details + * tags: [Public - Team] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Team member details with reports + * 404: + * description: Team member not found + */ +router.get('/team/:id', asyncHandler(collaboratorController.getById.bind(collaboratorController))); + +// ============================================ +// ORGANIZATION ENDPOINTS +// ============================================ + +/** + * @swagger + * /organization: + * get: + * summary: Get default organization + * tags: [Public - Organization] + * responses: + * 200: + * description: Default organization information + * 404: + * description: No organization found + */ +router.get('/organization', asyncHandler(organizationController.getDefault.bind(organizationController))); + +/** + * @swagger + * /organization/{id}: + * get: + * summary: Get organization by ID + * tags: [Public - Organization] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Organization details + * 404: + * description: Organization not found + */ +router.get('/organization/:id', asyncHandler(organizationController.getById.bind(organizationController))); + +// ============================================ +// TASKS (Public Accomplishments) ENDPOINTS +// ============================================ + +/** + * @swagger + * /tasks/public/{collaboratorId}: + * get: + * summary: Get public accomplishments for a team member + * tags: [Public - Tasks] + * parameters: + * - in: path + * name: collaboratorId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: List of public accomplishments + */ +router.get('/tasks/public/:collaboratorId', asyncHandler(taskController.getPublicTasks.bind(taskController))); + +/** + * @swagger + * /tasks/public/{collaboratorId}/summary: + * get: + * summary: Get accomplishments summary for a team member + * tags: [Public - Tasks] + * parameters: + * - in: path + * name: collaboratorId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Summary statistics of accomplishments + */ +router.get('/tasks/public/:collaboratorId/summary', asyncHandler(taskController.getPublicSummary.bind(taskController))); + +export default router; diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/routes/seo.routes.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/seo.routes.ts new file mode 100644 index 0000000000000000000000000000000000000000..70c2d6c482a6fc73a0f7675f5735b580a4eeac28 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/seo.routes.ts @@ -0,0 +1,51 @@ +/** + * @fileoverview SEO Routes + * @description Routes for CMS pages, sitemap, robots.txt, and site settings. + * @author Armand Richelet-Kleinberg + */ + +import { Router } from 'express'; +import * as pageController from '../controllers/page.controller'; +import * as siteSettingsController from '../controllers/site-settings.controller'; +import * as sitemapController from '../controllers/sitemap.controller'; +import { authMiddleware } from '../middleware/auth.middleware'; + +const router = Router(); + +// ============================================ +// PUBLIC ROUTES +// ============================================ + +// Pages +router.get('/pages', pageController.getAllPages); +router.get('/pages/:slug', pageController.getPageBySlug); +router.get('/pages/type/:pageType', pageController.getPagesByType); + +// Site Settings +router.get('/site-settings', siteSettingsController.getSettings); + +// SEO Schemas +router.get('/seo/organization-schema', siteSettingsController.getOrganizationSchema); +router.get('/seo/website-schema', siteSettingsController.getWebSiteSchema); + +// Sitemap +router.get('/sitemap', sitemapController.getSitemapData); + +// ============================================ +// ADMIN ROUTES (Protected) +// ============================================ + +// Page Management +router.get('/admin/pages', authMiddleware, pageController.getAllPagesAdmin); +router.post('/admin/pages', authMiddleware, pageController.createPage); +router.put('/admin/pages/:id', authMiddleware, pageController.updatePage); +router.delete('/admin/pages/:id', authMiddleware, pageController.deletePage); + +// SEO Metadata Management +router.put('/admin/pages/:pageId/seo', authMiddleware, pageController.updatePageSeo); +router.post('/admin/pages/:pageId/structured-data', authMiddleware, pageController.addStructuredData); + +// Site Settings Management +router.put('/admin/site-settings', authMiddleware, siteSettingsController.updateSettings); + +export default router; diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/routes/static-export.routes.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/static-export.routes.ts new file mode 100644 index 0000000000000000000000000000000000000000..114c03ab8a33f0b27872a3a2b24565201965368c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/routes/static-export.routes.ts @@ -0,0 +1,56 @@ +/** + * @fileoverview Static Export API Routes + * Backend endpoints for static site generation using the new engine. + * + * @author Armand Richelet-Kleinberg + */ + +import { Router, Request, Response } from 'express'; +import { staticGenerationService } from '../services/static-generation.service'; + +const router = Router(); + +/** + * POST /api/admin/export-static + * Generate and download complete static React/TSX website as ZIP. + */ +router.post('/export-static', async (req: Request, res: Response) => { + try { + console.log('[StaticExport] Starting static site generation...'); + + const config = req.body?.config; + await staticGenerationService.generateStaticSite(res, config); + + } catch (error) { + console.error('[StaticExport] Generation error:', error); + if (!res.headersSent) { + res.status(500).json({ + error: 'Failed to generate static export', + details: error instanceof Error ? error.message : 'Unknown error' + }); + } + } +}); + +/** + * GET /api/admin/export-static/preview + * Get preview data for static export. + */ +router.get('/export-static/preview', async (req: Request, res: Response) => { + try { + const preview = await staticGenerationService.getExportPreview(); + res.json({ + success: true, + data: preview + }); + } catch (error) { + console.error('[StaticExport] Preview error:', error); + res.status(500).json({ + error: 'Failed to get export preview', + details: error instanceof Error ? error.message : 'Unknown error' + }); + } +}); + +export default router; + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/seed-admin.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/seed-admin.ts new file mode 100644 index 0000000000000000000000000000000000000000..26e6e2fa9b430bb2aabf58c9e6ba2e18d44f790d --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/seed-admin.ts @@ -0,0 +1,35 @@ +/** + * @fileoverview Admin User Seeder + * Seeds or updates the initial admin user during application startup. + * + * @author Armand Richelet-Kleinberg + */ + +import { AuthService } from './services/auth.service'; + +/** + * Seed the default admin user. + * + * @remarks + * This function is called during server initialization to ensure + * an admin account exists. It uses the ADMIN_PASSWORD environment + * variable or defaults to 'Admin1234'. + * + * Behavior: + * - Creates admin user if not exists + * - Updates admin password if already exists + * + * @example + * ```typescript + * // In server startup + * import seed from './seed-admin'; + * await seed(); + * ``` + */ +const seed = async (): Promise => { + const authService = new AuthService(); + // Default credentials: admin / Admin1234 (or ADMIN_PASSWORD env var) + await authService.seedAdminUser(); +}; + +export default seed; diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-carousel.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-carousel.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..327d819f092a5739e389f7970120503e29fbc244 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-carousel.service.ts @@ -0,0 +1,119 @@ +/** + * @fileoverview Admin Carousel Service + * Handles administrative operations for carousel items (CRUD). + * + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { CarouselItem } from '../database/entities/carousel-item.entity'; +import { AdminCarouselItemDto, CrudResponseDto, ReorderCarouselDto } from '@arkalliance/startupcms-ai-share'; + +export class AdminCarouselService { + private carouselRepo = AppDataSource.getRepository(CarouselItem); + + async getAllCarouselItems(): Promise { + return this.carouselRepo.find({ + order: { order: 'ASC' } + }); + } + + async createCarouselItem(dto: AdminCarouselItemDto): Promise> { + try { + const item = this.carouselRepo.create(dto); + const saved = await this.carouselRepo.save(item); + return { + success: true, + message: 'Carousel item created successfully', + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to create carousel item', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + async updateCarouselItem(id: number, dto: AdminCarouselItemDto): Promise> { + try { + const item = await this.carouselRepo.findOne({ where: { id } }); + if (!item) { + return { + success: false, + message: 'Carousel item not found', + timestamp: new Date().toISOString() + }; + } + + this.carouselRepo.merge(item, dto); + const saved = await this.carouselRepo.save(item); + return { + success: true, + message: 'Carousel item updated successfully', + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to update carousel item', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + async deleteCarouselItem(id: number): Promise> { + try { + const result = await this.carouselRepo.delete(id); + if (result.affected === 0) { + return { + success: false, + message: 'Carousel item not found', + timestamp: new Date().toISOString() + }; + } + return { + success: true, + message: 'Carousel item deleted successfully', + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to delete carousel item', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + async reorderCarouselItems(dto: ReorderCarouselDto): Promise> { + try { + await AppDataSource.transaction(async transactionalEntityManager => { + for (let i = 0; i < dto.itemIds.length; i++) { + await transactionalEntityManager.update(CarouselItem, dto.itemIds[i], { order: i }); + } + }); + return { + success: true, + message: 'Carousel items reordered successfully', + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to reorder carousel items', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-media.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-media.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..b968b7f4ec533d54a03f492e05a1c979ce45fa1d --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-media.service.ts @@ -0,0 +1,328 @@ +/** + * @fileoverview Admin Media Service + * Handles administrative operations for media assets. + * Supports file upload, metadata management, and search. + * + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { Media, MediaType } from '../database/entities/media.entity'; +import { CrudResponseDto, MediaTypeEnum } from '@arkalliance/startupcms-ai-share'; +import { Like, In } from 'typeorm'; +import { getMediaType, getPublicUrl } from '../middleware/upload.middleware'; +import fs from 'fs'; + +/** + * Media upload request data. + */ +interface MediaUploadData { + name: string; + key?: string; + altText?: string; + description?: string; + tags?: string[]; + metadata?: Record; + isPublic?: boolean; +} + +/** + * Media search parameters. + */ +interface MediaSearchParams { + type?: MediaTypeEnum; + search?: string; + tags?: string[]; + page?: number; + pageSize?: number; +} + +export class AdminMediaService { + private mediaRepo = AppDataSource.getRepository(Media); + + /** + * Get all media with optional filtering. + * @param params - Search parameters + * @returns Array of media items + */ + async getAllMedia(params: MediaSearchParams = {}): Promise<{ items: Media[]; total: number }> { + const { type, search, tags, page = 1, pageSize = 50 } = params; + + const queryBuilder = this.mediaRepo.createQueryBuilder('media'); + + if (type) { + queryBuilder.andWhere('media.type = :type', { type }); + } + + if (search) { + queryBuilder.andWhere( + '(media.name LIKE :search OR media.description LIKE :search OR media.key LIKE :search)', + { search: `%${search}%` } + ); + } + + const [items, total] = await queryBuilder + .orderBy('media.createdAt', 'DESC') + .skip((page - 1) * pageSize) + .take(pageSize) + .getManyAndCount(); + + return { items, total }; + } + + /** + * Get a single media item by ID. + * @param id - Media UUID + * @returns Media item or null + */ + async getById(id: string): Promise { + return this.mediaRepo.findOneBy({ id }); + } + + /** + * Get a single media item by key. + * @param key - Unique media key + * @returns Media item or null + */ + async getByKey(key: string): Promise { + return this.mediaRepo.findOneBy({ key }); + } + + /** + * Create a new media item from uploaded file. + * @param file - Multer file object + * @param data - Media metadata + * @returns CRUD response with created media + */ + async createFromUpload( + file: Express.Multer.File, + data: MediaUploadData + ): Promise> { + try { + // Check for duplicate key + if (data.key) { + const existing = await this.getByKey(data.key); + if (existing) { + return { + success: false, + message: `Media with key '${data.key}' already exists`, + timestamp: new Date().toISOString() + }; + } + } + + const mediaType = getMediaType(file.mimetype) as MediaType; + const url = getPublicUrl(file.path); + + const media = this.mediaRepo.create({ + name: data.name || file.originalname, + key: data.key, + url, + type: mediaType, + mimeType: file.mimetype, + originalFileName: file.originalname, + fileSize: file.size, + altText: data.altText, + description: data.description, + tags: data.tags || [], + metadata: data.metadata, + isPublic: data.isPublic ?? true + }); + + const saved = await this.mediaRepo.save(media); + return { + success: true, + message: 'Media uploaded successfully', + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to upload media', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Create a new media item from URL. + * @param url - External media URL + * @param data - Media metadata + * @returns CRUD response with created media + */ + async createFromUrl( + url: string, + type: MediaTypeEnum, + data: MediaUploadData + ): Promise> { + try { + // Check for duplicate key + if (data.key) { + const existing = await this.getByKey(data.key); + if (existing) { + return { + success: false, + message: `Media with key '${data.key}' already exists`, + timestamp: new Date().toISOString() + }; + } + } + + const media = this.mediaRepo.create({ + name: data.name, + key: data.key, + url, + type: type as unknown as MediaType, + altText: data.altText, + description: data.description, + tags: data.tags || [], + metadata: data.metadata, + isPublic: data.isPublic ?? true + }); + + const saved = await this.mediaRepo.save(media); + return { + success: true, + message: 'Media created successfully', + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to create media', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Update media metadata. + * @param id - Media UUID + * @param data - Updated metadata + * @returns CRUD response with updated media + */ + async updateMedia(id: string, data: Partial): Promise> { + try { + const media = await this.getById(id); + if (!media) { + return { + success: false, + message: 'Media not found', + timestamp: new Date().toISOString() + }; + } + + // Check for key conflict + if (data.key && data.key !== media.key) { + const existing = await this.getByKey(data.key); + if (existing) { + return { + success: false, + message: `Media with key '${data.key}' already exists`, + timestamp: new Date().toISOString() + }; + } + } + + // Update fields + if (data.name !== undefined) media.name = data.name; + if (data.key !== undefined) media.key = data.key; + if (data.altText !== undefined) media.altText = data.altText; + if (data.description !== undefined) media.description = data.description; + if (data.tags !== undefined) media.tags = data.tags; + if (data.metadata !== undefined) media.metadata = { ...media.metadata, ...data.metadata }; + if (data.isPublic !== undefined) media.isPublic = data.isPublic; + + const saved = await this.mediaRepo.save(media); + return { + success: true, + message: 'Media updated successfully', + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to update media', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Delete media by ID. + * @param id - Media UUID + * @returns CRUD response + */ + async deleteMedia(id: string): Promise> { + try { + const media = await this.getById(id); + if (!media) { + return { + success: false, + message: 'Media not found', + timestamp: new Date().toISOString() + }; + } + + // Delete physical file if it's local + if (media.url.includes('/uploads/')) { + const filePath = media.url.replace(/^https?:\/\/[^/]+\/uploads/, './uploads'); + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } catch (e) { + console.warn('Failed to delete file:', filePath); + } + } + + await this.mediaRepo.remove(media); + return { + success: true, + message: 'Media deleted successfully', + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to delete media', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Get media items by tags. + * @param tags - Array of tag strings + * @returns Array of matching media items + */ + async getByTags(tags: string[]): Promise { + const allMedia = await this.mediaRepo.find(); + return allMedia.filter(media => + tags.some(tag => media.tags?.includes(tag)) + ); + } + + /** + * Get all available tags. + * @returns Array of unique tag strings + */ + async getAllTags(): Promise { + const allMedia = await this.mediaRepo.find(); + const tagSet = new Set(); + allMedia.forEach(media => { + media.tags?.forEach(tag => tagSet.add(tag)); + }); + return Array.from(tagSet).sort(); + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-menu.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-menu.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..18c7516afd161f35f3d2e7a330b5e8ae736c9c3b --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-menu.service.ts @@ -0,0 +1,180 @@ +/** + * @fileoverview Admin Menu Service + * Handles administrative operations for navigation menus. + * + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { MenuItem } from '../database/entities/menu-item.entity'; +import { AdminMenuItemDto, CrudResponseDto, ReorderMenuItemsDto, MenuPositionEnum } from '@arkalliance/startupcms-ai-share'; +import { IsNull } from 'typeorm'; + +/** + * Service for managing navigation menu items. + */ +export class AdminMenuService { + private menuRepo = AppDataSource.getRepository(MenuItem); + + /** + * Get menu items by position. + * @param position - Menu position (HEADER, FOOTER, etc.) + * @returns Root menu items with children + */ + async getMenuByPosition(position: MenuPositionEnum): Promise { + return this.menuRepo.find({ + where: { position, parentId: IsNull() }, // Get root items using IsNull() + relations: { children: true }, + order: { order: 'ASC' } + }); + } + + /** + * Get all menu items. + * @returns All menu items + */ + async getAllMenuItems(): Promise { + return this.menuRepo.find({ + relations: { children: true }, + order: { order: 'ASC' } + }); + } + + /** + * Create a new menu item. + * @param dto - Menu item data + * @returns CRUD response with created item + */ + async createMenuItem(dto: AdminMenuItemDto): Promise> { + try { + const item = this.menuRepo.create({ + label: dto.label, + icon: dto.icon, + route: dto.route, + position: dto.position, + order: dto.order ?? 0, + isVisible: dto.isVisible ?? true, + openInNewTab: dto.openInNewTab ?? false, + parentId: dto.parentId ?? undefined + }); + const saved = await this.menuRepo.save(item); + return { + success: true, + message: 'Menu item created successfully', + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to create menu item', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Update an existing menu item. + * @param id - Menu item ID + * @param dto - Updated menu item data + * @returns CRUD response with updated item + */ + async updateMenuItem(id: number, dto: AdminMenuItemDto): Promise> { + try { + const item = await this.menuRepo.findOne({ where: { id } }); + if (!item) { + return { + success: false, + message: 'Menu item not found', + timestamp: new Date().toISOString() + }; + } + + // Update fields explicitly + if (dto.label !== undefined) item.label = dto.label; + if (dto.icon !== undefined) item.icon = dto.icon; + if (dto.route !== undefined) item.route = dto.route; + if (dto.position !== undefined) item.position = dto.position; + if (dto.order !== undefined) item.order = dto.order; + if (dto.isVisible !== undefined) item.isVisible = dto.isVisible; + if (dto.openInNewTab !== undefined) item.openInNewTab = dto.openInNewTab; + if (dto.parentId !== undefined) item.parentId = dto.parentId ?? undefined as any; + + const saved = await this.menuRepo.save(item); + return { + success: true, + message: 'Menu item updated successfully', + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to update menu item', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Delete a menu item. + * @param id - Menu item ID + * @returns CRUD response + */ + async deleteMenuItem(id: number): Promise> { + try { + const result = await this.menuRepo.delete(id); + if (result.affected === 0) { + return { + success: false, + message: 'Menu item not found', + timestamp: new Date().toISOString() + }; + } + return { + success: true, + message: 'Menu item deleted successfully', + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to delete menu item', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Reorder menu items. + * @param dto - Reorder request with item IDs + * @returns CRUD response + */ + async reorderMenuItems(dto: ReorderMenuItemsDto): Promise> { + try { + await AppDataSource.transaction(async transactionalEntityManager => { + for (let i = 0; i < dto.itemIds.length; i++) { + await transactionalEntityManager.update(MenuItem, dto.itemIds[i], { order: i }); + } + }); + return { + success: true, + message: 'Menu items reordered successfully', + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to reorder menu items', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-project.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-project.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..e718970c0d1385b25f7691e689fcabef629f3f74 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-project.service.ts @@ -0,0 +1,221 @@ +/** + * @fileoverview Admin Project Service + * Handles administrative operations for projects (CRUD). + * + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { Project } from '../database/entities/project.entity'; +import { ProjectTechnology } from '../database/entities/project-technology.entity'; +import { AdminProjectDto, CrudResponseDto, PaginatedResponseDto } from '@arkalliance/startupcms-ai-share'; +import { Like } from 'typeorm'; +import { EventService } from './event.service'; + +/** + * Service for managing projects in the admin panel. + */ +export class AdminProjectService { + private projectRepo = AppDataSource.getRepository(Project); + private techRepo = AppDataSource.getRepository(ProjectTechnology); + private eventService = new EventService(); + + /** + * Get all projects with pagination and optional search. + * @param page - Page number (1-indexed) + * @param pageSize - Items per page + * @param search - Optional search term for title + * @returns Paginated response with projects + */ + async getAllProjects(page: number = 1, pageSize: number = 10, search?: string): Promise> { + const skip = (page - 1) * pageSize; + const where = search ? { title: Like(`%${search}%`) } : {}; + + const [items, total] = await this.projectRepo.findAndCount({ + where, + relations: { technologies: true }, + order: { startDate: 'DESC' }, + skip, + take: pageSize + }); + + return { + items, + total, + page, + pageSize, + hasNext: total > skip + pageSize, + hasPrevious: page > 1 + }; + } + + /** + * Get a single project by ID. + * @param id - Project UUID + * @returns Project with all relations or null + */ + async getProjectById(id: string): Promise { + return this.projectRepo.findOne({ + where: { id }, + relations: { technologies: true, features: true, pages: true } + }); + } + + /** + * Create a new project. + * @param dto - Project data + * @returns CRUD response with created project + */ + async createProject(dto: AdminProjectDto): Promise> { + try { + const project = this.projectRepo.create({ + title: dto.title, + description: dto.description, + imageUrl: dto.imageUrl, + status: dto.status as any, + isFeatured: dto.isFeatured ?? false, + startDate: dto.startDate ? new Date(dto.startDate) : undefined, + endDate: dto.endDate ? new Date(dto.endDate) : undefined, + // Map DTO fields to entity fields + repoUrl: dto.repositoryUrl, + demoUrl: dto.liveUrl + }); + + const savedProject = await this.projectRepo.save(project); + + // Handle technologies (array of technology objects or strings) + if (dto.technologies && dto.technologies.length > 0) { + const techs = dto.technologies.map(t => { + const tech = new ProjectTechnology(); + tech.projectId = savedProject.id; + tech.technology = typeof t === 'string' ? t : t.name; + tech.project = savedProject; + return tech; + }); + await this.techRepo.save(techs); + savedProject.technologies = techs; + } + + await this.eventService.publish('ProjectCreated', { id: savedProject.id }); + + return { + success: true, + message: 'Project created successfully', + data: savedProject, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to create project', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Update an existing project. + * @param id - Project UUID + * @param dto - Updated project data + * @returns CRUD response with updated project + */ + async updateProject(id: string, dto: AdminProjectDto): Promise> { + try { + const project = await this.projectRepo.findOne({ + where: { id }, + relations: { technologies: true } + }); + + if (!project) { + return { + success: false, + message: 'Project not found', + timestamp: new Date().toISOString() + }; + } + + // Update fields + if (dto.title !== undefined) project.title = dto.title; + if (dto.description !== undefined) project.description = dto.description; + if (dto.imageUrl !== undefined) project.imageUrl = dto.imageUrl; + if (dto.status !== undefined) project.status = dto.status as any; + if (dto.isFeatured !== undefined) project.isFeatured = dto.isFeatured; + if (dto.startDate !== undefined) project.startDate = new Date(dto.startDate); + if (dto.endDate !== undefined) project.endDate = dto.endDate ? new Date(dto.endDate) : undefined as any; + // Map DTO fields to entity fields + if (dto.repositoryUrl !== undefined) project.repoUrl = dto.repositoryUrl; + if (dto.liveUrl !== undefined) project.demoUrl = dto.liveUrl; + + await this.projectRepo.save(project); + + // Update technologies if provided + if (dto.technologies) { + // Remove existing + await this.techRepo.delete({ projectId: id }); + + // Add new + const techs = dto.technologies.map(t => { + const tech = new ProjectTechnology(); + tech.projectId = id; + tech.technology = typeof t === 'string' ? t : t.name; + tech.project = project; + return tech; + }); + await this.techRepo.save(techs); + project.technologies = techs; + } + + await this.eventService.publish('ProjectUpdated', { id: project.id }); + + return { + success: true, + message: 'Project updated successfully', + data: project, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to update project', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Delete a project. + * @param id - Project UUID + * @returns CRUD response + */ + async deleteProject(id: string): Promise> { + try { + const result = await this.projectRepo.delete(id); + if (result.affected === 0) { + return { + success: false, + message: 'Project not found', + timestamp: new Date().toISOString() + }; + } + await this.eventService.publish('ProjectDeleted', { id }); + + return { + success: true, + message: 'Project deleted successfully', + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to delete project', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-resume.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-resume.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..a40108f67ae38183e3f48f68c7fef3ec5ebaddef --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-resume.service.ts @@ -0,0 +1,701 @@ +/** + * @fileoverview Admin Resume Service + * Handles administrative operations for Resume (Profile, Experience, Education, Skills, Categories, + * Languages, Hobbies, Business Domains). + * + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { Profile } from '../database/entities/profile.entity'; +import { Experience } from '../database/entities/experience.entity'; +import { Education } from '../database/entities/education.entity'; +import { Skill } from '../database/entities/skill.entity'; +import { SkillCategory } from '../database/entities/skill-category.entity'; +import { Language } from '../database/entities/language.entity'; +import { Hobby } from '../database/entities/hobby.entity'; +import { BusinessDomain } from '../database/entities/business-domain.entity'; +import { + AdminResumeDto, + CrudResponseDto, + AdminExperienceDto, + AdminEducationDto, + AdminSkillDto, + SkillCategoryDto, + ReorderSkillsDto, + LanguageDto, + HobbyDto, + BusinessDomainDto, + validateLanguageDto, + validateHobbyDto, + validateBusinessDomainDto +} from '@arkalliance/startupcms-ai-share'; + +export class AdminResumeService { + private profileRepo = AppDataSource.getRepository(Profile); + private experienceRepo = AppDataSource.getRepository(Experience); + private educationRepo = AppDataSource.getRepository(Education); + private skillRepo = AppDataSource.getRepository(Skill); + private categoryRepo = AppDataSource.getRepository(SkillCategory); + private languageRepo = AppDataSource.getRepository(Language); + private hobbyRepo = AppDataSource.getRepository(Hobby); + private businessDomainRepo = AppDataSource.getRepository(BusinessDomain); + + // ============================================ + // Full Resume Retrieval + // ============================================ + + async getFullResume(): Promise { + const profile = await this.profileRepo.findOne({ where: {} }) || new Profile(); + const experiences = await this.experienceRepo.find({ order: { displayOrder: 'ASC', startDate: 'DESC' } }); + const education = await this.educationRepo.find({ order: { startDate: 'DESC' } }); + const skills = await this.skillRepo.find({ order: { displayOrder: 'ASC' }, relations: ['category'] }); + const languages = await this.languageRepo.find({ order: { displayOrder: 'ASC' } }); + const hobbies = await this.hobbyRepo.find({ order: { displayOrder: 'ASC' } }); + const businessDomains = await this.businessDomainRepo.find({ order: { displayOrder: 'ASC' } }); + + return { + profile: { + firstName: profile.firstName || '', + lastName: profile.lastName || '', + title: profile.title || '', + overview: profile.overview || '', + email: profile.email || '', + linkedinUrl: profile.linkedinUrl, + githubUrl: profile.githubUrl, + avatarUrl: profile.avatarUrl + }, + experiences: experiences.map(e => ({ + id: e.id, + company: e.company, + position: e.position, + description: e.description, + startDate: e.startDate?.toISOString?.() || '', + endDate: e.endDate?.toISOString?.() || undefined, + technologies: e.technologies || [], + isHighlighted: e.isHighlighted + })), + education: education.map(e => ({ + id: e.id, + institution: e.institution, + degree: e.degree, + fieldOfStudy: e.fieldOfStudy, + startDate: e.startDate?.toISOString?.() || '', + endDate: e.endDate?.toISOString?.() || undefined, + description: e.description + })), + skills: skills.map(s => ({ + id: s.id, + name: s.name, + level: s.level as any, + category: s.category?.name || 'Uncategorized', + yearsOfExperience: s.yearsOfExperience + })), + languages: languages.map(l => ({ + id: l.id, + language: l.language, + speaking: l.speaking, + writing: l.writing, + presenting: l.presenting, + displayOrder: l.displayOrder + })), + hobbies: hobbies.map(h => ({ + id: h.id, + name: h.name, + description: h.description, + icon: h.icon, + displayOrder: h.displayOrder + })), + businessDomains: businessDomains.map(d => ({ + id: d.id, + domain: d.domain, + level: d.level as any, + description: d.description, + yearsOfExperience: d.yearsOfExperience, + icon: d.icon, + displayOrder: d.displayOrder + })) + }; + } + + // ============================================ + // Profile CRUD + // ============================================ + + async updateProfile(dto: AdminResumeDto['profile']): Promise> { + try { + let profile = await this.profileRepo.findOne({ where: {} }); + if (!profile) { + profile = this.profileRepo.create(dto); + } else { + this.profileRepo.merge(profile, dto); + } + + const saved = await this.profileRepo.save(profile); + return { + success: true, + message: 'Profile updated successfully', + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to update profile', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + // ============================================ + // Experience CRUD + // ============================================ + + async createExperience(dto: AdminExperienceDto): Promise> { + try { + const experience = this.experienceRepo.create({ + company: dto.company, + position: dto.position, + description: dto.description, + startDate: dto.startDate ? new Date(dto.startDate) : undefined, + endDate: dto.endDate ? new Date(dto.endDate) : null, + technologies: dto.technologies, + isHighlighted: dto.isHighlighted || false, + displayOrder: await this.getNextExperienceOrder() + }); + + const saved = await this.experienceRepo.save(experience); + return { success: true, message: 'Experience created', data: saved, timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to create experience', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async updateExperience(id: number, dto: AdminExperienceDto): Promise> { + try { + const experience = await this.experienceRepo.findOneBy({ id }); + if (!experience) { + return { success: false, message: 'Experience not found', timestamp: new Date().toISOString() }; + } + + experience.company = dto.company; + experience.position = dto.position; + experience.description = dto.description; + experience.startDate = dto.startDate ? new Date(dto.startDate) : experience.startDate; + experience.endDate = dto.endDate ? new Date(dto.endDate) : null; + experience.technologies = dto.technologies; + experience.isHighlighted = dto.isHighlighted ?? experience.isHighlighted; + + const saved = await this.experienceRepo.save(experience); + return { success: true, message: 'Experience updated', data: saved, timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to update experience', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async deleteExperience(id: number): Promise> { + try { + await this.experienceRepo.delete(id); + return { success: true, message: 'Experience deleted', timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to delete experience', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + /** + * Reorder experiences by updating their displayOrder values. + * @param experienceIds - Array of experience IDs in new order + */ + async reorderExperiences(experienceIds: number[]): Promise> { + try { + for (let i = 0; i < experienceIds.length; i++) { + await this.experienceRepo.update(experienceIds[i], { displayOrder: i }); + } + return { success: true, message: 'Experiences reordered', timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to reorder experiences', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + // ============================================ + // Education CRUD + // ============================================ + + async createEducation(dto: AdminEducationDto): Promise> { + try { + const education = this.educationRepo.create({ + institution: dto.institution, + degree: dto.degree, + fieldOfStudy: dto.fieldOfStudy, + startDate: dto.startDate ? new Date(dto.startDate) : undefined, + endDate: dto.endDate ? new Date(dto.endDate) : undefined, + description: dto.description + }); + + const saved = await this.educationRepo.save(education); + return { success: true, message: 'Education created', data: saved, timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to create education', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async updateEducation(id: number, dto: AdminEducationDto): Promise> { + try { + const education = await this.educationRepo.findOneBy({ id }); + if (!education) { + return { success: false, message: 'Education not found', timestamp: new Date().toISOString() }; + } + + education.institution = dto.institution; + education.degree = dto.degree; + education.fieldOfStudy = dto.fieldOfStudy; + education.startDate = dto.startDate ? new Date(dto.startDate) : education.startDate; + education.endDate = dto.endDate ? new Date(dto.endDate) : education.endDate; + education.description = dto.description ?? education.description; + + const saved = await this.educationRepo.save(education); + return { success: true, message: 'Education updated', data: saved, timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to update education', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async deleteEducation(id: number): Promise> { + try { + await this.educationRepo.delete(id); + return { success: true, message: 'Education deleted', timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to delete education', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + /** + * Reorder education entries by updating their displayOrder values. + * @param educationIds - Array of education IDs in new order + */ + async reorderEducation(educationIds: number[]): Promise> { + try { + for (let i = 0; i < educationIds.length; i++) { + await this.educationRepo.update(educationIds[i], { displayOrder: i }); + } + return { success: true, message: 'Education reordered', timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to reorder education', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + + // ============================================ + // Skill CRUD + // ============================================ + + async createSkill(dto: AdminSkillDto): Promise> { + try { + // Find or create category + let category = await this.categoryRepo.findOneBy({ name: dto.category }); + if (!category && dto.category) { + category = await this.categoryRepo.save(this.categoryRepo.create({ + name: dto.category, + displayOrder: await this.getNextCategoryOrder() + })); + } + + const skill = this.skillRepo.create({ + name: dto.name, + level: dto.level, + categoryId: category?.id, + yearsOfExperience: dto.yearsOfExperience, + displayOrder: await this.getNextSkillOrder(category?.id) + }); + + const saved = await this.skillRepo.save(skill); + return { success: true, message: 'Skill created', data: saved, timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to create skill', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async updateSkill(id: number, dto: AdminSkillDto): Promise> { + try { + const skill = await this.skillRepo.findOneBy({ id }); + if (!skill) { + return { success: false, message: 'Skill not found', timestamp: new Date().toISOString() }; + } + + // Find or create category + let category = await this.categoryRepo.findOneBy({ name: dto.category }); + if (!category && dto.category) { + category = await this.categoryRepo.save(this.categoryRepo.create({ + name: dto.category, + displayOrder: await this.getNextCategoryOrder() + })); + } + + skill.name = dto.name; + skill.level = dto.level; + skill.categoryId = category?.id || skill.categoryId; + skill.yearsOfExperience = dto.yearsOfExperience ?? skill.yearsOfExperience; + + const saved = await this.skillRepo.save(skill); + return { success: true, message: 'Skill updated', data: saved, timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to update skill', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async deleteSkill(id: number): Promise> { + try { + await this.skillRepo.delete(id); + return { success: true, message: 'Skill deleted', timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to delete skill', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async reorderSkills(dto: ReorderSkillsDto): Promise> { + try { + for (let i = 0; i < dto.skillIds.length; i++) { + await this.skillRepo.update(dto.skillIds[i], { displayOrder: i }); + } + return { success: true, message: 'Skills reordered', timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to reorder skills', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + // ============================================ + // Skill Categories CRUD + // ============================================ + + async getSkillCategories(): Promise { + const categories = await this.categoryRepo.find({ + order: { displayOrder: 'ASC' }, + relations: ['skills'] + }); + return categories.map(c => ({ + id: c.id, + name: c.name, + description: c.description, + icon: c.icon, + color: c.color, + displayOrder: c.displayOrder + })); + } + + async createSkillCategory(dto: SkillCategoryDto): Promise> { + try { + const category = this.categoryRepo.create({ + name: dto.name, + description: dto.description, + icon: dto.icon, + color: dto.color || '#3b82f6', + displayOrder: dto.displayOrder ?? await this.getNextCategoryOrder() + }); + + const saved = await this.categoryRepo.save(category); + return { success: true, message: 'Category created', data: saved, timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to create category', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async updateSkillCategory(id: number, dto: SkillCategoryDto): Promise> { + try { + const category = await this.categoryRepo.findOneBy({ id }); + if (!category) { + return { success: false, message: 'Category not found', timestamp: new Date().toISOString() }; + } + + category.name = dto.name; + category.description = dto.description ?? category.description; + category.icon = dto.icon ?? category.icon; + category.color = dto.color ?? category.color; + + const saved = await this.categoryRepo.save(category); + return { success: true, message: 'Category updated', data: saved, timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to update category', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async deleteSkillCategory(id: number): Promise> { + try { + // Move skills to uncategorized first + await this.skillRepo.update({ categoryId: id }, { categoryId: undefined as any }); + await this.categoryRepo.delete(id); + return { success: true, message: 'Category deleted', timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to delete category', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + // ============================================ + // Language CRUD + // ============================================ + + async createLanguage(dto: LanguageDto): Promise> { + try { + // Validate DTO + const validation = validateLanguageDto(dto); + if (!validation.isValid) { + return { success: false, message: 'Validation failed', errors: validation.errors, timestamp: new Date().toISOString() }; + } + + const language = this.languageRepo.create({ + language: dto.language, + speaking: dto.speaking, + writing: dto.writing, + presenting: dto.presenting, + displayOrder: await this.getNextLanguageOrder() + }); + + const saved = await this.languageRepo.save(language); + return { success: true, message: 'Language created', data: saved, timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to create language', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async updateLanguage(id: number, dto: LanguageDto): Promise> { + try { + const language = await this.languageRepo.findOneBy({ id }); + if (!language) { + return { success: false, message: 'Language not found', timestamp: new Date().toISOString() }; + } + + // Validate DTO + const validation = validateLanguageDto(dto); + if (!validation.isValid) { + return { success: false, message: 'Validation failed', errors: validation.errors, timestamp: new Date().toISOString() }; + } + + language.language = dto.language; + language.speaking = dto.speaking; + language.writing = dto.writing; + language.presenting = dto.presenting; + + const saved = await this.languageRepo.save(language); + return { success: true, message: 'Language updated', data: saved, timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to update language', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async deleteLanguage(id: number): Promise> { + try { + await this.languageRepo.delete(id); + return { success: true, message: 'Language deleted', timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to delete language', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async reorderLanguages(languageIds: number[]): Promise> { + try { + for (let i = 0; i < languageIds.length; i++) { + await this.languageRepo.update(languageIds[i], { displayOrder: i }); + } + return { success: true, message: 'Languages reordered', timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to reorder languages', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + // ============================================ + // Hobby CRUD + // ============================================ + + async createHobby(dto: HobbyDto): Promise> { + try { + // Validate DTO + const validation = validateHobbyDto(dto); + if (!validation.isValid) { + return { success: false, message: 'Validation failed', errors: validation.errors, timestamp: new Date().toISOString() }; + } + + const hobby = this.hobbyRepo.create({ + name: dto.name, + description: dto.description, + icon: dto.icon, + displayOrder: await this.getNextHobbyOrder() + }); + + const saved = await this.hobbyRepo.save(hobby); + return { success: true, message: 'Hobby created', data: saved, timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to create hobby', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async updateHobby(id: number, dto: HobbyDto): Promise> { + try { + const hobby = await this.hobbyRepo.findOneBy({ id }); + if (!hobby) { + return { success: false, message: 'Hobby not found', timestamp: new Date().toISOString() }; + } + + // Validate DTO + const validation = validateHobbyDto(dto); + if (!validation.isValid) { + return { success: false, message: 'Validation failed', errors: validation.errors, timestamp: new Date().toISOString() }; + } + + hobby.name = dto.name; + hobby.description = dto.description ?? hobby.description; + hobby.icon = dto.icon ?? hobby.icon; + + const saved = await this.hobbyRepo.save(hobby); + return { success: true, message: 'Hobby updated', data: saved, timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to update hobby', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async deleteHobby(id: number): Promise> { + try { + await this.hobbyRepo.delete(id); + return { success: true, message: 'Hobby deleted', timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to delete hobby', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async reorderHobbies(hobbyIds: number[]): Promise> { + try { + for (let i = 0; i < hobbyIds.length; i++) { + await this.hobbyRepo.update(hobbyIds[i], { displayOrder: i }); + } + return { success: true, message: 'Hobbies reordered', timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to reorder hobbies', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + // ============================================ + // Business Domain CRUD + // ============================================ + + async createBusinessDomain(dto: BusinessDomainDto): Promise> { + try { + // Validate DTO + const validation = validateBusinessDomainDto(dto); + if (!validation.isValid) { + return { success: false, message: 'Validation failed', errors: validation.errors, timestamp: new Date().toISOString() }; + } + + const domain = this.businessDomainRepo.create({ + domain: dto.domain, + level: dto.level, + description: dto.description, + yearsOfExperience: dto.yearsOfExperience, + icon: dto.icon, + displayOrder: await this.getNextBusinessDomainOrder() + }); + + const saved = await this.businessDomainRepo.save(domain); + return { success: true, message: 'Business domain created', data: saved, timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to create business domain', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async updateBusinessDomain(id: number, dto: BusinessDomainDto): Promise> { + try { + const domain = await this.businessDomainRepo.findOneBy({ id }); + if (!domain) { + return { success: false, message: 'Business domain not found', timestamp: new Date().toISOString() }; + } + + // Validate DTO + const validation = validateBusinessDomainDto(dto); + if (!validation.isValid) { + return { success: false, message: 'Validation failed', errors: validation.errors, timestamp: new Date().toISOString() }; + } + + domain.domain = dto.domain; + domain.level = dto.level ?? domain.level; + domain.description = dto.description ?? domain.description; + domain.yearsOfExperience = dto.yearsOfExperience ?? domain.yearsOfExperience; + domain.icon = dto.icon ?? domain.icon; + + const saved = await this.businessDomainRepo.save(domain); + return { success: true, message: 'Business domain updated', data: saved, timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to update business domain', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async deleteBusinessDomain(id: number): Promise> { + try { + await this.businessDomainRepo.delete(id); + return { success: true, message: 'Business domain deleted', timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to delete business domain', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + async reorderBusinessDomains(domainIds: number[]): Promise> { + try { + for (let i = 0; i < domainIds.length; i++) { + await this.businessDomainRepo.update(domainIds[i], { displayOrder: i }); + } + return { success: true, message: 'Business domains reordered', timestamp: new Date().toISOString() }; + } catch (error) { + return { success: false, message: 'Failed to reorder business domains', errors: [(error as Error).message], timestamp: new Date().toISOString() }; + } + } + + // ============================================ + // Helper Methods + // ============================================ + + private async getNextExperienceOrder(): Promise { + const max = await this.experienceRepo.createQueryBuilder('e') + .select('MAX(e.displayOrder)', 'max') + .getRawOne(); + return (max?.max ?? -1) + 1; + } + + private async getNextSkillOrder(categoryId?: number): Promise { + const qb = this.skillRepo.createQueryBuilder('s') + .select('MAX(s.displayOrder)', 'max'); + if (categoryId) { + qb.where('s.categoryId = :categoryId', { categoryId }); + } + const max = await qb.getRawOne(); + return (max?.max ?? -1) + 1; + } + + private async getNextCategoryOrder(): Promise { + const max = await this.categoryRepo.createQueryBuilder('c') + .select('MAX(c.displayOrder)', 'max') + .getRawOne(); + return (max?.max ?? -1) + 1; + } + + private async getNextLanguageOrder(): Promise { + const max = await this.languageRepo.createQueryBuilder('l') + .select('MAX(l.displayOrder)', 'max') + .getRawOne(); + return (max?.max ?? -1) + 1; + } + + private async getNextHobbyOrder(): Promise { + const max = await this.hobbyRepo.createQueryBuilder('h') + .select('MAX(h.displayOrder)', 'max') + .getRawOne(); + return (max?.max ?? -1) + 1; + } + + private async getNextBusinessDomainOrder(): Promise { + const max = await this.businessDomainRepo.createQueryBuilder('d') + .select('MAX(d.displayOrder)', 'max') + .getRawOne(); + return (max?.max ?? -1) + 1; + } +} + + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-style.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-style.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..964badf4e874a284f6c619addb59a12396d43d7e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-style.service.ts @@ -0,0 +1,133 @@ +/** + * @fileoverview Admin Style Service + * Handles administrative operations for theme configuration. + * + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { StyleConfig } from '../database/entities/style-config.entity'; +import { AdminStyleConfigDto, CrudResponseDto } from '@arkalliance/startupcms-ai-share'; + +export class AdminStyleService { + private styleRepo = AppDataSource.getRepository(StyleConfig); + + async getActiveStyle(): Promise { + return this.styleRepo.findOne({ where: { isActive: true } }); + } + + async getAllStyles(): Promise { + return this.styleRepo.find({ order: { updatedAt: 'DESC' } }); + } + + async createStyle(dto: AdminStyleConfigDto): Promise> { + try { + // If setting as active, deactivate others + if (dto.isActive) { + await this.styleRepo.update({}, { isActive: false }); + } + + const style = this.styleRepo.create(dto); + const saved = await this.styleRepo.save(style); + return { + success: true, + message: 'Style configuration created successfully', + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to create style configuration', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + async updateStyle(id: number, dto: AdminStyleConfigDto): Promise> { + try { + const style = await this.styleRepo.findOne({ where: { id } }); + if (!style) { + return { + success: false, + message: 'Style configuration not found', + timestamp: new Date().toISOString() + }; + } + + // If setting as active, deactivate others + if (dto.isActive && !style.isActive) { + await this.styleRepo.update({}, { isActive: false }); + } + + this.styleRepo.merge(style, dto); + const saved = await this.styleRepo.save(style); + return { + success: true, + message: 'Style configuration updated successfully', + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to update style configuration', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + async deleteStyle(id: number): Promise> { + try { + const result = await this.styleRepo.delete(id); + if (result.affected === 0) { + return { + success: false, + message: 'Style configuration not found', + timestamp: new Date().toISOString() + }; + } + return { + success: true, + message: 'Style configuration deleted successfully', + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to delete style configuration', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + async activateStyle(id: number): Promise> { + try { + await this.styleRepo.update({}, { isActive: false }); + await this.styleRepo.update(id, { isActive: true }); + + const activeStyle = await this.styleRepo.findOne({ where: { id } }); + if (!activeStyle) throw new Error('Style not found after activation'); + + return { + success: true, + message: 'Style activated successfully', + data: activeStyle, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to activate style', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-theme.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-theme.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..f67914f9dc790b4bd4e87a0a8fa688f3ddbf7a21 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-theme.service.ts @@ -0,0 +1,258 @@ +/** + * @fileoverview Admin Theme Service + * Handles administrative operations for theme management. + * + * @module services/admin-theme + * @author Armand Richelet-Kleinberg + * + * @description + * Provides CRUD operations for theme management in the admin panel. + * Includes safeguards to prevent deletion of the default theme. + * + * @see {@link Theme} for entity definition + * @see {@link ThemeService} for public theme operations + */ + +import { AppDataSource } from '../config/database'; +import { Theme } from '../database/entities/theme.entity'; +import { CrudResponseDto } from '@arkalliance/startupcms-ai-share'; + +/** + * DTO for creating/updating themes + */ +export interface AdminThemeDto { + name: string; + slug: string; + description?: string; + cssContent: string; + previewColor?: string; + icon?: string; + isDefault?: boolean; + order?: number; + isActive?: boolean; +} + +/** + * Admin Theme Service + * + * @class AdminThemeService + * @description Provides administrative operations for theme management + */ +export class AdminThemeService { + private themeRepo = AppDataSource.getRepository(Theme); + + /** + * Get all themes with full data + */ + async getAllThemes(): Promise { + return this.themeRepo.find({ order: { order: 'ASC' } }); + } + + /** + * Get theme by ID + */ + async getThemeById(id: number): Promise { + return this.themeRepo.findOne({ where: { id } }); + } + + /** + * Create a new theme + */ + async createTheme(dto: AdminThemeDto): Promise> { + try { + // Check if slug already exists + const existing = await this.themeRepo.findOne({ where: { slug: dto.slug } }); + if (existing) { + return { + success: false, + message: `Theme with slug '${dto.slug}' already exists`, + timestamp: new Date().toISOString() + }; + } + + // If setting as default, unset current default + if (dto.isDefault) { + await this.themeRepo.update({}, { isDefault: false }); + } + + const theme = this.themeRepo.create({ + name: dto.name, + slug: dto.slug, + description: dto.description, + cssContent: dto.cssContent, + previewColor: dto.previewColor, + icon: dto.icon, + isDefault: dto.isDefault ?? false, + order: dto.order ?? 0, + isActive: dto.isActive ?? true + }); + + const saved = await this.themeRepo.save(theme); + return { + success: true, + message: 'Theme created successfully', + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to create theme', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Update an existing theme + */ + async updateTheme(id: number, dto: AdminThemeDto): Promise> { + try { + const theme = await this.themeRepo.findOne({ where: { id } }); + if (!theme) { + return { + success: false, + message: 'Theme not found', + timestamp: new Date().toISOString() + }; + } + + // Check slug uniqueness if changing + if (dto.slug && dto.slug !== theme.slug) { + const existing = await this.themeRepo.findOne({ where: { slug: dto.slug } }); + if (existing) { + return { + success: false, + message: `Theme with slug '${dto.slug}' already exists`, + timestamp: new Date().toISOString() + }; + } + } + + // If setting as default, unset current default + if (dto.isDefault && !theme.isDefault) { + await this.themeRepo.update({}, { isDefault: false }); + } + + this.themeRepo.merge(theme, dto); + const saved = await this.themeRepo.save(theme); + return { + success: true, + message: 'Theme updated successfully', + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to update theme', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Delete a theme + * + * @remarks Prevents deletion of the default theme + */ + async deleteTheme(id: number): Promise> { + try { + const theme = await this.themeRepo.findOne({ where: { id } }); + if (!theme) { + return { + success: false, + message: 'Theme not found', + timestamp: new Date().toISOString() + }; + } + + // Prevent deletion of default theme + if (theme.isDefault) { + return { + success: false, + message: 'Cannot delete the default theme. Set another theme as default first.', + timestamp: new Date().toISOString() + }; + } + + await this.themeRepo.delete(id); + return { + success: true, + message: 'Theme deleted successfully', + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to delete theme', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Set a theme as the default + */ + async setDefaultTheme(id: number): Promise> { + try { + const theme = await this.themeRepo.findOne({ where: { id } }); + if (!theme) { + return { + success: false, + message: 'Theme not found', + timestamp: new Date().toISOString() + }; + } + + // Unset current default + await this.themeRepo.update({}, { isDefault: false }); + + // Set new default + theme.isDefault = true; + const saved = await this.themeRepo.save(theme); + + return { + success: true, + message: `Theme '${theme.name}' set as default`, + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to set default theme', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Reorder themes + */ + async reorderThemes(themeIds: number[]): Promise> { + try { + for (let i = 0; i < themeIds.length; i++) { + await this.themeRepo.update(themeIds[i], { order: i }); + } + return { + success: true, + message: 'Themes reordered successfully', + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to reorder themes', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-widget.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-widget.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..5dbbc6c837d43d73f808e1e42b3070d0ff932dbd --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/admin-widget.service.ts @@ -0,0 +1,164 @@ +/** + * @fileoverview Admin Widget Service + * Handles administrative operations for widgets. + * + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { Widget } from '../database/entities/widget.entity'; +import { AdminWidgetDto, CrudResponseDto, ReorderWidgetsDto } from '@arkalliance/startupcms-ai-share'; + +/** + * Service for managing widgets in the admin panel. + */ +export class AdminWidgetService { + private widgetRepo = AppDataSource.getRepository(Widget); + + /** + * Get all widgets, optionally filtered by page/context. + * @param pageId - Optional page ID filter (maps to context in entity) + * @returns Array of widgets ordered by display order + */ + async getWidgets(pageId?: string): Promise { + const where = pageId ? { context: pageId } : {}; + return this.widgetRepo.find({ + where, + order: { order: 'ASC' } + }); + } + + /** + * Create a new widget. + * @param dto - Widget data + * @returns CRUD response with created widget + */ + async createWidget(dto: AdminWidgetDto): Promise> { + try { + const widget = this.widgetRepo.create({ + type: dto.type, + title: dto.title ?? '', + order: dto.order ?? 0, + config: dto.config, + // Map DTO pageId to entity context + context: dto.pageId + }); + const saved = await this.widgetRepo.save(widget); + return { + success: true, + message: 'Widget created successfully', + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to create widget', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Update an existing widget. + * @param id - Widget UUID + * @param dto - Updated widget data + * @returns CRUD response with updated widget + */ + async updateWidget(id: string, dto: AdminWidgetDto): Promise> { + try { + const widget = await this.widgetRepo.findOne({ where: { id } }); + if (!widget) { + return { + success: false, + message: 'Widget not found', + timestamp: new Date().toISOString() + }; + } + + // Update fields + if (dto.type !== undefined) widget.type = dto.type; + if (dto.title !== undefined) widget.title = dto.title; + if (dto.order !== undefined) widget.order = dto.order; + if (dto.config !== undefined) widget.config = dto.config; + // Map DTO pageId to entity context + if (dto.pageId !== undefined) widget.context = dto.pageId; + + const saved = await this.widgetRepo.save(widget); + return { + success: true, + message: 'Widget updated successfully', + data: saved, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to update widget', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Delete a widget. + * @param id - Widget UUID + * @returns CRUD response + */ + async deleteWidget(id: string): Promise> { + try { + const result = await this.widgetRepo.delete(id); + if (result.affected === 0) { + return { + success: false, + message: 'Widget not found', + timestamp: new Date().toISOString() + }; + } + return { + success: true, + message: 'Widget deleted successfully', + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to delete widget', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Reorder widgets by updating their order values. + * @param dto - Reorder request with widget IDs in new order + * @returns CRUD response + */ + async reorderWidgets(dto: ReorderWidgetsDto): Promise> { + try { + await AppDataSource.transaction(async transactionalEntityManager => { + for (let i = 0; i < dto.widgetIds.length; i++) { + await transactionalEntityManager.update(Widget, dto.widgetIds[i], { order: i }); + } + }); + return { + success: true, + message: 'Widgets reordered successfully', + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to reorder widgets', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/ai-profile.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/ai-profile.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..b53a3c9fd22a0d1c2ab9494ba51411513c3265c4 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/ai-profile.service.ts @@ -0,0 +1,385 @@ +/** + * @fileoverview AI Profile Service + * Orchestrates AI-assisted profile creation and updates. + * Integrates with PromptTemplateService and AiService for generation. + * + * @module services/ai-profile + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { User } from '../database/entities/user.entity'; +import { Collaborator } from '../database/entities/collaborator.entity'; +import { Experience } from '../database/entities/experience.entity'; +import { Skill } from '../database/entities/skill.entity'; +import { Language } from '../database/entities/language.entity'; +import { AiService } from './ai.service'; +import { promptTemplateService } from './prompt-template.service'; +import { DEFAULT_ORGANIZATION_ID } from '../database/seeds/seeders/organization.seeder'; +import bcrypt from 'bcryptjs'; +import { v4 as uuidv4 } from 'uuid'; + +/** + * Input for AI profile generation. + */ +export interface AiProfileInput { + firstName: string; + lastName: string; + function: string; + managerName: string; + description: string; + urls?: string; + avatarBase64?: string; + userId?: string; // For updates + promptName?: string; +} + +/** + * Parsed profile data from AI response. + */ +export interface ParsedProfileData { + firstName: string; + lastName: string; + email?: string; + position: string; + department?: string; + bio: string; + linkedinUrl?: string; + githubUrl?: string; + twitterUrl?: string; + reportsToName?: string; + experiences?: Array<{ + company: string; + position: string; + startDate: string; + endDate?: string; + description: string; + technologies: string[]; + }>; + skills?: Array<{ + name: string; + level: string; + yearsOfExperience: number; + }>; + languages?: Array<{ + language: string; + speaking: number; + writing: number; + }>; +} + +/** + * Result of AI profile generation. + */ +export interface AiProfileResult { + success: boolean; + userId?: string; + collaboratorId?: string; + message: string; + errors?: string[]; +} + +/** + * Service for AI-assisted profile generation. + */ +export class AiProfileService { + private aiService = new AiService(); + + /** + * Generate or update a profile from description using AI. + * + * @param input - Profile generation input data + * @returns Promise resolving to generation result + */ + async generateProfile(input: AiProfileInput): Promise { + try { + // 1. Get prompt template + const promptName = input.promptName || 'profile-generation'; + const template = await promptTemplateService.getByName(promptName); + + if (!template) { + return { + success: false, + message: `Prompt template '${promptName}' not found` + }; + } + + // 2. Format client prompt with input values + const clientPrompt = promptTemplateService.formatPrompt( + template.clientPromptTemplate, + { + firstName: input.firstName, + lastName: input.lastName, + function: input.function, + managerName: input.managerName, + description: input.description, + urls: input.urls || '' + } + ); + + // 3. Call AI service + const aiResponse = await this.aiService.generateCompletion({ + prompt: clientPrompt, + context: template.systemPrompt, + maxTokens: 3000 + }); + + if (aiResponse.error) { + return { + success: false, + message: `AI generation failed: ${aiResponse.error}` + }; + } + + // 4. Parse AI response as JSON + const profileData = this.parseAiResponse(aiResponse.result); + if (!profileData) { + return { + success: false, + message: 'Failed to parse AI response as valid JSON', + errors: ['AI output was not valid JSON'] + }; + } + + // 5. Validate required fields + const validationErrors = this.validateProfileData(profileData); + if (validationErrors.length > 0) { + return { + success: false, + message: 'Validation failed', + errors: validationErrors + }; + } + + // 6. Create or update profile in database + const result = await this.createOrUpdateProfile( + profileData, + input.avatarBase64, + input.userId + ); + + return result; + + } catch (error) { + console.error('AI Profile generation error:', error); + return { + success: false, + message: `Unexpected error: ${(error as Error).message}` + }; + } + } + + /** + * Parse AI response string as JSON. + * Handles potential markdown code blocks. + */ + private parseAiResponse(response: string): ParsedProfileData | null { + try { + // Remove markdown code blocks if present + let cleaned = response.trim(); + if (cleaned.startsWith('```json')) { + cleaned = cleaned.slice(7); + } else if (cleaned.startsWith('```')) { + cleaned = cleaned.slice(3); + } + if (cleaned.endsWith('```')) { + cleaned = cleaned.slice(0, -3); + } + cleaned = cleaned.trim(); + + return JSON.parse(cleaned); + } catch { + console.error('Failed to parse AI response:', response); + return null; + } + } + + /** + * Validate parsed profile data. + */ + private validateProfileData(data: ParsedProfileData): string[] { + const errors: string[] = []; + + if (!data.firstName) errors.push('firstName is required'); + if (!data.lastName) errors.push('lastName is required'); + if (!data.position) errors.push('position is required'); + if (!data.bio) errors.push('bio is required'); + + return errors; + } + + /** + * Create or update user and collaborator in database. + */ + private async createOrUpdateProfile( + data: ParsedProfileData, + avatarBase64?: string, + existingUserId?: string + ): Promise { + const queryRunner = AppDataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const userRepo = queryRunner.manager.getRepository(User); + const collaboratorRepo = queryRunner.manager.getRepository(Collaborator); + const experienceRepo = queryRunner.manager.getRepository(Experience); + const skillRepo = queryRunner.manager.getRepository(Skill); + const languageRepo = queryRunner.manager.getRepository(Language); + + let user: User; + let collaborator: Collaborator; + const collaboratorId = uuidv4(); + + if (existingUserId) { + // Update existing user + const existingUser = await userRepo.findOne({ + where: { id: existingUserId } + }); + if (!existingUser) { + throw new Error('User not found for update'); + } + user = existingUser; + } else { + // Create new user + const username = `${data.firstName.toLowerCase()}${data.lastName.toLowerCase()}`.replace(/\s/g, ''); + const hashedPassword = await bcrypt.hash('ArkAlliance2026!Secure', 10); + + user = userRepo.create({ + username, + email: data.email || `${username}@m2h.ai`, + passwordHash: hashedPassword, + firstName: data.firstName, + lastName: data.lastName, + title: data.position, + bio: data.bio, + linkedinUrl: data.linkedinUrl, + githubUrl: data.githubUrl, + twitterUrl: data.twitterUrl, + emailConfirmed: true, + isActive: true, + collaboratorId + }); + } + + // Update user fields from AI data + user.firstName = data.firstName; + user.lastName = data.lastName; + user.title = data.position; + user.bio = data.bio; + if (data.linkedinUrl) user.linkedinUrl = data.linkedinUrl; + if (data.githubUrl) user.githubUrl = data.githubUrl; + if (data.twitterUrl) user.twitterUrl = data.twitterUrl; + + await userRepo.save(user); + + // Find or create collaborator + if (existingUserId) { + const existingCollab = await collaboratorRepo.findOne({ + where: { userId: existingUserId } + }); + collaborator = existingCollab || collaboratorRepo.create({ + id: collaboratorId, + userId: user.id, + organizationId: DEFAULT_ORGANIZATION_ID + }); + } else { + collaborator = collaboratorRepo.create({ + id: collaboratorId, + userId: user.id, + organizationId: DEFAULT_ORGANIZATION_ID + }); + } + + // Update collaborator fields + collaborator.firstName = data.firstName; + collaborator.lastName = data.lastName; + collaborator.email = data.email || user.email; + collaborator.position = data.position; + collaborator.department = data.department; + collaborator.bio = data.bio; + collaborator.linkedinUrl = data.linkedinUrl; + collaborator.githubUrl = data.githubUrl; + collaborator.twitterUrl = data.twitterUrl; + if (avatarBase64) collaborator.avatarUrl = avatarBase64; + collaborator.isActive = true; + + // Lookup manager by name if provided + if (data.reportsToName) { + const nameParts = data.reportsToName.split(' '); + const manager = await collaboratorRepo.findOne({ + where: { + firstName: nameParts[0], + lastName: nameParts.slice(1).join(' ') || nameParts[0] + } + }); + if (manager) { + collaborator.reportsToId = manager.id; + } + } + + await collaboratorRepo.save(collaborator); + + // Create experiences if provided + if (data.experiences && data.experiences.length > 0) { + for (const exp of data.experiences) { + const experience = experienceRepo.create({ + userId: user.id, + company: exp.company, + position: exp.position, + startDate: new Date(exp.startDate), + endDate: exp.endDate ? new Date(exp.endDate) : undefined, + description: exp.description, + technologies: exp.technologies + }); + await experienceRepo.save(experience); + } + } + + // Create skills if provided + if (data.skills && data.skills.length > 0) { + for (const skill of data.skills) { + const skillEntity = skillRepo.create({ + userId: user.id, + name: skill.name, + level: skill.level, + yearsOfExperience: skill.yearsOfExperience + }); + await skillRepo.save(skillEntity); + } + } + + // Create languages if provided + if (data.languages && data.languages.length > 0) { + for (const lang of data.languages) { + const langEntity = languageRepo.create({ + userId: user.id, + language: lang.language, + speaking: lang.speaking, + writing: lang.writing, + presenting: lang.speaking // Use speaking level + }); + await languageRepo.save(langEntity); + } + } + + await queryRunner.commitTransaction(); + + return { + success: true, + userId: user.id, + collaboratorId: collaborator.id, + message: `Profile ${existingUserId ? 'updated' : 'created'} successfully` + }; + + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } +} + +// Singleton instance +export const aiProfileService = new AiProfileService(); diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/ai.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/ai.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f0e0a37a7afb25419f5ea3bcddd145a8c191b57 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/ai.service.ts @@ -0,0 +1,569 @@ +/** + * @fileoverview AI Service + * Multi-provider AI integration for text generation and CV assistance. + * + * @module services/ai + * @author Armand Richelet-Kleinberg + * @since 1.0.0 + * + * @description + * Provides a unified interface for AI completions across multiple providers: + * - OpenAI (GPT-4, GPT-3.5) + * - Anthropic (Claude) + * - Google (Gemini) + * - Custom/self-hosted endpoints + * + * Features: + * - Encrypted API key storage + * - Connection testing + * - CV-specific operations (skill organization, text improvement) + * + * @example + * // Basic usage + * const aiService = new AiService(); + * + * // Get current settings + * const settings = await aiService.getSettings(); + * + * // Generate completion + * const response = await aiService.generateCompletion({ + * prompt: 'Summarize this text...', + * context: 'You are a professional editor.' + * }); + * + * // CV assistance + * const improved = await aiService.improveDescription( + * 'Worked on projects', + * 'work experience' + * ); + * + * @see {@link AiSettings} for configuration entity + * @see {@link encrypt} for API key encryption + */ + +import { AiSettingsRepository } from '../database/repositories/ai-settings.repository'; +import { AiSettings } from '../database/entities/ai-settings.entity'; +import { encrypt, decrypt, maskApiKey } from '../utils/encryption'; +import { CrudResponseDto } from '@arkalliance/startupcms-ai-share'; + +/** + * Request object for AI completion. + * + * @interface AiCompletionRequest + */ +interface AiCompletionRequest { + /** The prompt/question to send to the AI */ + prompt: string; + /** Optional system context/instructions */ + context?: string; + /** Override default max tokens */ + maxTokens?: number; +} + +/** + * Response object from AI completion. + * + * @interface AiCompletionResponse + */ +interface AiCompletionResponse { + /** Generated text response */ + result: string; + /** Token usage statistics (if available) */ + usage?: { + promptTokens: number; + completionTokens: number; + totalTokens: number; + }; + /** Error message if request failed */ + error?: string; +} + +/** + * Data transfer object for AI settings. + * + * @interface AiSettingsDto + */ +interface AiSettingsDto { + /** Settings record ID */ + id?: number; + /** AI provider identifier */ + provider: string; + /** Custom API URL (optional) */ + apiUrl?: string; + /** API key (plaintext, for updates only) */ + apiKey?: string; + /** Masked API key for display */ + apiKeyMasked?: string; + /** Model identifier */ + model: string; + /** Temperature setting (0-2) */ + temperature: number; + /** Maximum response tokens */ + maxTokens: number; + /** Whether AI is enabled */ + isActive: boolean; +} + +/** + * AI Service class for multi-provider AI integration. + * + * @class AiService + * @description Provides AI completion capabilities with secure API key management + * and CV-specific assistance features. + * + * @remarks + * - API keys are stored encrypted in the database + * - Supports OpenAI, Anthropic, Google, and custom providers + * - Includes CV-specific helpers for skill organization and text improvement + */ +export class AiService { + /** @private Repository for AI settings */ + private settingsRepo = new AiSettingsRepository(); + + /** + * Default API URLs for each provider. + * @private + */ + private readonly providerUrls: Record = { + 'openai': 'https://api.openai.com/v1/chat/completions', + 'anthropic': 'https://api.anthropic.com/v1/messages', + 'google': 'https://generativelanguage.googleapis.com/v1beta/models', + }; + + /** + * Available models for each provider. + * @private + */ + private readonly providerModels: Record = { + 'openai': ['gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo'], + 'anthropic': ['claude-3-opus', 'claude-3-sonnet', 'claude-3-haiku'], + 'google': ['gemini-pro', 'gemini-pro-vision'], + 'custom': [] + }; + + /** + * Get current AI settings. + * + * @returns {Promise} Settings DTO with masked API key, or null if not configured + * + * @description + * Retrieves the current AI configuration with the API key masked for security. + * + * @example + * const settings = await aiService.getSettings(); + * if (settings) { + * console.log(`Using ${settings.provider} - ${settings.model}`); + * console.log(`API Key: ${settings.apiKeyMasked}`); // 'sk-a...xyz' + * } + */ + async getSettings(): Promise { + const settings = await this.settingsRepo.getFirst(); + if (!settings) return null; + + return { + id: settings.id, + provider: settings.provider, + apiUrl: settings.apiUrl, + apiKeyMasked: maskApiKey(decrypt(settings.apiKeyEncrypted || '')), + model: settings.model, + temperature: Number(settings.temperature), + maxTokens: settings.maxTokens, + isActive: settings.isActive + }; + } + + /** + * Update AI settings. + * + * @param dto - Settings data to update + * @returns {Promise>} CRUD response with updated settings + * + * @description + * Updates AI configuration. Creates new settings if none exist. + * API key is only updated if a new (non-masked) value is provided. + * + * @example + * const result = await aiService.updateSettings({ + * provider: 'openai', + * apiKey: 'sk-new-key...', + * model: 'gpt-4', + * temperature: 0.7, + * maxTokens: 2000, + * isActive: true + * }); + * + * @remarks + * - Masked API keys (containing '...') are ignored + * - New API keys are encrypted before storage + */ + async updateSettings(dto: AiSettingsDto): Promise> { + try { + let settings = await this.settingsRepo.getFirst(); + + const updateData: Partial = { + provider: dto.provider, + apiUrl: dto.apiUrl || this.providerUrls[dto.provider], + model: dto.model, + temperature: dto.temperature, + maxTokens: dto.maxTokens, + isActive: dto.isActive + }; + + // Only update API key if a new one is provided (not masked) + if (dto.apiKey && !dto.apiKey.includes('...')) { + updateData.apiKeyEncrypted = encrypt(dto.apiKey); + } + + if (!settings) { + settings = await this.settingsRepo.create(updateData as AiSettings); + } else { + await this.settingsRepo.update(settings.id, updateData); + settings = await this.settingsRepo.findById(settings.id) || settings; + } + + return { + success: true, + message: 'AI settings updated successfully', + data: settings, + timestamp: new Date().toISOString() + }; + } catch (error) { + return { + success: false, + message: 'Failed to update AI settings', + errors: [(error as Error).message], + timestamp: new Date().toISOString() + }; + } + } + + /** + * Get available models for a provider. + * + * @param provider - Provider identifier ('openai', 'anthropic', 'google') + * @returns {Promise} Array of model identifiers + * + * @example + * const models = await aiService.getProviderModels('openai'); + * // ['gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo'] + */ + async getProviderModels(provider: string): Promise { + return this.providerModels[provider] || []; + } + + /** + * Test the AI connection with current settings. + * + * @returns {Promise<{ success: boolean; message: string }>} Connection test result + * + * @description + * Sends a simple test prompt to verify API connectivity and authentication. + * + * @example + * const result = await aiService.testConnection(); + * if (result.success) { + * console.log('AI provider connected successfully'); + * } else { + * console.error('Connection failed:', result.message); + * } + */ + async testConnection(): Promise<{ success: boolean; message: string }> { + const settings = await this.settingsRepo.getFirst(); + if (!settings) { + return { success: false, message: 'No AI settings configured' }; + } + + try { + const apiKey = decrypt(settings.apiKeyEncrypted || ''); + if (!apiKey) { + return { success: false, message: 'API key not configured' }; + } + + const response = await this.generateCompletion({ + prompt: 'Say "connected" if you receive this.', + maxTokens: 10 + }); + + if (response.error) { + return { success: false, message: response.error }; + } + + return { success: true, message: 'Connection successful' }; + } catch (error) { + return { success: false, message: (error as Error).message }; + } + } + + /** + * Generate an AI completion. + * + * @param request - Completion request with prompt and options + * @returns {Promise} Generated response or error + * + * @description + * Sends a prompt to the configured AI provider and returns the response. + * + * @example + * const response = await aiService.generateCompletion({ + * prompt: 'Explain TypeScript generics', + * context: 'You are a programming tutor.', + * maxTokens: 500 + * }); + * + * if (response.error) { + * console.error(response.error); + * } else { + * console.log(response.result); + * } + */ + async generateCompletion(request: AiCompletionRequest): Promise { + const settings = await this.settingsRepo.getFirst(); + if (!settings || !settings.isActive) { + return { result: '', error: 'AI service not configured or disabled' }; + } + + const apiKey = decrypt(settings.apiKeyEncrypted || ''); + if (!apiKey) { + return { result: '', error: 'API key not configured' }; + } + + try { + const response = await this.callProvider(settings, apiKey, request); + return response; + } catch (error) { + return { result: '', error: (error as Error).message }; + } + } + + /** + * Route request to appropriate provider. + * @private + */ + private async callProvider( + settings: AiSettings, + apiKey: string, + request: AiCompletionRequest + ): Promise { + const url = settings.apiUrl || this.providerUrls[settings.provider]; + const maxTokens = request.maxTokens || settings.maxTokens; + + switch (settings.provider) { + case 'openai': + return this.callOpenAI(url, apiKey, settings, request, maxTokens); + case 'anthropic': + return this.callAnthropic(url, apiKey, settings, request, maxTokens); + case 'google': + return this.callGoogle(url, apiKey, settings, request, maxTokens); + default: + return this.callOpenAI(url, apiKey, settings, request, maxTokens); + } + } + + /** + * Call OpenAI API. + * @private + */ + private async callOpenAI( + url: string, + apiKey: string, + settings: AiSettings, + request: AiCompletionRequest, + maxTokens: number + ): Promise { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify({ + model: settings.model, + messages: [ + { role: 'system', content: request.context || 'You are a helpful assistant.' }, + { role: 'user', content: request.prompt } + ], + temperature: Number(settings.temperature), + max_tokens: maxTokens + }) + }); + + const data = await response.json(); + if (!response.ok) { + return { result: '', error: data.error?.message || 'API request failed' }; + } + + return { + result: data.choices?.[0]?.message?.content || '', + usage: data.usage ? { + promptTokens: data.usage.prompt_tokens, + completionTokens: data.usage.completion_tokens, + totalTokens: data.usage.total_tokens + } : undefined + }; + } + + /** + * Call Anthropic API. + * @private + */ + private async callAnthropic( + url: string, + apiKey: string, + settings: AiSettings, + request: AiCompletionRequest, + maxTokens: number + ): Promise { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2024-01-01' + }, + body: JSON.stringify({ + model: settings.model, + system: request.context || 'You are a helpful assistant.', + messages: [{ role: 'user', content: request.prompt }], + temperature: Number(settings.temperature), + max_tokens: maxTokens + }) + }); + + const data = await response.json(); + if (!response.ok) { + return { result: '', error: data.error?.message || 'API request failed' }; + } + + return { + result: data.content?.[0]?.text || '', + usage: data.usage ? { + promptTokens: data.usage.input_tokens, + completionTokens: data.usage.output_tokens, + totalTokens: data.usage.input_tokens + data.usage.output_tokens + } : undefined + }; + } + + /** + * Call Google Gemini API. + * @private + */ + private async callGoogle( + url: string, + apiKey: string, + settings: AiSettings, + request: AiCompletionRequest, + maxTokens: number + ): Promise { + const fullUrl = `${url}/${settings.model}:generateContent?key=${apiKey}`; + + const response = await fetch(fullUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contents: [{ parts: [{ text: request.prompt }] }], + generationConfig: { + temperature: Number(settings.temperature), + maxOutputTokens: maxTokens + } + }) + }); + + const data = await response.json(); + if (!response.ok) { + return { result: '', error: data.error?.message || 'API request failed' }; + } + + return { + result: data.candidates?.[0]?.content?.parts?.[0]?.text || '' + }; + } + + // ============================================ + // AI-Assisted CV Operations + // ============================================ + + /** + * Use AI to organize skills into categories. + * + * @param skills - Array of skills with id and name + * @returns {Promise<{ categories: Array, suggestions: string[] }>} Organized categories and suggestions + * + * @description + * Analyzes skills using AI and groups them into logical categories. + * Also provides suggestions for missing skills or improvements. + * + * @example + * const result = await aiService.organizeSkills([ + * { id: 1, name: 'TypeScript' }, + * { id: 2, name: 'React' }, + * { id: 3, name: 'PostgreSQL' } + * ]); + * // { categories: [{ name: 'Frontend', skills: [1, 2] }], suggestions: [...] } + */ + async organizeSkills(skills: Array<{ id: number; name: string; level?: string }>): Promise<{ + categories: Array<{ name: string; skills: number[] }>; + suggestions: string[]; + }> { + const prompt = `Analyze these skills and organize them into logical categories. +Skills: ${skills.map(s => s.name).join(', ')} + +Respond in JSON format: +{ + "categories": [ + { "name": "Category Name", "skillNames": ["skill1", "skill2"] } + ], + "suggestions": ["Any missing skills or improvements suggested"] +}`; + + const response = await this.generateCompletion({ + prompt, + context: 'You are a professional CV and HR expert organizing technical skills.' + }); + + try { + const parsed = JSON.parse(response.result); + const categories = parsed.categories.map((cat: any) => ({ + name: cat.name, + skills: cat.skillNames.map((name: string) => + skills.find(s => s.name.toLowerCase() === name.toLowerCase())?.id + ).filter(Boolean) + })); + + return { categories, suggestions: parsed.suggestions || [] }; + } catch { + return { categories: [], suggestions: ['AI parsing failed'] }; + } + } + + /** + * Use AI to improve a text description. + * + * @param text - Original text to improve + * @param context - Context for improvement (default: 'professional CV') + * @returns {Promise} Improved text + * + * @description + * Enhances text to be more professional, impactful, and concise. + * Preserves the original meaning while improving language. + * + * @example + * const improved = await aiService.improveDescription( + * 'I worked on many projects and did coding.', + * 'work experience' + * ); + * // 'Led development of multiple software projects, implementing...' + */ + async improveDescription(text: string, context: string = 'professional CV'): Promise { + const prompt = `Improve this ${context} description to be more professional, impactful, and concise. +Keep the same meaning but enhance the language. Return only the improved text, no explanations. + +Original: ${text}`; + + const response = await this.generateCompletion({ prompt }); + return response.result || text; + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/audit-log.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/audit-log.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb8877af974e4a45810ed59da1302c1237be86fe --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/audit-log.service.ts @@ -0,0 +1,170 @@ +/** + * @fileoverview Audit Log Service + * Business logic for security event logging with resilience. + * + * @author Armand Richelet-Kleinberg + */ + +import { AuditAction, AuditLogDto, AuditLogQueryDto, AuditLogListResponseDto } from '@arkalliance/startupcms-ai-share'; +import { AuditLogRepository, AuditLogQueryOptions } from '../database/repositories/audit-log.repository'; +import { AuditLog } from '../database/entities/audit-log.entity'; + +/** + * Service for audit logging operations. + * Implements resilience patterns to ensure security events are not lost. + */ +export class AuditLogService { + private repository: AuditLogRepository; + + // Resilience tracking + private consecutiveFailures = 0; + private readonly MAX_CONSECUTIVE_FAILURES = 5; + private readonly MAX_RETRIES = 3; + private lastFailureAlertTime?: Date; + + constructor() { + this.repository = new AuditLogRepository(); + } + + /** + * Log a security event with retry logic and failure tracking. + * Never throws errors - audit logging should not break application flow. + */ + async log(entry: { + userId?: string; + action: AuditAction; + ipAddress?: string; + userAgent?: string; + success?: boolean; + details?: Record; + }): Promise { + for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) { + try { + await this.repository.log(entry); + // Reset failure counter on success + this.consecutiveFailures = 0; + return; + } catch (error) { + const isLastAttempt = attempt === this.MAX_RETRIES; + + if (isLastAttempt) { + this.consecutiveFailures++; + console.error( + `[AUDIT] Failed to write log after ${this.MAX_RETRIES} attempts:`, + { action: entry.action, userId: entry.userId, error } + ); + + // Alert on persistent failures (no more than once per minute) + this.checkAndAlertOnPersistentFailures(); + } else { + // Exponential backoff before retry + await this.delay(Math.pow(2, attempt - 1) * 100); + } + } + } + } + + /** + * Check if we should alert about persistent audit log failures. + */ + private checkAndAlertOnPersistentFailures(): void { + if (this.consecutiveFailures >= this.MAX_CONSECUTIVE_FAILURES) { + const now = new Date(); + const shouldAlert = !this.lastFailureAlertTime || + (now.getTime() - this.lastFailureAlertTime.getTime()) > 60000; // 1 minute + + if (shouldAlert) { + this.lastFailureAlertTime = now; + console.error( + `[AUDIT] CRITICAL: Audit logging has failed ${this.consecutiveFailures} consecutive times! ` + + `Security events may be lost. Investigate database connectivity.` + ); + // In production, this could trigger: + // - Email alert to security team + // - PagerDuty/OpsGenie alert + // - Fallback to file-based logging + } + } + } + + /** + * Get current failure status for monitoring. + */ + getHealthStatus(): { healthy: boolean; consecutiveFailures: number } { + return { + healthy: this.consecutiveFailures < this.MAX_CONSECUTIVE_FAILURES, + consecutiveFailures: this.consecutiveFailures + }; + } + + /** + * Helper for exponential backoff. + */ + private delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /** + * Query audit logs with pagination and filters. + */ + async query(params: AuditLogQueryDto): Promise { + const options: AuditLogQueryOptions = { + userId: params.userId, + actions: params.actions, + startDate: params.startDate ? new Date(params.startDate) : undefined, + endDate: params.endDate ? new Date(params.endDate) : undefined, + success: params.success, + ipAddress: params.ipAddress, + page: params.page || 1, + limit: Math.min(params.limit || 50, 100) // Cap at 100 + }; + + const { logs, total } = await this.repository.query(options); + const limit = options.limit!; + const page = options.page!; + + return { + logs: logs.map(log => this.toDto(log)), + total, + page, + limit, + totalPages: Math.ceil(total / limit) + }; + } + + /** + * Get recent login history for a user. + */ + async getLoginHistory(userId: string, limit = 10): Promise { + const logs = await this.repository.getRecentLoginAttempts(userId, limit); + return logs.map(log => this.toDto(log)); + } + + /** + * Check if account should be locked due to failed logins. + * @returns true if more than 5 failed attempts in last 15 minutes + */ + async shouldLockAccount(userId: string): Promise { + const failedCount = await this.repository.getRecentFailedLoginCount(userId, 15); + return failedCount >= 5; + } + + /** + * Convert entity to DTO. + */ + private toDto(log: AuditLog): AuditLogDto { + return { + id: log.id, + userId: log.userId, + username: log.user?.username, + action: log.action, + ipAddress: log.ipAddress, + userAgent: log.userAgent, + success: log.success, + details: log.details, + createdAt: log.createdAt.toISOString() + }; + } +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/auth.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/auth.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f2648fed7e8c39f0de414743f6da242b96e1b2d --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/auth.service.ts @@ -0,0 +1,215 @@ +/** + * @fileoverview Authentication Service + * Handles user authentication, JWT token management, and password operations. + * Core security service for the Ark.Alliance.StartupCms.Ai application. + * + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { User } from '../database/entities/user.entity'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; + +/** + * Service for user authentication and authorization. + * + * @remarks + * - Uses JWT tokens for stateless authentication + * - Passwords are hashed using bcrypt with salt + * - Supports multi-role via UserRole junction table + * + * @example + * ```typescript + * const authService = new AuthService(); + * const result = await authService.login('admin', 'password'); + * if (result) { + * console.log('Token:', result.token); + * } + * ``` + */ +export class AuthService { + private userRepository = AppDataSource.getRepository(User); + + /** JWT secret key from environment or default (change in production!) */ + private readonly JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; + + /** Token expiration time */ + private readonly TOKEN_EXPIRY = '24h'; + + /** + * Authenticate user and generate JWT token. + * + * @param username - User's username + * @param password - User's plaintext password + * @returns Token and user data if successful, null if authentication fails + * + * @example + * ```typescript + * const result = await authService.login('admin', 'Admin1234'); + * if (result) { + * res.json({ token: result.token }); + * } + * ``` + */ + async login(username: string, password: string): Promise<{ token: string; user: User } | null> { + // Explicitly load userRoles for role-based JWT claims + const user = await this.userRepository.findOne({ + where: { username }, + relations: ['userRoles'] + }); + + if (!user) { + return null; + } + + const isValidPassword = await bcrypt.compare(password, user.passwordHash); + if (!isValidPassword) { + return null; + } + + // Update last login timestamp + user.lastLogin = new Date(); + await this.userRepository.save(user); + + // Generate JWT with user info and roles + const token = jwt.sign( + { id: user.id, username: user.username, role: user.roleLegacy, roles: user.roles }, + this.JWT_SECRET, + { expiresIn: this.TOKEN_EXPIRY } + ); + + return { token, user }; + } + + /** + * Register a new user account. + * + * @param username - Desired username (must be unique) + * @param password - Plaintext password (will be hashed) + * @returns The newly created user entity + * @throws Error if username already exists + * + * @example + * ```typescript + * try { + * const user = await authService.register('newuser', 'SecurePass123'); + * console.log('Registered:', user.username); + * } catch (error) { + * console.error('Registration failed:', error.message); + * } + * ``` + */ + async register(username: string, password: string): Promise { + const existingUser = await this.userRepository.findOne({ where: { username } }); + if (existingUser) { + throw new Error('Username already exists'); + } + + const salt = await bcrypt.genSalt(10); + const passwordHash = await bcrypt.hash(password, salt); + + const user = this.userRepository.create({ + username, + passwordHash, + roleLegacy: 'admin' + }); + + const savedUser = await this.userRepository.save(user); + return savedUser as User; + } + + /** + * Validate a JWT token. + * + * @param token - JWT token string to validate + * @returns Decoded token payload if valid, null if invalid or expired + * + * @example + * ```typescript + * const decoded = await authService.validateToken(token); + * if (decoded) { + * console.log('User ID:', decoded.id); + * } + * ``` + */ + async validateToken(token: string): Promise { + try { + return jwt.verify(token, this.JWT_SECRET); + } catch (error) { + return null; + } + } + + /** + * Change user's password after validating current password. + * + * @param userId - User's UUID + * @param oldPassword - Current password for verification + * @param newPassword - New password to set + * @returns true if password changed, false if old password is incorrect + * @throws Error if user not found + * + * @example + * ```typescript + * const success = await authService.changePassword(userId, 'oldPass', 'newPass'); + * if (!success) { + * res.status(401).json({ message: 'Current password incorrect' }); + * } + * ``` + */ + async changePassword(userId: string, oldPassword: string, newPassword: string): Promise { + const user = await this.userRepository.findOne({ where: { id: userId } }); + if (!user) { + throw new Error('User not found'); + } + + const isValidPassword = await bcrypt.compare(oldPassword, user.passwordHash); + if (!isValidPassword) { + return false; + } + + const salt = await bcrypt.genSalt(10); + const passwordHash = await bcrypt.hash(newPassword, salt); + + user.passwordHash = passwordHash; + await this.userRepository.save(user); + + return true; + } + + /** + * Seed or update the CEO/admin user at application startup. + * Uses ADMIN_KEY_CMS environment variable or defaults to 'ArkAlliance2026!Secure'. + * + * @remarks + * - Creates CEO user (Bryan) if not exists + * - Updates password if CEO already exists + * - Called during server initialization + * + * @example + * ```typescript + * // In server startup + * const authService = new AuthService(); + * await authService.seedAdminUser(); + * ``` + */ + async seedAdminUser(): Promise { + const ceoUser = await this.userRepository.findOne({ where: { username: 'bryanwiedmersoler' } }); + const adminPassword = process.env.ADMIN_KEY_CMS || 'ArkAlliance2026!Secure'; + + if (!ceoUser) { + console.log('Seeding initial CEO user (Bryan Wiedmer Soler)...'); + await this.register('bryanwiedmersoler', adminPassword); + console.log('CEO user seeded.'); + } else { + console.log('CEO user exists. Updating password to configured value...'); + const salt = await bcrypt.genSalt(10); + const passwordHash = await bcrypt.hash(adminPassword, salt); + ceoUser.passwordHash = passwordHash; + await this.userRepository.save(ceoUser); + console.log('CEO password updated.'); + } + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/collaborator.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/collaborator.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..7691dd27e753aa7d7611abe51d83ec14fc8f6b7c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/collaborator.service.ts @@ -0,0 +1,221 @@ +/** + * @fileoverview Collaborator Service + * Business logic for team member management with hierarchy support. + * + * @author Armand Richelet-Kleinberg + */ + +import { + CollaboratorDto, + CollaboratorSummaryDto, + CollaboratorWithReportsDto, + OrgChartDto, + OrgChartNodeDto, + CreateCollaboratorDto, + UpdateCollaboratorDto +} from '@arkalliance/startupcms-ai-share'; +import { CollaboratorRepository } from '../database/repositories/collaborator.repository'; +import { OrganizationRepository } from '../database/repositories/organization.repository'; +import { Collaborator } from '../database/entities/collaborator.entity'; + +/** + * Service for collaborator operations. + */ +export class CollaboratorService { + private repository: CollaboratorRepository; + private orgRepository: OrganizationRepository; + + constructor() { + this.repository = new CollaboratorRepository(); + this.orgRepository = new OrganizationRepository(); + } + + /** + * Get collaborator by ID. + */ + async getById(id: string): Promise { + const collaborator = await this.repository.findById(id); + return collaborator ? this.toDto(collaborator) : null; + } + + /** + * Get collaborator with direct reports and resume data. + */ + async getWithReports(id: string): Promise { + const collaborator = await this.repository.findById(id); + if (!collaborator) return null; + + const directReports = await this.repository.findDirectReports(id); + + // Fetch resume data from linked user if exists + let resumeData: any = {}; + if (collaborator.userId) { + const { AppDataSource } = await import('../config/database'); + const userRepo = AppDataSource.getRepository((await import('../database/entities/user.entity')).User); + + const user = await userRepo.findOne({ + where: { id: collaborator.userId }, + relations: ['experiences', 'skills', 'educations', 'languages', 'hobbies', 'businessDomains'] + }); + + if (user) { + resumeData = { + experiences: user.experiences || [], + skills: user.skills || [], + educations: user.educations || [], + languages: user.languages || [], + hobbies: user.hobbies || [], + businessDomains: user.businessDomains || [] + }; + } + } + + return { + ...this.toDto(collaborator), + reportsTo: collaborator.reportsTo ? this.toSummaryDto(collaborator.reportsTo) : undefined, + directReports: directReports.map(c => this.toSummaryDto(c)), + directReportsCount: directReports.length, + ...resumeData // Include resume data in response + }; + } + + /** + * Get all collaborators for an organization. + */ + async getByOrganization(organizationId: string): Promise { + const collaborators = await this.repository.findByOrganization(organizationId); + return collaborators.map(c => this.toDto(c)); + } + + /** + * Get the default (first) organization. + * Used for public endpoints that don't require organizationId. + */ + async getDefaultOrganization(): Promise<{ id: string; name: string } | null> { + const org = await this.orgRepository.getDefault(); + return org ? { id: org.id, name: org.name } : null; + } + + /** + * Get organization chart as tree structure. + * Optimized to O(n) using lookup map instead of O(n²) filtering. + */ + async getOrgChart(organizationId: string): Promise { + const org = await this.orgRepository.findById(organizationId); + if (!org) return null; + + const rootCollaborators = await this.repository.findRootCollaborators(organizationId); + const allCollaborators = await this.repository.findByOrganization(organizationId); + + // Build lookup map for O(1) children access + const childrenMap = new Map(); + allCollaborators.forEach(c => { + const parentId = c.reportsToId || '__ROOT__'; + if (!childrenMap.has(parentId)) { + childrenMap.set(parentId, []); + } + childrenMap.get(parentId)!.push(c); + }); + + const buildNode = (collaborator: Collaborator): OrgChartNodeDto => { + const children = childrenMap.get(collaborator.id) || []; + return { + id: collaborator.id, + name: `${collaborator.firstName} ${collaborator.lastName}`, + position: collaborator.position, + department: collaborator.department, + avatarUrl: collaborator.avatarUrl, + email: collaborator.email, + children: children.map(c => buildNode(c)) + }; + }; + + return { + organizationId: org.id, + organizationName: org.name, + rootNodes: rootCollaborators.map(c => buildNode(c)), + totalCount: allCollaborators.length + }; + } + + /** + * Create a new collaborator. + */ + async create(data: CreateCollaboratorDto): Promise { + const collaborator = await this.repository.create({ + ...data, + hireDate: data.hireDate ? new Date(data.hireDate) : undefined + }); + return this.toDto(collaborator); + } + + /** + * Update a collaborator. + */ + async update(id: string, data: UpdateCollaboratorDto): Promise { + const collaborator = await this.repository.update(id, { + ...data, + hireDate: data.hireDate ? new Date(data.hireDate) : undefined + }); + return collaborator ? this.toDto(collaborator) : null; + } + + /** + * Update collaborator's manager (hierarchy). + */ + async updateHierarchy(id: string, reportsToId: string | null): Promise { + return this.repository.updateReportsTo(id, reportsToId); + } + + /** + * Convert entity to full DTO. + */ + private toDto(collaborator: Collaborator): CollaboratorDto { + return { + id: collaborator.id, + firstName: collaborator.firstName, + lastName: collaborator.lastName, + fullName: `${collaborator.firstName} ${collaborator.lastName}`, + email: collaborator.email, + position: collaborator.position, + department: collaborator.department, + bio: collaborator.bio, + avatarUrl: collaborator.avatarUrl, + linkedinUrl: collaborator.linkedinUrl, + githubUrl: collaborator.githubUrl, + twitterUrl: collaborator.twitterUrl, + hireDate: collaborator.hireDate instanceof Date + ? collaborator.hireDate.toISOString() + : collaborator.hireDate, + isActive: collaborator.isActive, + organizationId: collaborator.organizationId, + reportsToId: collaborator.reportsToId, + userId: collaborator.userId, + createdAt: collaborator.createdAt instanceof Date + ? collaborator.createdAt.toISOString() + : collaborator.createdAt, + updatedAt: collaborator.updatedAt instanceof Date + ? collaborator.updatedAt.toISOString() + : collaborator.updatedAt + }; + } + + /** + * Convert entity to summary DTO. + */ + private toSummaryDto(collaborator: Collaborator): CollaboratorSummaryDto { + return { + id: collaborator.id, + firstName: collaborator.firstName, + lastName: collaborator.lastName, + fullName: `${collaborator.firstName} ${collaborator.lastName}`, + position: collaborator.position, + department: collaborator.department, + avatarUrl: collaborator.avatarUrl, + reportsToId: collaborator.reportsToId, + isActive: collaborator.isActive + }; + } +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/dashboard.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/dashboard.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..5f374b376cfb654ec90d0ad7974cd2d61fa5bed5 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/dashboard.service.ts @@ -0,0 +1,76 @@ +/** + * @fileoverview Dashboard Service + * Provides aggregated statistics and activity data for the home page. + * + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { Project } from '../database/entities/project.entity'; +import { Skill } from '../database/entities/skill.entity'; +import { Experience } from '../database/entities/experience.entity'; +import { ProjectStatus } from '@arkalliance/startupcms-ai-share'; +import { DashboardDataDto } from '@arkalliance/startupcms-ai-share'; + +/** + * Service class for dashboard-related operations. + * Aggregates data from multiple sources for portfolio overview. + */ +export class DashboardService { + private projectRepo = AppDataSource.getRepository(Project); + private skillRepo = AppDataSource.getRepository(Skill); + private expRepo = AppDataSource.getRepository(Experience); + + /** + * Retrieves complete dashboard data including statistics and activity graphs. + * + * Statistics include: + * - Total project count + * - Active projects count + * - Total skills count + * - Total years of experience (calculated from experience entries) + * + * Activity data is currently mocked and would require a Commit/Activity table + * for real implementation. + * + * @returns Promise resolving to complete dashboard data DTO + */ + public async getDashboardData(): Promise { + // Fetch real counts from database + const totalProjects = await this.projectRepo.count(); + const activeProjects = await this.projectRepo.count({ + where: { status: ProjectStatus.IN_PROGRESS } + }); + const totalSkills = await this.skillRepo.count(); + + // Calculate total experience years from all experience entries + const experiences = await this.expRepo.find(); + let totalExperienceYears = 0; + experiences.forEach(exp => { + const start = new Date(exp.startDate).getTime(); + const end = exp.endDate ? new Date(exp.endDate).getTime() : Date.now(); + const diffYears = (end - start) / (1000 * 60 * 60 * 24 * 365); + totalExperienceYears += diffYears; + }); + + // Mock activity data (requires Commit/Activity table for real data) + const activity = { + labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], + commits: [120, 150, 180, 90, 200, 240], + complexity: [45, 60, 75, 40, 85, 95] + }; + + return { + stats: { + totalProjects, + activeProjects, + totalSkills, + totalExperienceYears: Math.round(totalExperienceYears * 10) / 10 + }, + activity + }; + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/event.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/event.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec4b51dfde763aa49ce6c164bf05b1860e34aa80 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/event.service.ts @@ -0,0 +1,66 @@ +/** + * @fileoverview Event Service + * Implements the Outbox Pattern for reliable event publishing. + * Events are persisted to database before being processed, ensuring + * at-least-once delivery even in case of system failures. + * + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { Outbox } from '../database/entities/outbox.entity'; + +/** + * Service for publishing domain events using the Outbox Pattern. + * + * @remarks + * The Outbox Pattern ensures reliable event publishing by: + * 1. Persisting events to a database table (outbox) + * 2. A separate process picks up and processes pending events + * 3. Events are marked as processed after successful handling + * + * This guarantees events are not lost even if the system crashes + * immediately after a business operation completes. + * + * @example + * ```typescript + * const eventService = new EventService(); + * + * // After saving a project + * await eventService.publish('PROJECT_CREATED', { projectId: project.id }); + * ``` + */ +export class EventService { + private outboxRepo = AppDataSource.getRepository(Outbox); + + /** + * Publish an event to the outbox for later processing. + * + * @param type - Event type identifier (e.g., 'PROJECT_CREATED', 'USER_UPDATED') + * @param payload - Event data payload (will be JSON serialized) + * @returns Promise that resolves when event is persisted + * + * @remarks + * Events are stored with `processed: false` and should be picked up + * by a background worker for actual processing/dispatch. + * + * @example + * ```typescript + * // Publish a project update event + * await eventService.publish('PROJECT_UPDATED', { + * projectId: '123', + * changes: ['title', 'description'], + * updatedBy: userId + * }); + * ``` + */ + async publish(type: string, payload: any): Promise { + const event = this.outboxRepo.create({ + type, + payload, + processed: false + }); + await this.outboxRepo.save(event); + console.log(`[EventService] Published event: ${type}`); + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/organization.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/organization.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..81dda8a44fa48cafbdab9d8633c7bd17b7efb8a0 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/organization.service.ts @@ -0,0 +1,111 @@ +/** + * @fileoverview Organization Service + * Business logic for organization management. + * + * @author Armand Richelet-Kleinberg + */ + +import { + OrganizationDto, + OrganizationWithStatsDto, + UpdateOrganizationDto +} from '@arkalliance/startupcms-ai-share'; +import { OrganizationRepository } from '../database/repositories/organization.repository'; +import { CollaboratorRepository } from '../database/repositories/collaborator.repository'; +import { Organization } from '../database/entities/organization.entity'; + +/** + * Service for organization operations. + */ +export class OrganizationService { + private repository: OrganizationRepository; + private collaboratorRepository: CollaboratorRepository; + + constructor() { + this.repository = new OrganizationRepository(); + this.collaboratorRepository = new CollaboratorRepository(); + } + + /** + * Get the default organization. + */ + async getDefault(): Promise { + const org = await this.repository.getDefault(); + return org ? this.toDto(org) : null; + } + + /** + * Get organization by ID. + */ + async getById(id: string): Promise { + const org = await this.repository.findById(id); + return org ? this.toDto(org) : null; + } + + /** + * Get organization with stats (collaborator counts). + */ + async getWithStats(id: string): Promise { + const org = await this.repository.findWithStats(id); + if (!org) return null; + + const collaboratorCount = await this.collaboratorRepository.countByOrganization(id); + + return { + ...this.toDto(org), + collaboratorCount, + activeCollaboratorCount: collaboratorCount // All found are active due to filter + }; + } + + /** + * Update organization. + */ + async update(id: string, data: UpdateOrganizationDto): Promise { + // Map DTO to entity updates + const updates: Partial = { + ...data, + // Handle nested GPS coordinates + latitude: data.gps?.latitude, + longitude: data.gps?.longitude + }; + delete (updates as any).gps; + + const org = await this.repository.update(id, updates); + return org ? this.toDto(org) : null; + } + + /** + * Convert entity to DTO. + */ + private toDto(org: Organization): OrganizationDto { + return { + id: org.id, + name: org.name, + legalName: org.legalName, + tagline: org.tagline, + mission: org.mission, + vision: org.vision, + description: org.description, + logoUrl: org.logoUrl, + iconUrl: org.iconUrl, + address: org.address, + city: org.city, + country: org.country, + postalCode: org.postalCode, + timezone: org.timezone, + gps: org.latitude && org.longitude ? { + latitude: Number(org.latitude), + longitude: Number(org.longitude) + } : undefined, + socialLinks: org.socialLinks, + foundedYear: org.foundedYear, + employeeCount: org.employeeCount, + isActive: org.isActive, + createdAt: org.createdAt.toISOString(), + updatedAt: org.updatedAt.toISOString() + }; + } +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/page.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/page.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..6306b829d2bb73e1641d67ac92c83980a4583a96 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/page.service.ts @@ -0,0 +1,218 @@ +/** + * @fileoverview Page Service + * @description Service layer for CMS page management with SEO metadata and structured data. + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { Page, PageType } from '../database/entities/page.entity'; +import { SeoMeta } from '../database/entities/seo-meta.entity'; +import { StructuredData } from '../database/entities/structured-data.entity'; +import { Repository } from 'typeorm'; + +/** + * Service for managing CMS pages with SEO + */ +export class PageService { + private pageRepository: Repository; + private seoMetaRepository: Repository; + private structuredDataRepository: Repository; + + constructor() { + this.pageRepository = AppDataSource.getRepository(Page); + this.seoMetaRepository = AppDataSource.getRepository(SeoMeta); + this.structuredDataRepository = AppDataSource.getRepository(StructuredData); + } + + /** + * Get all published pages + */ + async getAllPublishedPages(): Promise { + return this.pageRepository.find({ + where: { isPublished: true }, + relations: ['seoMeta', 'structuredData', 'author'], + order: { publishedAt: 'DESC' } + }); + } + + /** + * Get all pages (admin) + */ + async getAllPages(): Promise { + return this.pageRepository.find({ + relations: ['seoMeta', 'structuredData', 'author'], + order: { updatedAt: 'DESC' } + }); + } + + /** + * Get page by slug (with SEO data) + */ + async getPageBySlug(slug: string): Promise { + return this.pageRepository.findOne({ + where: { slug }, + relations: ['seoMeta', 'structuredData', 'author'] + }); + } + + /** + * Get page by ID + */ + async getPageById(id: string): Promise { + return this.pageRepository.findOne({ + where: { id }, + relations: ['seoMeta', 'structuredData', 'author'] + }); + } + + /** + * Get pages by type + */ + async getPagesByType(pageType: PageType): Promise { + return this.pageRepository.find({ + where: { pageType, isPublished: true }, + relations: ['seoMeta', 'structuredData'], + order: { publishedAt: 'DESC' } + }); + } + + /** + * Create new page with SEO metadata + */ + async createPage(pageData: { + slug: string; + title: string; + content: string; + pageType?: PageType; + authorId?: string; + isPublished?: boolean; + seoMeta?: Partial; + structuredData?: Array<{ schemaType: string; schemaData: Record }>; + }): Promise { + // Create page entity + const page = this.pageRepository.create({ + slug: pageData.slug, + title: pageData.title, + content: pageData.content, + pageType: pageData.pageType || PageType.CUSTOM, + authorId: pageData.authorId, + isPublished: pageData.isPublished || false, + publishedAt: pageData.isPublished ? new Date() : undefined + }); + + // Create SEO meta if provided + if (pageData.seoMeta) { + const seoMeta = this.seoMetaRepository.create({ + ...pageData.seoMeta, + page + }); + page.seoMeta = seoMeta; + } + + // Create structured data if provided + if (pageData.structuredData && pageData.structuredData.length > 0) { + page.structuredData = pageData.structuredData.map((sd, index) => { + const structuredData = this.structuredDataRepository.create({ + schemaType: sd.schemaType, + schemaData: JSON.stringify(sd.schemaData), + displayOrder: index, + page + }); + return structuredData; + }); + } + + return this.pageRepository.save(page); + } + + /** + * Update page + */ + async updatePage(id: string, updates: Partial): Promise { + const page = await this.getPageById(id); + if (!page) return null; + + // Update page fields + Object.assign(page, updates); + + // Update published date if status changed + if (updates.isPublished && !page.publishedAt) { + page.publishedAt = new Date(); + } + + return this.pageRepository.save(page); + } + + /** + * Delete page + */ + async deletePage(id: string): Promise { + const result = await this.pageRepository.delete(id); + return !!result.affected && result.affected > 0; + } + + /** + * Update SEO metadata for a page + */ + async updateSeoMeta(pageId: string, seoData: Partial): Promise { + const page = await this.getPageById(pageId); + if (!page) return null; + + if (page.seoMeta) { + // Update existing + Object.assign(page.seoMeta, seoData); + return this.seoMetaRepository.save(page.seoMeta); + } else { + // Create new + const seoMeta = this.seoMetaRepository.create({ + ...seoData, + pageId + }); + return this.seoMetaRepository.save(seoMeta); + } + } + + /** + * Add structured data to page + */ + async addStructuredData( + pageId: string, + schemaType: string, + schemaData: Record + ): Promise { + const page = await this.getPageById(pageId); + if (!page) return null; + + const structuredData = this.structuredDataRepository.create({ + pageId, + schemaType, + schemaData: JSON.stringify(schemaData), + displayOrder: page.structuredData?.length || 0 + }); + + return this.structuredDataRepository.save(structuredData); + } + + /** + * Remove structured data + */ + async removeStructuredData(id: string): Promise { + const result = await this.structuredDataRepository.delete(id); + return !!result.affected && result.affected > 0; + } + + /** + * Check if slug is available + */ + async isSlugAvailable(slug: string, excludeId?: string): Promise { + const query = this.pageRepository.createQueryBuilder('page') + .where('page.slug = :slug', { slug }); + + if (excludeId) { + query.andWhere('page.id != :excludeId', { excludeId }); + } + + const count = await query.getCount(); + return count === 0; + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/profile.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/profile.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..6e73daae4337a4e076d6d05343cde64e9103fb64 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/profile.service.ts @@ -0,0 +1,40 @@ +/** + * @fileoverview Profile Service + * Handles retrieval of portfolio owner's profile information. + * + * @author Armand Richelet-Kleinberg + */ + +import { ProfileRepository } from '../database/repositories/profile.repository'; +import { ProfileDto } from '@arkalliance/startupcms-ai-share'; + +/** + * Service class for profile-related operations. + * Manages portfolio owner's personal and contact information. + */ +export class ProfileService { + /** + * Retrieves the portfolio owner's profile. + * Assumes a single profile with ID 1 exists in the database. + * + * @returns Promise resolving to profile DTO or null if not found + */ + async getProfile(): Promise { + const profile = await ProfileRepository.findOne({ where: { id: 1 } }); + if (!profile) return null; + + return { + firstName: profile.firstName, + lastName: profile.lastName, + title: profile.title, + overview: profile.overview, + email: profile.email, + linkedinUrl: profile.linkedinUrl, + githubUrl: profile.githubUrl, + avatarUrl: profile.avatarUrl + }; + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/project-presentation.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/project-presentation.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..12de08c1a2633ebbaadd62bef1f6179668f3b5cd --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/project-presentation.service.ts @@ -0,0 +1,91 @@ +/** + * @fileoverview Project Presentation Service + * Business logic for public-facing project display and portfolio presentation. + * Provides optimized queries for frontend consumption. + * + * @author Armand Richelet-Kleinberg + */ + +import { ProjectRepository } from '../database/repositories/project.repository'; +import { Project } from '../database/entities/project.entity'; + +/** + * Service for project presentation and portfolio display. + * + * @remarks + * This service is optimized for public-facing endpoints where: + * - Full project trees are needed for detail pages + * - Summary lists are needed for catalogue/grid views + * - Relations are selectively loaded for performance + * + * @example + * ```typescript + * const service = new ProjectPresentationService(); + * + * // For project detail page + * const project = await service.getFullProject('project-id'); + * + * // For projects catalogue + * const projects = await service.getAllProjectsSummary(); + * ``` + */ +export class ProjectPresentationService { + private projectRepo: ProjectRepository; + + constructor() { + this.projectRepo = new ProjectRepository(); + } + + /** + * Get full project with all related data for detail page. + * + * @param id - Project UUID or slug + * @returns Complete project entity with all relations, or null if not found + * + * @remarks + * Loads the complete project tree including: + * - All pages and their widgets + * - Technologies with categories + * - Features list + * - Carousel images + * + * @example + * ```typescript + * const project = await service.getFullProject('ark-portfolio'); + * if (project) { + * console.log('Pages:', project.pages.length); + * console.log('Technologies:', project.technologies.length); + * } + * ``` + */ + async getFullProject(id: string): Promise { + return this.projectRepo.findFullProjectTree(id); + } + + /** + * Get all projects with minimal data for catalogue/list views. + * + * @returns Array of projects with only essential fields for display + * + * @remarks + * Optimized query that: + * - Selects only necessary columns (id, title, imageUrl, description, status) + * - Includes technologies for tech stack badges + * - Excludes heavy relations (pages, widgets, features) + * + * @example + * ```typescript + * const projects = await service.getAllProjectsSummary(); + * // Render project cards with minimal data + * projects.forEach(p => { + * console.log(`${p.title}: ${p.technologies.length} technologies`); + * }); + * ``` + */ + async getAllProjectsSummary(): Promise { + return this.projectRepo.findAll({ + select: ['id', 'title', 'imageUrl', 'description', 'status'], + relations: ['technologies'] + }); + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/project.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/project.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..56ead2f10b343878291dab0e2e692c818c626716 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/project.service.ts @@ -0,0 +1,140 @@ +/** + * @fileoverview Project Service + * Handles business logic for project-related operations. + * Uses centralized mappers for consistent entity-to-DTO transformation. + * + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { Project } from '../database/entities/project.entity'; +import { ProjectDto } from '@arkalliance/startupcms-ai-share'; +import { mapProjectToDto, mapProjectsToDtos } from '../mappers'; + +/** + * Service class for project-related operations. + * Provides CRUD operations and queries for portfolio projects. + */ +export class ProjectService { + /** + * Retrieves all projects with their relations. + * Projects are sorted by start date (most recent first). + * + * @returns Promise resolving to array of all project DTOs + */ + async getAllProjects(): Promise { + const projectRepo = AppDataSource.getRepository(Project); + const projects = await projectRepo.find({ + relations: { + technologies: true, + features: true, + pages: true + }, + order: { + startDate: 'DESC' + } + }); + + return mapProjectsToDtos(projects); + } + + /** + * Retrieves featured projects for homepage carousel. + * Limited to 5 most recent featured projects. + * + * @returns Promise resolving to array of featured project DTOs + */ + async getFeaturedProjects(): Promise { + const projectRepo = AppDataSource.getRepository(Project); + const projects = await projectRepo.find({ + where: { isFeatured: true }, + relations: { + technologies: true, + features: true, + pages: true + }, + order: { + startDate: 'DESC' + }, + take: 5 + }); + + return mapProjectsToDtos(projects); + } + + /** + * Retrieves a single project by its ID. + * + * @param id - The project's unique identifier + * @returns Promise resolving to project DTO or null if not found + */ + async getProjectById(id: string): Promise { + const projectRepo = AppDataSource.getRepository(Project); + const project = await projectRepo.findOne({ + where: { id }, + relations: { + technologies: true, + features: true, + pages: true + } + }); + + if (!project) return null; + + return mapProjectToDto(project); + } + + /** + * Retrieves a single project by its slug (URL-friendly name). + * The slug is derived from the project title (lowercase, spaces/dots to hyphens). + * + * @param slug - The project's URL-friendly slug (e.g., 'ark-alliance') + * @returns Promise resolving to project DTO or null if not found + */ + async getProjectBySlug(slug: string): Promise { + const projectRepo = AppDataSource.getRepository(Project); + const projects = await projectRepo.find({ + relations: { + technologies: true, + features: true, + pages: true + } + }); + + // Convert slug back to title pattern and find matching project + const normalizedSlug = slug.toLowerCase(); + const project = projects.find(p => { + const projectSlug = p.title + .toLowerCase() + .replace(/\./g, '-') + .replace(/\s+/g, '-') + .replace(/-+/g, '-'); + return projectSlug === normalizedSlug; + }); + + if (!project) return null; + + return mapProjectToDto(project); + } + + /** + * Retrieves a project by ID or slug. + * Automatically detects if the parameter is a UUID or slug. + * + * @param idOrSlug - Either a UUID string or URL-friendly slug + * @returns Promise resolving to project DTO or null if not found + */ + async getProjectByIdOrSlug(idOrSlug: string): Promise { + // Check if it's a UUID pattern + const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + + if (uuidPattern.test(idOrSlug)) { + return this.getProjectById(idOrSlug); + } else { + return this.getProjectBySlug(idOrSlug); + } + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/prompt-template.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/prompt-template.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..37c9ccd4a8bbb956984187f8403bceee6558df3c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/prompt-template.service.ts @@ -0,0 +1,108 @@ +/** + * @fileoverview PromptTemplate Service + * CRUD operations for AI prompt templates. + * + * @module services/prompt-template + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { PromptTemplate } from '../database/entities/prompt-template.entity'; + +/** + * Service for managing AI prompt templates. + */ +export class PromptTemplateService { + private promptRepo = AppDataSource.getRepository(PromptTemplate); + + /** + * Get all prompt templates. + * @returns Promise resolving to array of prompt templates + */ + async getAll(): Promise { + return this.promptRepo.find({ + where: { isActive: true }, + order: { name: 'ASC' } + }); + } + + /** + * Get a prompt template by name. + * @param name - Unique name of the prompt template + * @returns Promise resolving to prompt template or null + */ + async getByName(name: string): Promise { + return this.promptRepo.findOne({ + where: { name, isActive: true } + }); + } + + /** + * Get a prompt template by ID. + * @param id - Prompt template ID + * @returns Promise resolving to prompt template or null + */ + async getById(id: number): Promise { + return this.promptRepo.findOne({ + where: { id } + }); + } + + /** + * Create a new prompt template. + * @param data - Prompt template data + * @returns Promise resolving to created template + */ + async create(data: Partial): Promise { + const template = this.promptRepo.create(data); + return this.promptRepo.save(template); + } + + /** + * Update an existing prompt template. + * @param id - Template ID + * @param data - Updated data + * @returns Promise resolving to updated template or null + */ + async update(id: number, data: Partial): Promise { + const template = await this.getById(id); + if (!template) return null; + + Object.assign(template, data); + return this.promptRepo.save(template); + } + + /** + * Soft delete a prompt template (set isActive to false). + * @param id - Template ID + * @returns Promise resolving to success boolean + */ + async delete(id: number): Promise { + const template = await this.getById(id); + if (!template) return false; + + template.isActive = false; + await this.promptRepo.save(template); + return true; + } + + /** + * Format a client prompt template with placeholder values. + * @param template - Prompt template string with {placeholders} + * @param values - Key-value pairs for placeholder substitution + * @returns Formatted prompt string + * + * @example + * formatPrompt('Hello {name}!', { name: 'World' }) // 'Hello World!' + */ + formatPrompt(template: string, values: Record): string { + let result = template; + for (const [key, value] of Object.entries(values)) { + result = result.replace(new RegExp(`\\{${key}\\}`, 'g'), value || ''); + } + return result; + } +} + +// Singleton instance +export const promptTemplateService = new PromptTemplateService(); diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/resume.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/resume.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..137bc543bd9342551716e3a10a7bd45c47b1089a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/resume.service.ts @@ -0,0 +1,164 @@ +/** + * @fileoverview Resume Service + * Handles retrieval of resume data including education, + * work experience, skills, languages, hobbies, and business domains. + * + * @author Armand Richelet-Kleinberg + */ + +import { EducationRepository, ExperienceRepository, SkillRepository } from '../database/repositories/resume.repository'; +import { ResumeDto, SkillLevel } from '@arkalliance/startupcms-ai-share'; +import { AppDataSource } from '../config/database'; +import { Profile } from '../database/entities/profile.entity'; +import { User } from '../database/entities/user.entity'; +import { Language } from '../database/entities/language.entity'; +import { Hobby } from '../database/entities/hobby.entity'; +import { BusinessDomain } from '../database/entities/business-domain.entity'; +import { mapProfileToDto } from '../mappers'; + +/** + * Service class for Resume-related operations. + * Aggregates education, experience, skills, languages, hobbies, and business domains. + */ +export class ResumeService { + /** + * Retrieves complete Resume data for a specific user or default (Armand) profile. + * Education and experience are sorted by start date (most recent first). + * Skills, languages, hobbies, and business domains are sorted by displayOrder. + * + * @param userId - Optional user ID to fetch resume for. If not provided, fetches Armand's resume. + * @returns Promise resolving to complete Resume DTO + */ + async getResume(userId?: string): Promise { + // If no userId provided, get Armand's userId (default profile) + let effectiveUserId = userId; + + if (!effectiveUserId) { + const userRepo = AppDataSource.getRepository(User); + const armandUser = await userRepo.findOne({ + where: { email: 'arkleinberg@gmail.com' } + }); + if (armandUser) { + effectiveUserId = armandUser.id; + } + } + + // Build where clause for user-specific data + const userFilter = effectiveUserId ? { userId: effectiveUserId } : {}; + + const education = await EducationRepository.find({ + where: userFilter, + order: { startDate: 'DESC' } + }); + const experience = await ExperienceRepository.find({ + where: userFilter, + order: { startDate: 'DESC' } + }); + const skills = await SkillRepository.find({ + where: userFilter, + order: { displayOrder: 'ASC', name: 'ASC' }, + relations: ['category'] + }); + + // Fetch new resume sections + const languageRepo = AppDataSource.getRepository(Language); + const hobbyRepo = AppDataSource.getRepository(Hobby); + const domainRepo = AppDataSource.getRepository(BusinessDomain); + + const languages = await languageRepo.find({ + where: userFilter, + order: { displayOrder: 'ASC' } + }); + const hobbies = await hobbyRepo.find({ + where: userFilter, + order: { displayOrder: 'ASC' } + }); + const businessDomains = await domainRepo.find({ + where: userFilter, + order: { displayOrder: 'ASC' } + }); + + // Get profile data - either from User (if effectiveUserId exists) or legacy Profile + let profileDto: any = null; + + if (effectiveUserId) { + // Fetch user and build profile from User entity + const userRepo = AppDataSource.getRepository(User); + const user = await userRepo.findOne({ where: { id: effectiveUserId } }); + if (user) { + profileDto = { + firstName: user.firstName || '', + lastName: user.lastName || '', + title: user.title || '', + overview: user.bio || '', + email: user.email || '', + avatarUrl: user.avatarUrl || '/Assets/Site/Icon.png', + linkedinUrl: user.linkedinUrl || null, + githubUrl: user.githubUrl || null, + twitterUrl: user.twitterUrl || null, + }; + } + } + + // Fallback to legacy Profile if no user found or no userId provided + if (!profileDto) { + const profileRepo = AppDataSource.getRepository(Profile); + const profile = await profileRepo.findOne({ where: {} }); + if (!profile) throw new Error('Profile not found'); + profileDto = mapProfileToDto(profile); + } + + return { + profile: profileDto as any, + education: education.map((e: any) => ({ + id: e.id, + institution: e.institution, + degree: e.degree, + fieldOfStudy: e.fieldOfStudy, + startDate: e.startDate, + endDate: e.endDate, + description: e.description + })), + experiences: experience.map((e: any) => ({ + id: e.id, + company: e.company, + position: e.position, + startDate: e.startDate, + endDate: e.endDate, + description: e.description, + technologies: e.technologies || [] + })), + skills: skills.map((s: any) => ({ + name: s.name, + level: s.level as SkillLevel, + category: s.category?.name || 'Uncategorized' + })), + languages: languages.map((l: Language) => ({ + id: l.id, + language: l.language, + speaking: l.speaking, + writing: l.writing, + presenting: l.presenting, + displayOrder: l.displayOrder + })), + hobbies: hobbies.map((h: Hobby) => ({ + id: h.id, + name: h.name, + description: h.description, + icon: h.icon, + displayOrder: h.displayOrder + })), + businessDomains: businessDomains.map((d: BusinessDomain) => ({ + id: d.id, + domain: d.domain, + level: d.level as any, + description: d.description, + yearsOfExperience: d.yearsOfExperience, + icon: d.icon, + displayOrder: d.displayOrder + })) + }; + } +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/role.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/role.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..4568bb5cde80b23e9c33a62d6bf3ae74b9c0726f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/role.service.ts @@ -0,0 +1,219 @@ +/** + * @fileoverview Role Service + * Business logic for role assignment and management. + * + * @author Armand Richelet-Kleinberg + */ + +import { Role, SupervisorAssignableRoles, hasHigherOrEqualPriority } from '@arkalliance/startupcms-ai-share'; +import { UserRoleRepository } from '../database/repositories/user-role.repository'; +import { AuditLogService } from './audit-log.service'; +import { AuditAction } from '@arkalliance/startupcms-ai-share'; +import { AppDataSource } from '../config/database'; +import { User } from '../database/entities/user.entity'; + +/** + * Service for managing user roles. + */ +export class RoleService { + private userRoleRepository: UserRoleRepository; + private auditLogService: AuditLogService; + + constructor() { + this.userRoleRepository = new UserRoleRepository(); + this.auditLogService = new AuditLogService(); + } + + /** + * Assign a role to a user. + * Uses database transaction to ensure atomicity. + * @throws Error if current user lacks permission to assign the role + */ + async assignRole( + userId: string, + role: Role, + assignedById: string, + assignedByRoles: Role[], + ipAddress?: string + ): Promise<{ success: boolean; error?: string }> { + // Check permission to assign this role + const canAssign = this.canAssignRole(role, assignedByRoles); + if (!canAssign) { + await this.auditLogService.log({ + userId: assignedById, + action: AuditAction.ROLE_ASSIGNED, + ipAddress, + success: false, + details: { targetUserId: userId, role, reason: 'Insufficient permission' } + }); + return { success: false, error: 'Insufficient permission to assign this role' }; + } + + // Verify target user exists + const userRepo = AppDataSource.getRepository(User); + const targetUser = await userRepo.findOne({ where: { id: userId } }); + if (!targetUser) { + await this.auditLogService.log({ + userId: assignedById, + action: AuditAction.ROLE_ASSIGNED, + ipAddress, + success: false, + details: { targetUserId: userId, role, reason: 'User not found' } + }); + return { success: false, error: 'Target user not found' }; + } + + // Use transaction for atomicity + const queryRunner = AppDataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // Assign the role (using repository with transaction) + const txUserRoleRepo = new UserRoleRepository(queryRunner.manager.connection); + await txUserRoleRepo.assignRole(userId, role, assignedById); + + await queryRunner.commitTransaction(); + + // Log success (outside transaction - audit logging has its own resilience) + await this.auditLogService.log({ + userId: assignedById, + action: AuditAction.ROLE_ASSIGNED, + ipAddress, + success: true, + details: { targetUserId: userId, role } + }); + + return { success: true }; + } catch (error: any) { + await queryRunner.rollbackTransaction(); + + // Log failure + await this.auditLogService.log({ + userId: assignedById, + action: AuditAction.ROLE_ASSIGNED, + ipAddress, + success: false, + details: { targetUserId: userId, role, reason: error.message } + }); + + return { success: false, error: 'Failed to assign role: ' + error.message }; + } finally { + await queryRunner.release(); + } + } + + /** + * Revoke a role from a user. + * Uses database transaction to ensure atomicity. + * @throws Error if current user lacks permission to revoke the role + */ + async revokeRole( + userId: string, + role: Role, + revokedById: string, + revokedByRoles: Role[], + ipAddress?: string + ): Promise<{ success: boolean; error?: string }> { + // Check permission + const canRevoke = this.canAssignRole(role, revokedByRoles); + if (!canRevoke) { + await this.auditLogService.log({ + userId: revokedById, + action: AuditAction.ROLE_REVOKED, + ipAddress, + success: false, + details: { targetUserId: userId, role, reason: 'Insufficient permission' } + }); + return { success: false, error: 'Insufficient permission to revoke this role' }; + } + + // Use transaction for atomicity + const queryRunner = AppDataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // Revoke the role + const txUserRoleRepo = new UserRoleRepository(queryRunner.manager.connection); + const revoked = await txUserRoleRepo.revokeRole(userId, role); + + if (!revoked) { + await queryRunner.rollbackTransaction(); + return { success: false, error: 'Role was not assigned to user' }; + } + + await queryRunner.commitTransaction(); + + // Log success + await this.auditLogService.log({ + userId: revokedById, + action: AuditAction.ROLE_REVOKED, + ipAddress, + success: true, + details: { targetUserId: userId, role } + }); + + return { success: true }; + } catch (error: any) { + await queryRunner.rollbackTransaction(); + + await this.auditLogService.log({ + userId: revokedById, + action: AuditAction.ROLE_REVOKED, + ipAddress, + success: false, + details: { targetUserId: userId, role, reason: error.message } + }); + + return { success: false, error: 'Failed to revoke role: ' + error.message }; + } finally { + await queryRunner.release(); + } + } + + /** + * Check if a user can assign a specific role. + * - Admins can assign any role + * - Supervisors can only assign roles in SupervisorAssignableRoles + */ + canAssignRole(roleToAssign: Role, currentUserRoles: Role[]): boolean { + // Admins can assign any role + if (currentUserRoles.includes(Role.ADMIN)) { + return true; + } + + // Supervisors can only assign limited roles + if (currentUserRoles.includes(Role.SUPERVISOR)) { + return SupervisorAssignableRoles.includes(roleToAssign); + } + + // No other roles can assign + return false; + } + + /** + * Get all roles for a user. + */ + async getUserRoles(userId: string): Promise { + const userRoles = await this.userRoleRepository.findByUserId(userId); + return userRoles.map(ur => ur.role); + } + + /** + * Check if user has a specific role. + */ + async hasRole(userId: string, role: Role): Promise { + return this.userRoleRepository.hasRole(userId, role); + } + + /** + * Check if user has any of the specified roles. + */ + async hasAnyRole(userId: string, roles: Role[]): Promise { + const userRoles = await this.getUserRoles(userId); + return roles.some(r => userRoles.includes(r)); + } +} + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/site-settings.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/site-settings.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..b69e11c2ab7b7dec0af5298486dbe3c1e076064a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/site-settings.service.ts @@ -0,0 +1,115 @@ +/** + * @fileoverview Site Settings Service + * @description Service layer for managing global site configuration. + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { SiteSettings } from '../database/entities/site-settings.entity'; +import { Repository } from 'typeorm'; + +/** + * Service for managing site settings (singleton pattern) + */ +export class SiteSettingsService { + private repository: Repository; + + constructor() { + this.repository = AppDataSource.getRepository(SiteSettings); + } + + /** + * Get site settings (creates default if not exists) + */ + async getSettings(): Promise { + const settings = await this.repository.findOne({ + where: {}, + order: { createdAt: 'ASC' } + }); + + if (!settings) { + // Create default settings + return this.createDefaultSettings(); + } + + return settings; + } + + /** + * Update site settings + */ + async updateSettings(updates: Partial): Promise { + const settings = await this.getSettings(); + Object.assign(settings, updates); + return this.repository.save(settings); + } + + /** + * Create default site settings + */ + private async createDefaultSettings(): Promise { + const defaultSettings = this.repository.create({ + siteName: 'Ark Alliance CMS', + siteUrl: 'https://example.com', + companyName: 'Ark Alliance', + defaultMetaDescription: 'AI-powered startup CMS platform', + defaultKeywords: 'CMS, AI, startup, portfolio' + }); + + return this.repository.save(defaultSettings); + } + + /** + * Get social links for Schema.org + */ + async getSocialLinks(): Promise { + const settings = await this.getSettings(); + return settings.getSameAsLinks(); + } + + /** + * Get Organization schema data + */ + async getOrganizationSchema(): Promise> { + const settings = await this.getSettings(); + + return { + '@context': 'https://schema.org', + '@type': 'Organization', + name: settings.companyName, + url: settings.siteUrl, + logo: settings.logoUrl, + sameAs: settings.getSameAsLinks(), + contactPoint: settings.contactEmail ? { + '@type': 'ContactPoint', + email: settings.contactEmail, + telephone: settings.contactPhone, + contactType: 'customer service' + } : undefined, + address: settings.address ? { + '@type': 'PostalAddress', + streetAddress: settings.address + } : undefined + }; + } + + /** + * Get WebSite schema data + */ + async getWebSiteSchema(): Promise> { + const settings = await this.getSettings(); + + return { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: settings.siteName, + url: settings.siteUrl, + description: settings.defaultMetaDescription, + publisher: { + '@type': 'Organization', + name: settings.companyName, + logo: settings.logoUrl + } + }; + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/static-generation.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/static-generation.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..f2528d4c2f4a172ecf330dea7cd0facc3060acfa --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/static-generation.service.ts @@ -0,0 +1,1523 @@ +/** + * @fileoverview Static Generation Service + * Orchestrates the generation of complete static React/TSX website packages. + * + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { Profile } from '../database/entities/profile.entity'; +import { Project } from '../database/entities/project.entity'; +import { Experience } from '../database/entities/experience.entity'; +import { Education } from '../database/entities/education.entity'; +import { Skill } from '../database/entities/skill.entity'; +import { SkillCategory } from '../database/entities/skill-category.entity'; +import { Language } from '../database/entities/language.entity'; +import { Hobby } from '../database/entities/hobby.entity'; +import { BusinessDomain } from '../database/entities/business-domain.entity'; +import { CarouselItem } from '../database/entities/carousel-item.entity'; +import { StyleConfig } from '../database/entities/style-config.entity'; +import { PageDefinition } from '../database/entities/page-definition.entity'; +import { + StaticExportConfigDto, + StaticExportPreviewDto, + StaticExportResultDto, + StaticExportStatus, + StaticThemePreset, + DEFAULT_STATIC_PAGES, + DEFAULT_STATIC_EXPORT_CONFIG, + STATIC_THEME_CSS_VARS, + STATIC_VITE_DEPENDENCIES +} from '@arkalliance/startupcms-ai-share'; +import archiver from 'archiver'; +import { Response } from 'express'; +import path from 'path'; +import fs from 'fs'; + +/** + * Portfolio data structure for static export. + */ +interface PortfolioData { + profile: Profile | null; + projects: Project[]; + experiences: Experience[]; + education: Education[]; + skills: Skill[]; + skillCategories: SkillCategory[]; + languages: Language[]; + hobbies: Hobby[]; + businessDomains: BusinessDomain[]; + carousel: CarouselItem[]; + generatedAt: string; +} + +/** + * Static Generation Service + * Handles the complete static site generation workflow. + */ +export class StaticGenerationService { + private profileRepo = AppDataSource.getRepository(Profile); + private projectRepo = AppDataSource.getRepository(Project); + private experienceRepo = AppDataSource.getRepository(Experience); + private educationRepo = AppDataSource.getRepository(Education); + private skillRepo = AppDataSource.getRepository(Skill); + private skillCategoryRepo = AppDataSource.getRepository(SkillCategory); + private languageRepo = AppDataSource.getRepository(Language); + private hobbyRepo = AppDataSource.getRepository(Hobby); + private domainRepo = AppDataSource.getRepository(BusinessDomain); + private carouselRepo = AppDataSource.getRepository(CarouselItem); + private styleRepo = AppDataSource.getRepository(StyleConfig); + private pageDefRepo = AppDataSource.getRepository(PageDefinition); + + /** + * Get preview data for export. + */ + async getExportPreview(): Promise { + const [profile, projects, experiences, skills, languages, hobbies, domains, pageDefs] = await Promise.all([ + this.profileRepo.findOne({ where: {} }), + this.projectRepo.find(), + this.experienceRepo.find(), + this.skillRepo.find(), + this.languageRepo.find(), + this.hobbyRepo.find(), + this.domainRepo.find(), + this.pageDefRepo.find({ where: { isEnabled: true }, order: { displayOrder: 'ASC' } }) + ]); + + const profileName = profile ? `${profile.firstName} ${profile.lastName}` : 'Portfolio'; + + return { + profileName, + projectCount: projects.length, + experienceCount: experiences.length, + skillCount: skills.length, + languageCount: languages.length, + hobbyCount: hobbies.length, + domainCount: domains.length, + theme: StaticThemePreset.ARCHITECTURAL, + estimatedSize: '~1-3MB', + pages: pageDefs.length > 0 ? pageDefs.map(p => ({ + pageType: p.pageType, + title: p.title, + route: p.route, + isEnabled: p.isEnabled, + displayOrder: p.displayOrder, + })) : DEFAULT_STATIC_PAGES + }; + } + + /** + * Fetch all portfolio data. + */ + async getPortfolioData(): Promise { + const [ + profile, projects, experiences, education, + skills, skillCategories, languages, hobbies, + businessDomains, carousel + ] = await Promise.all([ + this.profileRepo.findOne({ where: {} }), + this.projectRepo.find({ order: { startDate: 'DESC' } }), + this.experienceRepo.find({ order: { startDate: 'DESC' } }), + this.educationRepo.find({ order: { startDate: 'DESC' } }), + this.skillRepo.find({ order: { displayOrder: 'ASC' } }), + this.skillCategoryRepo.find({ order: { displayOrder: 'ASC' } }), + this.languageRepo.find({ order: { displayOrder: 'ASC' } }), + this.hobbyRepo.find({ order: { displayOrder: 'ASC' } }), + this.domainRepo.find({ order: { displayOrder: 'ASC' } }), + this.carouselRepo.find({ where: { isActive: true }, order: { order: 'ASC' } }) + ]); + + return { + profile, + projects, + experiences, + education, + skills, + skillCategories, + languages, + hobbies, + businessDomains, + carousel, + generatedAt: new Date().toISOString() + }; + } + + /** + * Generate and stream static site as ZIP. + */ + async generateStaticSite(res: Response, config?: Partial): Promise { + const data = await this.getPortfolioData(); + const profileName = data.profile + ? `${data.profile.firstName} ${data.profile.lastName}` + : 'Portfolio'; + + const exportConfig: StaticExportConfigDto = { + ...DEFAULT_STATIC_EXPORT_CONFIG, + ...config, + siteMetadata: { + ...DEFAULT_STATIC_EXPORT_CONFIG.siteMetadata, + title: `${profileName} - Portfolio`, + description: data.profile?.overview || '', + author: profileName, + ...config?.siteMetadata + } + }; + + console.log(`[StaticGen] Starting export for: ${profileName}`); + console.log(`[StaticGen] Projects: ${data.projects.length}, Experiences: ${data.experiences.length}`); + + // Set response headers + const safeFilename = profileName.replace(/[^a-zA-Z0-9]/g, '_'); + res.setHeader('Content-Type', 'application/zip'); + res.setHeader('Content-Disposition', `attachment; filename="${safeFilename}_Portfolio_Static.zip"`); + + // Create archive + const archive = archiver('zip', { zlib: { level: 9 } }); + archive.on('error', (err) => { throw err; }); + archive.pipe(res); + + // Generate project structure + this.addPackageJson(archive, profileName); + this.addViteConfig(archive); + this.addTsConfig(archive); + this.addIndexHtml(archive, profileName, data); + this.addMainTsx(archive); + this.addAppTsx(archive, data); + this.addPortfolioData(archive, data); + this.addStyles(archive, exportConfig.theme); + this.addComponents(archive); + this.addPages(archive, data); + this.addReadme(archive, profileName, data); + await this.addAssets(archive); + + await archive.finalize(); + console.log(`[StaticGen] Export completed successfully`); + } + + /** + * Add package.json + */ + private addPackageJson(archive: archiver.Archiver, profileName: string): void { + const pkg = { + name: profileName.toLowerCase().replace(/[^a-z0-9]/g, '-') + '-portfolio', + version: '1.0.0', + description: `${profileName}'s Portfolio - Static Website`, + type: 'module', + scripts: { + dev: 'vite', + build: 'tsc && vite build', + preview: 'vite preview' + }, + ...STATIC_VITE_DEPENDENCIES, + author: profileName, + license: 'MIT', + keywords: ['portfolio', 'react', 'typescript', 'vite'] + }; + archive.append(JSON.stringify(pkg, null, 2), { name: 'package.json' }); + } + + /** + * Add vite.config.ts + */ + private addViteConfig(archive: archiver.Archiver): void { + const viteConfig = `import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + base: './', + build: { + outDir: 'dist', + sourcemap: false + } +}) +`; + archive.append(viteConfig, { name: 'vite.config.ts' }); + } + + /** + * Add tsconfig.json + */ + private addTsConfig(archive: archiver.Archiver): void { + const tsConfig = { + compilerOptions: { + target: 'ES2020', + useDefineForClassFields: true, + lib: ['ES2020', 'DOM', 'DOM.Iterable'], + module: 'ESNext', + skipLibCheck: true, + moduleResolution: 'bundler', + allowImportingTsExtensions: true, + resolveJsonModule: true, + isolatedModules: true, + noEmit: true, + jsx: 'react-jsx', + strict: true, + noUnusedLocals: true, + noUnusedParameters: true, + noFallthroughCasesInSwitch: true + }, + include: ['src'], + references: [{ path: './tsconfig.node.json' }] + }; + archive.append(JSON.stringify(tsConfig, null, 2), { name: 'tsconfig.json' }); + + const tsNodeConfig = { + compilerOptions: { + composite: true, + skipLibCheck: true, + module: 'ESNext', + moduleResolution: 'bundler', + allowSyntheticDefaultImports: true + }, + include: ['vite.config.ts'] + }; + archive.append(JSON.stringify(tsNodeConfig, null, 2), { name: 'tsconfig.node.json' }); + } + + /** + * Add index.html + */ + private addIndexHtml(archive: archiver.Archiver, profileName: string, data: PortfolioData): void { + const html = ` + + + + + + + + + + + + + ${profileName} - Portfolio + + +
+ + +`; + archive.append(html, { name: 'index.html' }); + } + + /** + * Add main.tsx + */ + private addMainTsx(archive: archiver.Archiver): void { + const mainTsx = `import React from 'react' +import ReactDOM from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import App from './App' +import './styles/index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + +) +`; + archive.append(mainTsx, { name: 'src/main.tsx' }); + } + + /** + * Add App.tsx with public routes only (NO admin routes) + */ + private addAppTsx(archive: archiver.Archiver, data: PortfolioData): void { + const appTsx = `import { Routes, Route, Navigate } from 'react-router-dom' +import { HomePage } from './pages/Home' +import { ResumePage } from './pages/Resume' +import { ProjectsPage } from './pages/Projects' +import { ProjectDetailPage } from './pages/ProjectDetail' +import portfolioData from './data/portfolio.json' + +// Portfolio data is embedded at build time +export const usePortfolioData = () => portfolioData + +function App() { + return ( + + {/* Public Routes Only - No Admin */} + } /> + } /> + } /> + } /> + } /> + + ) +} + +export default App +`; + archive.append(appTsx, { name: 'src/App.tsx' }); + } + + /** + * Add portfolio data as JSON + */ + private addPortfolioData(archive: archiver.Archiver, data: PortfolioData): void { + archive.append(JSON.stringify(data, null, 2), { name: 'src/data/portfolio.json' }); + } + + /** + * Add CSS styles - Professional Theme + */ + private addStyles(archive: archiver.Archiver, theme: StaticThemePreset): void { + const themeVars = STATIC_THEME_CSS_VARS[theme] || STATIC_THEME_CSS_VARS[StaticThemePreset.ARCHITECTURAL]; + const cssVars = Object.entries(themeVars).map(([k, v]) => ` ${k}: ${v};`).join('\n'); + + const indexCss = `/* Static Portfolio - Professional Theme Styles */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Outfit:wght@400;500;600;700&display=swap'); + +:root { +${cssVars} + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-display: 'Outfit', 'Inter', sans-serif; + + /* Professional Theme Colors */ + --bg-primary: #0A0E17; + --bg-secondary: #0F172A; + --bg-card: rgba(15, 23, 42, 0.85); + --text-primary: #F1F5F9; + --text-secondary: #94A3B8; + --text-muted: #64748B; + --accent: #00D4FF; + --accent-muted: #06B6D4; + --border-color: rgba(51, 65, 85, 0.6); + + /* Spacing */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.25rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-10: 2.5rem; + --space-12: 3rem; + + /* Radii */ + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; + --radius-2xl: 1.5rem; + --radius-full: 9999px; + + /* Shadows */ + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5); + + --header-height: 72px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: var(--font-sans); + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.7; + min-height: 100vh; + -webkit-font-smoothing: antialiased; +} + +a { + color: var(--accent); + text-decoration: none; + transition: color 0.2s ease; +} + +a:hover { + color: var(--text-primary); +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 0 var(--space-8); +} + +/* Header */ +.header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--header-height); + background: rgba(10, 14, 23, 0.95); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border-color); + z-index: 100; + display: flex; + align-items: center; + padding: 0 var(--space-8); +} + +.header-logo { + font-family: var(--font-display); + font-size: 1.25rem; + font-weight: 700; + color: var(--text-primary); +} + +.header-nav { + margin-left: auto; + display: flex; + gap: var(--space-8); +} + +.header-nav a { + color: var(--text-secondary); + font-size: 0.9375rem; + font-weight: 500; + transition: color 0.2s; +} + +.header-nav a:hover, +.header-nav a.active { + color: var(--accent); +} + +/* Page Layout */ +.page { + padding-top: var(--header-height); + min-height: 100vh; +} + +.page-content { + max-width: 1400px; + margin: 0 auto; + padding: var(--space-12) var(--space-8); +} + +.page-title { + font-family: var(--font-display); + font-size: 3rem; + font-weight: 700; + margin-bottom: var(--space-4); + letter-spacing: -0.025em; + text-align: center; +} + +.page-title span { + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-muted) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.page-subtitle { + color: var(--text-secondary); + font-size: 1.125rem; + margin-bottom: var(--space-10); + text-align: center; +} + +/* Hero Card */ +.hero-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-2xl); + padding: var(--space-10); + max-width: 900px; + margin: var(--space-8) auto; + font-size: 1.0625rem; + line-height: 1.75; + color: var(--text-secondary); + text-align: center; + backdrop-filter: blur(12px); +} + +/* Cards - Professional */ +.card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-xl); + padding: var(--space-8); + transition: all 0.3s ease; +} + +.card:hover { + border-color: var(--accent); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); + transform: translateY(-4px); +} + +/* Grid */ +.grid { + display: grid; + gap: var(--space-6); +} + +.grid-2 { grid-template-columns: repeat(2, 1fr); } +.grid-3 { grid-template-columns: repeat(3, 1fr); } + +@media (max-width: 1024px) { + .grid-3 { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 768px) { + .grid-2, .grid-3 { grid-template-columns: 1fr; } + .page-content { padding: var(--space-8) var(--space-4); } +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-4) var(--space-6); + font-size: 0.9375rem; + font-weight: 600; + border-radius: var(--radius-full); + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-primary { + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-muted) 100%); + color: #0A0E17; + box-shadow: 0 2px 8px rgba(0, 212, 255, 0.3); +} + +.btn-primary:hover { + box-shadow: 0 4px 16px rgba(0, 212, 255, 0.4); + transform: translateY(-2px); +} + +/* Tab Navigation - Pills */ +.tab-container { + display: flex; + justify-content: center; + margin-bottom: var(--space-10); +} + +.tab-pills { + display: flex; + gap: var(--space-2); + background: var(--bg-card); + border: 1px solid var(--border-color); + padding: 0.5rem; + border-radius: var(--radius-full); +} + +.tab-pill { + padding: 0.875rem 1.5rem; + font-size: 0.9375rem; + font-weight: 500; + color: var(--text-secondary); + background: transparent; + border: none; + border-radius: var(--radius-full); + cursor: pointer; + transition: all 0.2s ease; +} + +.tab-pill:hover { + background: rgba(0, 212, 255, 0.1); + color: var(--text-primary); +} + +.tab-pill.active { + background: var(--accent); + color: #0A0E17; + font-weight: 600; + box-shadow: 0 2px 8px rgba(0, 212, 255, 0.3); +} + +/* Timeline */ +.timeline { + position: relative; + padding-left: var(--space-10); +} + +.timeline::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 2px; + background: var(--border-color); +} + +.timeline-item { + position: relative; + padding-bottom: var(--space-10); +} + +.timeline-item::before { + content: ''; + position: absolute; + left: calc(-1 * var(--space-10) - 5px); + top: 0; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent); + border: 3px solid var(--bg-primary); +} + +/* Professional Badge - Expert Level */ +.badge-expert, +.domain-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.375rem 1rem; + background: var(--accent); + color: #0A0E17; + border-radius: var(--radius-full); + font-size: 0.6875rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + box-shadow: 0 2px 8px rgba(0, 212, 255, 0.25); +} + +/* Tech Chip */ +.skill-badge, +.tech-chip { + display: inline-flex; + align-items: center; + padding: 0.375rem 0.875rem; + background: rgba(51, 65, 85, 0.5); + color: var(--text-secondary); + border: 1px solid rgba(51, 65, 85, 0.6); + border-radius: var(--radius-md); + font-size: 0.8125rem; + font-weight: 500; + transition: all 0.15s ease; +} + +.skill-badge:hover, +.tech-chip:hover { + background: rgba(0, 212, 255, 0.1); + border-color: rgba(0, 212, 255, 0.3); + color: var(--text-primary); +} + +/* Hobby Icon */ +.hobby-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + background: rgba(0, 212, 255, 0.1); + color: var(--accent); + border: 1px solid rgba(0, 212, 255, 0.2); + border-radius: var(--radius-lg); + font-size: 1.5rem; + margin-bottom: var(--space-3); +} + +/* Project Card */ +.project-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-xl); + overflow: hidden; + transition: all 0.3s ease; +} + +.project-card:hover { + border-color: var(--accent); + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5), 0 0 1px rgba(0, 212, 255, 0.4); + transform: translateY(-6px); +} + +.project-card-image { + width: 100%; + height: 220px; + object-fit: cover; + transition: transform 0.3s ease; +} + +.project-card:hover .project-card-image { + transform: scale(1.05); +} + +.project-card-content { + padding: var(--space-6); +} + +.project-card-title { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: var(--space-2); +} + +.project-card-description { + font-size: 0.875rem; + color: var(--text-secondary); + line-height: 1.6; + margin-bottom: var(--space-4); +} + +/* Link Arrow Animation */ +.link-arrow { + display: inline-flex; + align-items: center; + gap: 0.5rem; + color: var(--accent); + font-weight: 500; + transition: all 0.2s ease; +} + +.link-arrow:hover { + gap: 0.75rem; + color: var(--text-primary); +} + +/* Featured Badge */ +.badge-featured { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.75rem; + background: var(--accent); + color: #0A0E17; + border-radius: var(--radius-full); + font-size: 0.625rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* Star Rating */ +.star-rating { + display: flex; + align-items: center; + gap: 2px; +} + +.star-filled { + color: #f59e0b; +} + +.star-empty { + color: rgba(100, 116, 139, 0.3); +} + +/* Carousel */ +.carousel { + position: relative; + overflow: hidden; + border-radius: var(--radius-xl); + margin-bottom: var(--space-12); +} + +.carousel-slide { + position: relative; + min-height: 400px; + display: flex; + align-items: center; + justify-content: center; + background-size: cover; + background-position: center; +} + +.carousel-overlay { + position: absolute; + inset: 0; + background: linear-gradient(to right, rgba(10, 14, 23, 0.9) 0%, rgba(10, 14, 23, 0.5) 100%); +} + +.carousel-content { + position: relative; + z-index: 1; + padding: var(--space-12); + max-width: 600px; +} + +.carousel-title { + font-family: var(--font-display); + font-size: 2.5rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: var(--space-4); +} + +.carousel-subtitle { + font-size: 1.125rem; + color: var(--text-secondary); + margin-bottom: var(--space-6); + line-height: 1.7; +} + +.carousel-dots { + display: flex; + justify-content: center; + gap: var(--space-2); + padding: var(--space-4) 0; +} + +.carousel-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--border-color); + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.carousel-dot.active { + background: var(--accent); + box-shadow: 0 0 8px rgba(0, 212, 255, 0.4); +} + +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-in { + animation: fadeIn 0.4s ease-out forwards; +} + +.delay-1 { animation-delay: 0.1s; } +.delay-2 { animation-delay: 0.2s; } +.delay-3 { animation-delay: 0.3s; } +`; + archive.append(indexCss, { name: 'src/styles/index.css' }); + } + + /** + * Add reusable components + */ + private addComponents(archive: archiver.Archiver): void { + // Header component + const headerTsx = `import { NavLink } from 'react-router-dom' +import { usePortfolioData } from '../App' + +export const Header = () => { + const data = usePortfolioData() + const profileName = data.profile + ? \`\${data.profile.firstName} \${data.profile.lastName}\` + : 'Portfolio' + + return ( +
+ {profileName} + +
+ ) +} +`; + archive.append(headerTsx, { name: 'src/components/Header.tsx' }); + + // Footer component + const footerTsx = `import { usePortfolioData } from '../App' + +export const Footer = () => { + const data = usePortfolioData() + const profileName = data.profile + ? \`\${data.profile.firstName} \${data.profile.lastName}\` + : 'Portfolio' + + return ( +
+

+ © {new Date().getFullYear()} {profileName}. Built with Ark.Alliance.Portfolio +

+
+ ) +} +`; + archive.append(footerTsx, { name: 'src/components/Footer.tsx' }); + + // Components index + archive.append(`export * from './Header'\nexport * from './Footer'\n`, { name: 'src/components/index.ts' }); + } + + /** + * Add page components + */ + private addPages(archive: archiver.Archiver, data: PortfolioData): void { + // Home page with carousel + const homeTsx = `import { useState, useEffect } from 'react' +import { Header, Footer } from '../components' +import { usePortfolioData } from '../App' +import { Link } from 'react-router-dom' + +export const HomePage = () => { + const data = usePortfolioData() + const profile = data.profile + const carousel = data.carousel || [] + const [currentSlide, setCurrentSlide] = useState(0) + + // Auto-advance carousel + useEffect(() => { + if (carousel.length <= 1) return + const timer = setInterval(() => { + setCurrentSlide(prev => (prev + 1) % carousel.length) + }, 5000) + return () => clearInterval(timer) + }, [carousel.length]) + + return ( +
+
+
+ {/* Carousel Section */} + {carousel.length > 0 && ( +
+
+ {carousel.map((item, index) => ( +
+
+

+ {item.title} +

+

+ {item.subtitle} +

+
+
+ ))} + {carousel.length > 1 && ( +
+ {carousel.map((_, index) => ( +
+ )} +
+
+ )} + + {/* Hero Section - Professional */} +
+

+ {profile?.title || 'Welcome'} +

+

+ {profile ? \`\${profile.firstName} \` : 'Portfolio'} + {profile?.lastName || ''} +

+ {profile?.overview && ( +
+ {profile.overview} +
+ )} +
+ View Projects → + + View Resume + +
+
+ + {/* Recent Projects */} +
+

+ Recent Projects +

+
+ {data.projects.slice(0, 3).map(project => ( + +

{project.title}

+

+ {project.description?.substring(0, 100)}... +

+ + ))} +
+
+
+
+
+ ) +} +`; + archive.append(homeTsx, { name: 'src/pages/Home.tsx' }); + + // Resume page with tabs (matching Admin pattern) + const resumeTsx = `import { useState } from 'react' +import { Header, Footer } from '../components' +import { usePortfolioData } from '../App' + +// Tab definition +const RESUME_TABS = [ + { id: 'experience', label: 'Experience', icon: '💼' }, + { id: 'education', label: 'Education', icon: '🎓' }, + { id: 'skills', label: 'Skills', icon: '💻' }, + { id: 'languages', label: 'Languages', icon: '🌐' }, + { id: 'domains', label: 'Domains', icon: '🏛️' }, + { id: 'hobbies', label: 'Hobbies', icon: '❤️' }, +] + +export const ResumePage = () => { + const data = usePortfolioData() + const [activeTab, setActiveTab] = useState('experience') + + // Helper to render star rating + const renderStars = (value: number) => { + return Array.from({ length: 5 }, (_, i) => ( + + )) + } + + // Filter tabs based on available data + const availableTabs = RESUME_TABS.filter(tab => { + switch (tab.id) { + case 'experience': return data.experiences?.length > 0 + case 'education': return data.education?.length > 0 + case 'skills': return data.skills?.length > 0 + case 'languages': return data.languages?.length > 0 + case 'domains': return data.businessDomains?.length > 0 + case 'hobbies': return data.hobbies?.length > 0 + default: return true + } + }) + + return ( +
+
+
+

Resume

+

Professional experience, education, and skills

+ + {/* Tab Navigation - Pills Style */} +
+
+ {availableTabs.map(tab => ( + + ))} +
+
+ + {/* Tab Content */} +
+ {/* Experience Tab */} + {activeTab === 'experience' && ( +
+ {data.experiences.map(exp => ( +
+

{exp.position}

+

{exp.company}

+

+ {exp.startDate} - {exp.endDate || 'Present'} +

+

+ {exp.description} +

+
+ ))} +
+ )} + + {/* Education Tab */} + {activeTab === 'education' && ( +
+ {data.education.map(edu => ( +
+

{edu.degree}

+

{edu.institution}

+

+ {edu.startDate} - {edu.endDate || 'Present'} +

+
+ ))} +
+ )} + + {/* Skills Tab */} + {activeTab === 'skills' && ( +
+ {data.skills.map(skill => ( + {skill.name} + ))} +
+ )} + + {/* Languages Tab */} + {activeTab === 'languages' && data.languages && ( +
+ {data.languages.map(lang => ( +
+

{lang.language}

+
+
+ Speaking + {renderStars(lang.speaking)} +
+
+ Writing + {renderStars(lang.writing)} +
+
+ Presenting + {renderStars(lang.presenting)} +
+
+
+ ))} +
+ )} + + {/* Domains Tab */} + {activeTab === 'domains' && data.businessDomains && ( +
+ {data.businessDomains.map(domain => ( +
+
+

{domain.domain}

+ {domain.level} + {domain.yearsOfExperience && ( + + {domain.yearsOfExperience} years + + )} +
+ {domain.description && ( +

+ {domain.description} +

+ )} +
+ ))} +
+ )} + + {/* Hobbies Tab */} + {activeTab === 'hobbies' && data.hobbies && ( +
+ {data.hobbies.map(hobby => ( +
+ {hobby.icon && ( +
{hobby.icon}
+ )} +

{hobby.name}

+ {hobby.description && ( +

+ {hobby.description} +

+ )} +
+ ))} +
+ )} +
+
+
+
+ ) +} +`; + archive.append(resumeTsx, { name: 'src/pages/Resume.tsx' }); + + // Projects page + const projectsTsx = `import { Header, Footer } from '../components' +import { usePortfolioData } from '../App' +import { Link } from 'react-router-dom' + +export const ProjectsPage = () => { + const data = usePortfolioData() + + return ( +
+
+
+

Projects

+

A showcase of my work and technical projects

+ +
+ {data.projects.map(project => ( + +

{project.title}

+

+ {project.status} +

+

+ {project.description} +

+ + ))} +
+
+
+
+ ) +} +`; + archive.append(projectsTsx, { name: 'src/pages/Projects.tsx' }); + + // Project detail page + const projectDetailTsx = `import { useParams, Link } from 'react-router-dom' +import { Header, Footer } from '../components' +import { usePortfolioData } from '../App' + +export const ProjectDetailPage = () => { + const { id } = useParams() + const data = usePortfolioData() + const project = data.projects.find(p => String(p.id) === id) + + if (!project) { + return ( +
+
+
+

Project Not Found

+ + Back to Projects + +
+
+
+ ) + } + + return ( +
+
+
+ + ← Back to Projects + +

{project.title}

+

{project.status}

+

+ {project.description} +

+ + {project.repositoryUrl && ( + + View Repository + + )} +
+
+
+ ) +} +`; + archive.append(projectDetailTsx, { name: 'src/pages/ProjectDetail.tsx' }); + + // Pages index + archive.append(`export * from './Home' +export * from './Resume' +export * from './Projects' +export * from './ProjectDetail' +`, { name: 'src/pages/index.ts' }); + } + + /** + * Add README with deployment guide + */ + private addReadme(archive: archiver.Archiver, profileName: string, data: PortfolioData): void { + const readme = `# ${profileName} - Portfolio Website + +> Generated by **Ark.Alliance.Portfolio – Static Website Generator** +> Generated on: ${new Date().toLocaleString()} + +## 📋 Contents + +This static portfolio includes: +- **Profile**: ${profileName} - ${data.profile?.title || 'Professional'} +- **Projects**: ${data.projects.length} projects +- **Experience**: ${data.experiences.length} positions +- **Education**: ${data.education.length} entries +- **Skills**: ${data.skills.length} skills + +## 🚀 Quick Start + +### Install Dependencies + +\`\`\`bash +npm install +\`\`\` + +### Run Development Server + +\`\`\`bash +npm run dev +\`\`\` + +Open http://localhost:5173 in your browser. + +### Build for Production + +\`\`\`bash +npm run build +\`\`\` + +The production bundle will be in the \`dist/\` folder. + +### Preview Production Build + +\`\`\`bash +npm run preview +\`\`\` + +## 📦 Deployment + +### Netlify (Drag & Drop) +1. Run \`npm run build\` +2. Go to [netlify.com/drop](https://app.netlify.com/drop) +3. Drag the \`dist\` folder +4. Done! Your site is live. + +### Vercel +\`\`\`bash +npx vercel deploy --prod +\`\`\` + +### GitHub Pages +1. Create a new GitHub repository +2. Install gh-pages: \`npm install -D gh-pages\` +3. Add to package.json scripts: \`"deploy": "gh-pages -d dist"\` +4. Run: \`npm run build && npm run deploy\` +5. Access at: \`https://username.github.io/repo-name\` + +### Cloudflare Pages +1. Connect your GitHub repo +2. Build command: \`npm run build\` +3. Build output: \`dist\` +4. Deploy + +### Docker +\`\`\`bash +# Build image +docker build -t portfolio . + +# Run container +docker run -p 80:80 portfolio +\`\`\` + +## 📁 Project Structure + +\`\`\` +├── src/ +│ ├── main.tsx # React entry point +│ ├── App.tsx # Routes (public only) +│ ├── data/ +│ │ └── portfolio.json # Embedded portfolio data +│ ├── pages/ +│ │ ├── Home.tsx +│ │ ├── Resume.tsx +│ │ ├── Projects.tsx +│ │ └── ProjectDetail.tsx +│ ├── components/ +│ │ ├── Header.tsx +│ │ └── Footer.tsx +│ └── styles/ +│ └── index.css +├── package.json +├── vite.config.ts +├── tsconfig.json +└── README.md +\`\`\` + +## 🎨 Features + +- ✅ Responsive design +- ✅ Dark theme +- ✅ React 18 + TypeScript +- ✅ Vite build system +- ✅ Zero backend required +- ✅ SEO meta tags + +## ⚠️ Note + +This is a **static export** - to update content, edit your portfolio in the Ark.Alliance.Portfolio application and re-export. + +--- + +*Generated with ❤️ by Ark.Alliance.Portfolio v1.0* +`; + archive.append(readme, { name: 'README.md' }); + } + + /** + * Add media assets + */ + private async addAssets(archive: archiver.Archiver): Promise { + const uploadsDir = path.join(process.cwd(), 'uploads'); + if (fs.existsSync(uploadsDir)) { + archive.directory(uploadsDir, 'public/uploads'); + } + + // Add assets directory if exists + const assetsDir = path.join(process.cwd(), 'assets'); + if (fs.existsSync(assetsDir)) { + archive.directory(assetsDir, 'public/assets'); + } + } +} + +// Export singleton instance +export const staticGenerationService = new StaticGenerationService(); + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/task.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/task.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..3cb2b3424d3745c3ce232378bd53ecd6e91d9227 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/task.service.ts @@ -0,0 +1,240 @@ +/** + * @fileoverview Task Service + * Business logic for tasks with public/internal visibility and recognition features. + * + * Philosophy: + * - Public queries return only achievements and lessons learned (emulation) + * - Internal queries return all tasks including backlog/ongoing (work management) + * - Deadline performance emphasizes growth, never punishment + * + * @author Armand Richelet-Kleinberg + */ + +import { AppDataSource } from '../config/database'; +import { Task, TaskStatusType } from '../database/entities/task.entity'; +import { + PublicTaskDto, + TaskDto, + AccomplishmentsSummaryDto, + CreateTaskDto, + UpdateTaskDto, + TaskStatus +} from '@arkalliance/startupcms-ai-share'; +import { Repository } from 'typeorm'; + +export class TaskService { + private taskRepository: Repository; + + constructor() { + this.taskRepository = AppDataSource.getRepository(Task); + } + + // ═══════════════════════════════════════════════════════════════════════ + // PUBLIC QUERIES (External Resume - Recognition Focus) + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Get publicly visible tasks for a collaborator. + * Only returns Achieved tasks and Mistakes with lessons learned. + * Ordered by rating and recency for maximum recognition impact. + */ + async getPublicTasks(collaboratorId: string): Promise { + const tasks = await this.taskRepository + .createQueryBuilder('task') + .leftJoinAndSelect('task.project', 'project') + .where('task.collaboratorId = :collaboratorId', { collaboratorId }) + .andWhere( + '(task.status = :achieved OR (task.status = :mistake AND task.lessonLearned = true))', + { achieved: 'achieved', mistake: 'mistake' } + ) + .orderBy('task.isHighlighted', 'DESC') + .addOrderBy('task.averageRating', 'DESC') + .addOrderBy('task.closingDate', 'DESC') + .getMany(); + + return tasks.map(task => this.toPublicDto(task)); + } + + /** + * Get public accomplishments summary statistics. + * Emphasizes positive metrics for recognition. + */ + async getPublicSummary(collaboratorId: string): Promise { + const tasks = await this.getPublicTasks(collaboratorId); + + const ratedTasks = tasks.filter(t => t.ratingCount > 0); + const totalRating = ratedTasks.reduce((sum, t) => sum + t.averageRating, 0); + + const hoursSaved = tasks + .filter(t => t.deadlinePerformanceHours != null && t.deadlinePerformanceHours > 0) + .reduce((sum, t) => sum + (t.deadlinePerformanceHours || 0), 0); + + return { + totalAchieved: tasks.filter(t => t.status === TaskStatus.Achieved).length, + overallAverageRating: ratedTasks.length > 0 ? totalRating / ratedTasks.length : 0, + totalHoursSaved: hoursSaved, + lessonsLearnedCount: tasks.filter(t => t.lessonLearned).length, + highlightedCount: tasks.filter(t => t.isHighlighted).length + }; + } + + // ═══════════════════════════════════════════════════════════════════════ + // INTERNAL QUERIES (Authenticated Access) + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Get all tasks for a collaborator (internal view). + * Includes all statuses: backlog, ongoing, achieved, mistake. + */ + async getAllTasks(collaboratorId: string): Promise { + const tasks = await this.taskRepository.find({ + where: { collaboratorId }, + relations: ['project'], + order: { displayOrder: 'ASC', createdAt: 'DESC' } + }); + + return tasks.map(task => this.toFullDto(task)); + } + + /** + * Get a single task by ID (internal). + */ + async getById(id: string): Promise { + const task = await this.taskRepository.findOne({ + where: { id }, + relations: ['project', 'collaborator'] + }); + + return task ? this.toFullDto(task) : null; + } + + // ═══════════════════════════════════════════════════════════════════════ + // MUTATIONS + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Create a new task. + */ + async create(collaboratorId: string, data: CreateTaskDto): Promise { + const task = this.taskRepository.create({ + ...data, + collaboratorId, + status: data.status || 'backlog', + dueDate: data.dueDate ? new Date(data.dueDate) : undefined + }); + + await this.taskRepository.save(task); + return this.toFullDto(task); + } + + /** + * Update a task. + * Automatically calculates deadline performance when closing. + */ + async update(id: string, data: UpdateTaskDto): Promise { + const task = await this.taskRepository.findOne({ where: { id } }); + if (!task) return null; + + // Auto-set closing date when marking as achieved or mistake + if (data.closingDate) { + task.closingDate = new Date(data.closingDate); + } else if ( + (data as any).status && + ['achieved', 'mistake'].includes((data as any).status) && + !task.closingDate + ) { + task.closingDate = new Date(); + } + + Object.assign(task, data); + await this.taskRepository.save(task); + + return this.toFullDto(task); + } + + /** + * Add a peer rating to a task. + * Recalculates the running average. + */ + async addRating(id: string, rating: number): Promise { + const task = await this.taskRepository.findOne({ where: { id } }); + if (!task) return null; + + // Calculate new running average + const totalPoints = task.averageRating * task.ratingCount + rating; + task.ratingCount += 1; + task.averageRating = totalPoints / task.ratingCount; + + await this.taskRepository.save(task); + return this.toFullDto(task); + } + + /** + * Delete a task. + */ + async delete(id: string): Promise { + const result = await this.taskRepository.delete(id); + return (result.affected || 0) > 0; + } + + // ═══════════════════════════════════════════════════════════════════════ + // DTO TRANSFORMATIONS + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Transform to public DTO (recognition-focused, limited fields). + */ + private toPublicDto(task: Task): PublicTaskDto { + return { + id: task.id, + title: task.title, + description: task.description, + role: task.role, + status: task.status as TaskStatus.Achieved | TaskStatus.Mistake, + projectName: task.project?.title, + projectId: task.projectId, + averageRating: Math.round(task.averageRating * 10) / 10, // 1 decimal + ratingCount: task.ratingCount, + deadlinePerformanceHours: this.calculateDeadlinePerformance(task), + lessonLearned: task.lessonLearned, + lessonTitle: task.lessonLearned ? task.lessonTitle : undefined, + closingDate: task.closingDate?.toISOString(), + isHighlighted: task.isHighlighted + }; + } + + /** + * Transform to full internal DTO. + */ + private toFullDto(task: Task): TaskDto { + return { + ...this.toPublicDto(task), + status: task.status as TaskStatus, + estimatedDurationHours: task.estimatedDurationHours, + actualDurationHours: task.actualDurationHours, + dueDate: task.dueDate?.toISOString(), + collaboratorId: task.collaboratorId, + collaboratorName: task.collaborator?.fullName, + lessonDescription: task.lessonDescription, + displayOrder: task.displayOrder, + createdAt: task.createdAt.toISOString(), + updatedAt: task.updatedAt.toISOString() + }; + } + + /** + * Calculate deadline performance in hours. + * Positive = ahead of schedule (celebrated) + * Negative = overrun (constructive if lessonLearned) + */ + private calculateDeadlinePerformance(task: Task): number | undefined { + if (task.estimatedDurationHours != null && task.actualDurationHours != null) { + return Math.round((task.estimatedDurationHours - task.actualDurationHours) * 10) / 10; + } + return undefined; + } +} + +export const taskService = new TaskService(); + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/theme-ai.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/theme-ai.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce8da531eb047f044689bd87b590770b7b812038 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/theme-ai.service.ts @@ -0,0 +1,259 @@ +/** + * @fileoverview Theme AI Service + * AI-assisted theme editing and generation. + * + * @module services/theme-ai + * @author Armand Richelet-Kleinberg + * + * @description + * Provides AI-powered theme design assistance: + * - Natural language theme property suggestions + * - Full theme generation from descriptions + * - Conversational theme refinement + * + * Reuses the existing AiService infrastructure for provider management. + * + * @see {@link AiService} for base AI functionality + */ + +import { AiService } from './ai.service'; + +/** + * Message in a theme AI conversation + */ +export interface ThemeAiMessage { + role: 'user' | 'assistant' | 'system'; + content: string; + timestamp: Date; +} + +/** + * Proposed theme property change from AI + */ +export interface ThemePropertyChange { + /** CSS variable or property name */ + property: string; + /** Original value (if editing) */ + originalValue?: string; + /** Suggested new value */ + suggestedValue: string; + /** Explanation of the change */ + reason: string; +} + +/** + * AI response for theme suggestions + */ +export interface ThemeAiResponse { + /** Chat response message */ + message: string; + /** Proposed CSS changes */ + changes?: ThemePropertyChange[]; + /** Generated CSS snippet */ + cssSnippet?: string; + /** Error if request failed */ + error?: string; +} + +/** + * Request for AI theme assistance + */ +export interface ThemeAiRequest { + /** User's message/prompt */ + message: string; + /** Current theme CSS content */ + currentCss?: string; + /** Conversation history */ + history?: ThemeAiMessage[]; + /** Current theme name for context */ + themeName?: string; +} + +/** + * System prompt for theme design assistance + */ +const THEME_SYSTEM_PROMPT = `You are an expert CSS/SCSS theme designer specializing in cyber-futuristic, neon-glow aesthetics. + +Your task is to help users modify and create themes through natural language. + +When the user describes style changes: +1. Suggest specific CSS variable changes +2. Explain the visual impact of each change +3. Provide the CSS code snippet + +Available CSS variables to modify: +- --neon-primary, --neon-cyan, --neon-accent (main colors) +- --bg-deep, --bg-surface, --bg-overlay (backgrounds) +- --text-neon, --text-neon-secondary, --text-neon-muted (text colors) +- --border-neon, --border-neon-bright (borders) +- --glow-soft, --glow-medium, --glow-strong (glow effects) +- --radius-sm, --radius-md, --radius-lg, --radius-xl (border radius) +- --glass-blur-light, --glass-blur-medium (blur effects) + +Respond with JSON in this format: +{ + "message": "Your explanation to the user", + "changes": [ + { + "property": "--variable-name", + "originalValue": "old value if known", + "suggestedValue": "new value", + "reason": "why this change helps" + } + ], + "cssSnippet": ":root { /* the CSS code */ }" +} + +Be creative but practical. Focus on cohesive color palettes and visual harmony.`; + +/** + * Theme AI Service + * + * @class ThemeAiService + * @description Provides AI-powered theme design assistance using existing AI infrastructure. + */ +export class ThemeAiService { + private aiService: AiService; + + constructor() { + this.aiService = new AiService(); + } + + /** + * Process a theme chat message + */ + async chat(request: ThemeAiRequest): Promise { + try { + // Build context with conversation history + let contextPrompt = THEME_SYSTEM_PROMPT; + + if (request.currentCss) { + contextPrompt += `\n\nCurrent theme CSS:\n\`\`\`css\n${request.currentCss.substring(0, 2000)}\n\`\`\``; + } + + if (request.themeName) { + contextPrompt += `\n\nCurrent theme name: ${request.themeName}`; + } + + // Build prompt with history + let prompt = request.message; + if (request.history && request.history.length > 0) { + const historyText = request.history + .slice(-5) // Last 5 messages for context + .map(m => `${m.role}: ${m.content}`) + .join('\n'); + prompt = `Previous conversation:\n${historyText}\n\nUser: ${request.message}`; + } + + const response = await this.aiService.generateCompletion({ + prompt, + context: contextPrompt, + maxTokens: 1500 + }); + + if (response.error) { + return { message: '', error: response.error }; + } + + // Parse JSON response + try { + const parsed = JSON.parse(response.result); + return { + message: parsed.message || '', + changes: parsed.changes || [], + cssSnippet: parsed.cssSnippet + }; + } catch { + // If not valid JSON, return as plain message + return { message: response.result }; + } + } catch (error) { + return { + message: '', + error: (error as Error).message || 'Failed to process request' + }; + } + } + + /** + * Generate a complete theme from a description + */ + async generateTheme(description: string): Promise { + const prompt = `Create a complete cyber-futuristic theme based on this description: +"${description}" + +Generate CSS variables for all available properties. Make it cohesive and visually striking. +Include at least: +- 3 primary/accent colors +- 3 background shades +- 3 text color variants +- Border and glow settings`; + + try { + const response = await this.aiService.generateCompletion({ + prompt, + context: THEME_SYSTEM_PROMPT, + maxTokens: 2000 + }); + + if (response.error) { + return { message: '', error: response.error }; + } + + try { + const parsed = JSON.parse(response.result); + return { + message: parsed.message || 'Theme generated successfully', + changes: parsed.changes || [], + cssSnippet: parsed.cssSnippet + }; + } catch { + return { message: response.result }; + } + } catch (error) { + return { + message: '', + error: (error as Error).message || 'Failed to generate theme' + }; + } + } + + /** + * Suggest improvements for existing CSS + */ + async suggestImprovements(css: string, goal?: string): Promise { + let prompt = `Analyze this theme CSS and suggest improvements for better visual appeal and consistency:\n\n${css.substring(0, 3000)}`; + + if (goal) { + prompt += `\n\nThe user wants to achieve: ${goal}`; + } + + try { + const response = await this.aiService.generateCompletion({ + prompt, + context: THEME_SYSTEM_PROMPT, + maxTokens: 1500 + }); + + if (response.error) { + return { message: '', error: response.error }; + } + + try { + const parsed = JSON.parse(response.result); + return { + message: parsed.message || '', + changes: parsed.changes || [], + cssSnippet: parsed.cssSnippet + }; + } catch { + return { message: response.result }; + } + } catch (error) { + return { + message: '', + error: (error as Error).message || 'Failed to suggest improvements' + }; + } + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/theme.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/theme.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..2504ed53d7c95360920c1b221c834add525918e6 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/theme.service.ts @@ -0,0 +1,103 @@ +/** + * @fileoverview Theme Service + * Handles business logic for theme-related operations. + * + * @module services/theme.service + * @author Armand Richelet-Kleinberg + */ + +import { ThemeRepository } from '../database/repositories/theme.repository'; +import { Theme } from '../database/entities/theme.entity'; + +/** + * DTO for theme list items (without CSS content). + */ +export interface ThemeListItemDto { + id: number; + name: string; + slug: string; + description?: string; + previewColor?: string; + icon?: string; + isDefault: boolean; + order: number; +} + +/** + * DTO for theme detail (with CSS content). + */ +export interface ThemeDetailDto extends ThemeListItemDto { + cssContent: string; +} + +/** + * Service class for theme-related operations. + * Provides methods for listing themes and retrieving theme CSS content. + */ +export class ThemeService { + private repository: ThemeRepository; + + constructor() { + this.repository = new ThemeRepository(); + } + + /** + * Retrieves all active themes for display in theme selector. + * CSS content is excluded for list performance. + * + * @returns Promise resolving to array of theme list items + */ + async listThemes(): Promise { + const themes = await this.repository.findAllActive(); + return themes.map(this.mapToListItem); + } + + /** + * Retrieves a specific theme by slug with full CSS content. + * + * @param slug - Theme slug identifier + * @returns Promise resolving to theme detail or null if not found + */ + async getThemeBySlug(slug: string): Promise { + const theme = await this.repository.findBySlug(slug); + if (!theme) return null; + return this.mapToDetail(theme); + } + + /** + * Retrieves the default theme with full CSS content. + * + * @returns Promise resolving to default theme or null if none set + */ + async getDefaultTheme(): Promise { + const theme = await this.repository.findDefault(); + if (!theme) return null; + return this.mapToDetail(theme); + } + + /** + * Maps Theme entity to ThemeListItemDto (without CSS). + */ + private mapToListItem(theme: Theme): ThemeListItemDto { + return { + id: theme.id, + name: theme.name, + slug: theme.slug, + description: theme.description, + previewColor: theme.previewColor, + icon: theme.icon, + isDefault: theme.isDefault, + order: theme.order + }; + } + + /** + * Maps Theme entity to ThemeDetailDto (with CSS). + */ + private mapToDetail(theme: Theme): ThemeDetailDto { + return { + ...this.mapToListItem(theme), + cssContent: theme.cssContent + }; + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/services/widget.service.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/services/widget.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..28a5a022252dad2d228f3ba634cf7d3433fc3a36 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/services/widget.service.ts @@ -0,0 +1,23 @@ +import { WidgetRepository } from '../database/repositories/widget.repository'; +import { Widget } from '../database/entities/widget.entity'; + +export class WidgetService { + private widgetRepo: WidgetRepository; + + constructor() { + this.widgetRepo = new WidgetRepository(); + } + + async getHomeWidgets(): Promise { + return this.widgetRepo.findByContext('HOME'); + } + + async getProjectWidgets(projectId: string): Promise { + return this.widgetRepo.findByContext(`PROJECT_${projectId}`); + } + + async createWidget(data: Partial): Promise { + return this.widgetRepo.create(data); + } +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/utils/encryption.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/utils/encryption.ts new file mode 100644 index 0000000000000000000000000000000000000000..e79adbeee350c6711813d9a6060369b47ed1633e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/utils/encryption.ts @@ -0,0 +1,237 @@ +/** + * @fileoverview Encryption Utility + * AES-256-GCM encryption/decryption for sensitive data storage. + * + * @module utils/encryption + * @author Armand Richelet-Kleinberg + * @since 1.0.0 + * + * @description + * Provides secure encryption and decryption functions using industry-standard + * AES-256-GCM algorithm with PBKDF2 key derivation. Designed for storing + * sensitive data like API keys in the database. + * + * @security + * - Uses AES-256-GCM authenticated encryption + * - PBKDF2 with 100,000 iterations for key derivation + * - Random salt and IV for each encryption operation + * - Authentication tag prevents tampering + * + * @example + * // Encrypt an API key before storage + * import { encrypt, decrypt, maskApiKey } from './encryption'; + * + * const encrypted = encrypt('sk-abc123...'); + * // Store encrypted in database + * + * // Decrypt when needed + * const apiKey = decrypt(encrypted); + * + * // Mask for display + * const masked = maskApiKey(apiKey); // 'sk-a...3xyz' + * + * @requires ENCRYPTION_KEY environment variable for production use + */ + +import * as crypto from 'crypto'; + +/** + * Encryption algorithm identifier. + * @constant {string} + * @private + */ +const ALGORITHM = 'aes-256-gcm'; + +/** + * Initialization vector length in bytes. + * @constant {number} + * @private + */ +const IV_LENGTH = 16; + +/** + * Authentication tag length in bytes. + * @constant {number} + * @private + */ +const TAG_LENGTH = 16; + +/** + * Salt length in bytes for key derivation. + * @constant {number} + * @private + */ +const SALT_LENGTH = 64; + +/** + * Derived key length in bytes. + * @constant {number} + * @private + */ +const KEY_LENGTH = 32; + +/** + * PBKDF2 iteration count for key derivation. + * @constant {number} + * @private + * @remarks Higher values increase security but slow down encryption/decryption + */ +const ITERATIONS = 100000; + +/** + * Get the encryption key from environment. + * + * @returns {string} The encryption key + * @private + * + * @description + * Retrieves the ENCRYPTION_KEY from environment variables. + * Falls back to a default key if not set (NOT SECURE FOR PRODUCTION). + * + * @security + * - MUST set ENCRYPTION_KEY in production environment + * - Key should be at least 32 characters + * - Use a cryptographically secure random string + * - Store securely (secrets manager recommended) + */ +function getEncryptionKey(): string { + const key = process.env.ENCRYPTION_KEY; + if (!key) { + console.warn('⚠️ ENCRYPTION_KEY not set, using default (NOT SECURE FOR PRODUCTION)'); + return 'ark-portfolio-default-encryption-key-change-me'; + } + return key; +} + +/** + * Derive a cryptographic key from password and salt. + * + * @param password - The master password/key + * @param salt - Random salt buffer + * @returns {Buffer} Derived key buffer + * @private + * + * @description + * Uses PBKDF2 with SHA-512 for secure key derivation. + * The high iteration count makes brute-force attacks impractical. + */ +function deriveKey(password: string, salt: Buffer): Buffer { + return crypto.pbkdf2Sync(password, salt, ITERATIONS, KEY_LENGTH, 'sha512'); +} + +/** + * Encrypt a plaintext string using AES-256-GCM. + * + * @param plaintext - The string to encrypt + * @returns {string} Base64-encoded encrypted string (salt + iv + tag + ciphertext) + * + * @description + * Encrypts the input using AES-256-GCM authenticated encryption. + * The result includes all components needed for decryption. + * + * @example + * const encrypted = encrypt('my-secret-api-key'); + * // Returns: 'base64-encoded-string' + * + * @remarks + * - Returns empty string for empty/null input + * - Each call produces different output due to random salt/IV + * - Output format: base64(salt || iv || tag || ciphertext) + */ +export function encrypt(plaintext: string): string { + if (!plaintext) return ''; + + const salt = crypto.randomBytes(SALT_LENGTH); + const iv = crypto.randomBytes(IV_LENGTH); + const key = deriveKey(getEncryptionKey(), salt); + + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); + const encrypted = Buffer.concat([ + cipher.update(plaintext, 'utf8'), + cipher.final() + ]); + + const tag = cipher.getAuthTag(); + + // Combine: salt + iv + tag + ciphertext + const combined = Buffer.concat([salt, iv, tag, encrypted]); + return combined.toString('base64'); +} + +/** + * Decrypt an encrypted string using AES-256-GCM. + * + * @param encryptedBase64 - Base64-encoded encrypted string + * @returns {string} Decrypted plaintext string + * + * @description + * Decrypts data that was encrypted with the encrypt() function. + * Verifies the authentication tag to ensure data integrity. + * + * @example + * const encrypted = encrypt('secret'); + * const decrypted = decrypt(encrypted); + * console.log(decrypted); // 'secret' + * + * @remarks + * - Returns empty string for empty/null input + * - Returns empty string if decryption fails (logs error) + * - Requires same ENCRYPTION_KEY used during encryption + */ +export function decrypt(encryptedBase64: string): string { + if (!encryptedBase64) return ''; + + try { + const combined = Buffer.from(encryptedBase64, 'base64'); + + // Extract components + const salt = combined.subarray(0, SALT_LENGTH); + const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH); + const tag = combined.subarray(SALT_LENGTH + IV_LENGTH, SALT_LENGTH + IV_LENGTH + TAG_LENGTH); + const ciphertext = combined.subarray(SALT_LENGTH + IV_LENGTH + TAG_LENGTH); + + const key = deriveKey(getEncryptionKey(), salt); + + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); + decipher.setAuthTag(tag); + + const decrypted = Buffer.concat([ + decipher.update(ciphertext), + decipher.final() + ]); + + return decrypted.toString('utf8'); + } catch (error) { + console.error('🔐 Decryption failed:', error); + return ''; + } +} + +/** + * Mask an API key for safe display. + * + * @param key - The API key to mask + * @returns {string} Masked key showing first 4 and last 4 characters + * + * @description + * Creates a masked version of an API key suitable for display + * in logs, UI, or error messages without exposing the full key. + * + * @example + * maskApiKey('sk-abc123456789xyz'); + * // Returns: 'sk-a...9xyz' + * + * @example + * maskApiKey('short'); + * // Returns: '****' + * + * @remarks + * - Keys shorter than 10 characters return '****' + * - Shows 4 characters at start and end + * - Use for logging and UI display only + */ +export function maskApiKey(key: string): string { + if (!key || key.length < 10) return '****'; + return `${key.substring(0, 4)}...${key.substring(key.length - 4)}`; +} + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/utils/image-converter.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/utils/image-converter.ts new file mode 100644 index 0000000000000000000000000000000000000000..c75b6a01dc7ef43105fc4612d47c4ec3ce1ca0fc --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/utils/image-converter.ts @@ -0,0 +1,158 @@ +/** + * @fileoverview Image to Base64 Converter Utility + * Converts image files (URL or file path) to base64-encoded strings. + * + * @author Armand Richelet-Kleinberg + */ + +import fs from 'fs/promises'; +import path from 'path'; +import https from 'https'; +import http from 'http'; + +/** + * Convert an image to base64 string + * Supports both local file paths and remote URLs + * + * @param imageSource - File path or URL to the image + * @returns Promise Base64-encoded image with data URI prefix + */ +export async function convertImageToBase64(imageSource: string): Promise { + if (imageSource.startsWith('http://') || imageSource.startsWith('https://')) { + // Handle remote URL + return convertUrlToBase64(imageSource); + } else { + // Handle local file path + return convertFileToBase64(imageSource); + } +} + +/** + * Convert a local file to base64 + */ +async function convertFileToBase64(filePath: string): Promise { + try { + // Resolve relative paths + const absolutePath = path.isAbsolute(filePath) + ? filePath + : path.join(process.cwd(), filePath); + + // Read file as buffer + const buffer = await fs.readFile(absolutePath); + + // Determine MIME type from extension + const ext = path.extname(filePath).toLowerCase(); + const mimeType = getMimeType(ext); + + // Convert to base64 + const base64 = buffer.toString('base64'); + + return `data:${mimeType};base64,${base64}`; + } catch (error) { + throw new Error(`Failed to convert file to base64: ${(error as Error).message}`); + } +} + +/** + * Convert a remote URL to base64 + */ +async function convertUrlToBase64(url: string): Promise { + return new Promise((resolve, reject) => { + const protocol = url.startsWith('https://') ? https : http; + + protocol.get(url, (response) => { + if (response.statusCode !== 200) { + reject(new Error(`Failed to fetch image: HTTP ${response.statusCode}`)); + return; + } + + const chunks: Buffer[] = []; + + response.on('data', (chunk) => { + chunks.push(chunk); + }); + + response.on('end', () => { + const buffer = Buffer.concat(chunks); + const mimeType = response.headers['content-type'] || 'image/jpeg'; + const base64 = buffer.toString('base64'); + + resolve(`data:${mimeType};base64,${base64}`); + }); + + response.on('error', (error) => { + reject(new Error(`Failed to download image: ${error.message}`)); + }); + }).on('error', (error) => { + reject(new Error(`Failed to fetch image: ${error.message}`)); + }); + }); +} + +/** + * Get MIME type from file extension + */ +function getMimeType(extension: string): string { + const mimeTypes: Record = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', + '.bmp': 'image/bmp', + '.ico': 'image/x-icon' + }; + + return mimeTypes[extension] || 'image/jpeg'; +} + +/** + * Extract base64 data from a data URI + * + * @param dataUri - Data URI string (e.g., "data:image/png;base64,iVBORw0...") + * @returns Base64 string without the data URI prefix + */ +export function extractBase64FromDataUri(dataUri: string): string { + const matches = dataUri.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/); + if (!matches) { + throw new Error('Invalid data URI format'); + } + return matches[2]; +} + +/** + * Get MIME type from a data URI + */ +export function getMimeTypeFromDataUri(dataUri: string): string | null { + const matches = dataUri.match(/^data:([A-Za-z-+\/]+);base64,/); + return matches ? matches[1] : null; +} + +/** + * Save a base64 image to a file + * + * @param base64Data - Base64 string or data URI + * @param outputPath - Where to save the file + * @returns Promise + */ +export async function saveBase64ToFile(base64Data: string, outputPath: string): Promise { + try { + // Extract base64 if it's a data URI + const base64 = base64Data.startsWith('data:') + ? extractBase64FromDataUri(base64Data) + : base64Data; + + // Convert to buffer + const buffer = Buffer.from(base64, 'base64'); + + // Ensure directory exists + const dir = path.dirname(outputPath); + await fs.mkdir(dir, { recursive: true }); + + // Write file + await fs.writeFile(outputPath, buffer); + } catch (error) { + throw new Error(`Failed to save base64 to file: ${(error as Error).message}`); + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Backend/src/utils/port-killer.ts b/Ark.Alliance.StartupCms.Ai.Backend/src/utils/port-killer.ts new file mode 100644 index 0000000000000000000000000000000000000000..56160a84a5b8a2bbe08234ecbbff4d5e746552d4 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/src/utils/port-killer.ts @@ -0,0 +1,137 @@ +/** + * @fileoverview Port Killer Utility + * Ensures a port is free before server startup. + * Includes proper delays and retry logic to handle race conditions. + * + * @author Armand Richelet-Kleinberg + */ + +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +/** + * Delay helper function. + * @param ms - Milliseconds to wait + */ +const delay = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); + +/** + * Check if port is currently in use. + * @param port - Port number to check + * @returns True if port is in use + */ +async function isPortInUse(port: number): Promise { + try { + if (process.platform === 'win32') { + const { stdout } = await execAsync(`netstat -ano | findstr LISTENING | findstr :${port}`); + // Check if any line actually contains our exact port in LISTENING state + const lines = stdout.split('\n').filter(line => { + const match = line.match(/:(\d+)\s+/); + return match && parseInt(match[1]) === port; + }); + return lines.length > 0; + } else { + const { stdout } = await execAsync(`lsof -t -i:${port}`); + return stdout.trim().length > 0; + } + } catch { + return false; // Error means port not in use + } +} + +/** + * Kills all processes using a specific port on Windows. + * @param port - Port number to free + */ +async function killProcessesOnPort(port: number): Promise { + try { + if (process.platform === 'win32') { + // Find PIDs listening on the exact port + const { stdout } = await execAsync(`netstat -ano | findstr LISTENING | findstr :${port}`); + if (!stdout) return; + + const pids = new Set(); + const lines = stdout.split('\n').filter(line => line.trim()); + + for (const line of lines) { + // Match exact port (e.g., :8085 not :80850) + const portMatch = line.match(/:(\d+)\s+/); + if (portMatch && parseInt(portMatch[1]) === port) { + const parts = line.trim().split(/\s+/); + const pid = parseInt(parts[parts.length - 1]); + if (pid > 0 && pid !== process.pid) { + pids.add(pid); + } + } + } + + // Kill each unique PID + for (const pid of pids) { + console.log(`Killing process ${pid} on port ${port}...`); + try { + await execAsync(`taskkill /F /PID ${pid}`); + } catch { + // Process might already be dead + } + } + } else { + // Linux/Mac + try { + const { stdout } = await execAsync(`lsof -t -i:${port}`); + if (stdout) { + const pids = stdout.split('\n').filter(p => p.trim()); + for (const pid of pids) { + if (parseInt(pid) !== process.pid) { + await execAsync(`kill -9 ${pid}`); + } + } + } + } catch { + // Ignore errors + } + } + } catch { + // Ignore errors + } +} + +/** + * Ensures a port is free by killing any processes using it. + * Includes retry logic and delays to handle race conditions. + * + * @param port - Port number to free + * @param maxRetries - Maximum number of retry attempts (default: 3) + * @param delayMs - Delay between retries in milliseconds (default: 500) + */ +export const killPort = async ( + port: number, + maxRetries: number = 3, + delayMs: number = 500 +): Promise => { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + // Check if port is in use + const inUse = await isPortInUse(port); + + if (!inUse) { + return; // Port is free + } + + console.log(`Port ${port} is in use. Attempt ${attempt}/${maxRetries} to free it...`); + + // Kill processes + await killProcessesOnPort(port); + + // Wait for processes to fully terminate + await delay(delayMs); + } + + // Final check + const stillInUse = await isPortInUse(port); + if (stillInUse) { + console.warn(`Warning: Port ${port} may still be in use after ${maxRetries} attempts.`); + } +}; + + diff --git a/Ark.Alliance.StartupCms.Ai.Backend/tsconfig.json b/Ark.Alliance.StartupCms.Ai.Backend/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..e1ef85993b64cfc6c07df2e25f97d04c0fb9c58c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "rootDir": "./src", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "@arkalliance/startupcms-ai-share": [ + "../Ark.Alliance.StartupCms.Ai.Share/dist/index" + ] + } + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Backend/uploads/other/ffed6876-8ac3-4423-be12-692acaa3ab5e.md b/Ark.Alliance.StartupCms.Ai.Backend/uploads/other/ffed6876-8ac3-4423-be12-692acaa3ab5e.md new file mode 100644 index 0000000000000000000000000000000000000000..632af8289c0458413c8a404e6b818038cfff6a79 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/uploads/other/ffed6876-8ac3-4423-be12-692acaa3ab5e.md @@ -0,0 +1,330 @@ +# Ark Alliance Trading Bot - Position Service + +## Overview + +Autonomous microservice for Binance Futures position tracking and Click Strategy execution. This is the backend that powers the automated trading engine. + +--- + +## Features + +- ✅ **Position Tracking** - Real-time position updates via User Data Stream +- ✅ **Click Strategy Engine** - Automated PnL-based trading with click thresholds +- ✅ **GTX Orders** - Post-Only limit orders for reduced fees (0.02%) +- ✅ **Algo Stop-Limit** - Conditional orders for inversion protection +- ✅ **Market Order Close** - Fast execution with taker fee (0.05%) +- ✅ **Wait Trends** - AI-powered direction analysis using GeminiTrendAnalyzer +- ✅ **WebSocket API** - Low-latency order execution via Binance WS API +- ✅ **Multi-Instance** - Support for multiple exchange accounts +- ✅ **Real-time Market Data** - BookTicker and MiniTicker streaming +- ✅ **Hybrid PnL Monitoring** - WebSocket events + 200ms polling loop + +--- + +## Technology Stack + +| Technology | Purpose | +|------------|---------| +| **Node.js 20+** | Runtime | +| **Express** | HTTP server | +| **TypeScript** | Type safety | +| **SQLite (better-sqlite3)** | Database | +| **Socket.IO** | WebSocket server | +| **Binance Futures API** | Exchange integration | + +--- + +## Prerequisites + +### Node.js Version +- **Recommended**: Node.js v20 LTS +- **Supported**: Node.js >= 18 + +### Windows Build Dependencies (for better-sqlite3) + +`better-sqlite3` is a native module that requires C++ compilation on Windows. + +#### Option 1: Install Visual Studio Build Tools (Recommended) + +1. Download [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) +2. Run the installer +3. Select **"Desktop development with C++"** workload +4. Complete installation and restart terminal + +```powershell +# Then install dependencies +npm install +``` + +#### Option 2: Use Node.js v20 LTS (Prebuilt binaries available) + +Prebuilt binaries for `better-sqlite3` are available for Node.js v20: + +```powershell +# Install nvm-windows if not already installed +# https://github.com/coreybutler/nvm-windows/releases + +nvm install 20 +nvm use 20 +npm install +``` + +#### Option 3: Use sql.js (JavaScript pure alternative) + +If you cannot install Visual Studio Build Tools: + +```powershell +npm uninstall better-sqlite3 +npm install sql.js +``` + +> ⚠️ Note: This requires code modifications to use async API + +--- + +## Installation + +```powershell +# Navigate to project directory +cd Ark.Alliance.Trading.Bot.PositionService + +# Install dependencies +npm install + +# Run development server +npm run dev +``` + +--- + +## Configuration + +### Environment Variables + +Create a `.env` file: + +```env +# Server +PORT=3055 +NODE_ENV=development + +# Binance Environment (TESTNET or MAINNET) +BINANCE_ENV=TESTNET +``` + +### Database + +SQLite database is automatically created at `./data/position-service.db` + +--- + +## Project Structure + +``` +src/ +├── controllers/ # HTTP route handlers +│ ├── InstanceController.ts +│ ├── StrategyController.ts +│ ├── MarketController.ts +│ └── AccountController.ts +├── core/ +│ ├── cache/ # In-memory caches +│ │ ├── PositionCache.ts +│ │ └── OrderCache.ts +│ ├── factory/ # Instance factories +│ │ └── ServicePositionFactory.ts +│ ├── instance/ # Service instances +│ │ ├── PositionServiceInstance.ts +│ │ └── handlers/ # Event handlers +│ ├── services/ # Business logic services +│ │ ├── ClickMonitoringService.ts +│ │ ├── MarketDataService.ts +│ │ ├── GeminiTrendAnalyzerService.ts +│ │ └── FuturesCostAndPnLService.ts +│ └── strategy/ # Click Strategy engine +│ └── click/ +│ ├── ClickStrategyCore.ts +│ ├── ClickStrategyEngine.ts +│ ├── ClickStrategyOrderManager.ts +│ ├── ClickStrategyStateManager.ts +│ └── ClickStrategyEventHandler.ts +├── Data/ +│ ├── Entities/ # Database entities +│ ├── repositories/ # Data access layer +│ ├── schemas/ # SQL schema files +│ └── DatabaseService.ts +├── clients/ # Binance API clients +│ ├── BinanceRestClient.ts +│ ├── BinanceApiWsClient.ts +│ └── BinanceUserDataStream.ts +├── models/ # TypeScript interfaces +│ └── strategy/ +│ └── ClickStrategyConfig.ts +├── helpers/ # Utility functions +└── server.ts # Express server entry +``` + +--- + +## API Endpoints + +### Instance Management + +| Route | Method | Description | +|-------|--------|-------------| +| `/api/instance/list` | GET | List all instances | +| `/api/instance/:key` | GET | Get instance details | +| `/api/instance/start` | POST | Start instance | +| `/api/instance/stop` | POST | Stop instance | + +### Strategy Control + +| Route | Method | Description | +|-------|--------|-------------| +| `/api/strategy/start` | POST | Start Click Strategy | +| `/api/strategy/stop` | POST | Stop Click Strategy | +| `/api/strategy/:instanceKey/:symbol` | GET | Get strategy lifecycle | +| `/api/strategy/:instanceKey/:symbol/monitoring` | GET | Get monitoring indicators | +| `/api/strategy/:instanceKey/:symbol/sigma` | PATCH | Update sigma | +| `/api/strategy/:instanceKey/:symbol/takeProfit` | PATCH | Update take profit | + +### Market Data + +| Route | Method | Description | +|-------|--------|-------------| +| `/api/market/prices` | GET | All cached market prices | +| `/api/market/price/:symbol` | GET | Single symbol price | +| `/api/market/symbols` | GET | List tradeable symbols | +| `/api/market/symbol/:symbol/info` | GET | Get symbol precision/filters | + +### Account + +| Route | Method | Description | +|-------|--------|-------------| +| `/api/account/:instanceKey` | GET | Get account info | +| `/api/account/:instanceKey/positions` | GET | Get positions | +| `/api/account/:instanceKey/orders` | GET | Get open orders | + +### System Status + +| Route | Method | Description | +|-------|--------|-------------| +| `/api/status/metrics` | GET | System metrics | +| `/api/status/memory` | GET | Memory usage | +| `/api/status/cpu` | GET | CPU usage | + +See Swagger UI at `http://localhost:3055/api-docs` for full documentation. + +--- + +## Strategy Engine Logic Reference + +For complete technical documentation of the Click Strategy Engine, see: + +📖 [**Strategy Engine Logic**](./Docs/StrategyEngineLogic.md) + +This comprehensive document includes: + +| Section | Description | +|---------|-------------| +| Service Architecture | Layered, event-driven system design | +| Monitoring Loop | Hybrid PnL strategy (Push + Pull) | +| Click Event Processing | Threshold formulas and state machine | +| Inversion Processing | GTX vs Stop-Limit mutual exclusion | +| TrendCalculatorService | Pre-entry AI direction analysis | +| Market Order Close | Post-inversion with Wait Trends mode | + +--- + +## System Settings + +Default settings managed in `system_settings` table: + +| Key | Default | Description | +|-----|---------|-------------| +| `strategy_click_debounce_ms` | 500 | Min time between CLICKs | +| `strategy_inversion_order_timeout_ms` | 1300 | GTX inversion timeout | +| `strategy_initial_order_timeout_ms` | 30000 | Initial order timeout | +| `strategy_max_inversion_retries` | 5 | Max inversion retries | +| `stop_limit_initial_order_timeout_ms` | 1000 | Stop-limit timeout | +| `market_data_default_symbols` | JSON | Symbols to stream at startup | + +--- + +## WebSocket Events + +### Server → Client + +| Event | Description | +|-------|-------------| +| `positions:snapshot` | All positions for instance | +| `position:update` | Single position update | +| `orders:snapshot` | All orders for instance | +| `strategy:status` | Strategy state changes | +| `click:event` | Click triggered | +| `inversion:event` | Inversion triggered | +| `monitoring:indicators` | Real-time PnL indicators | + +### Client → Server + +| Event | Description | +|-------|-------------| +| `subscribe:instance` | Subscribe to instance updates | +| `unsubscribe:instance` | Unsubscribe from instance | + +--- + +## Troubleshooting + +### npm install fails with gyp errors + +This is related to `better-sqlite3` native compilation. See [Windows Build Dependencies](#windows-build-dependencies-for-better-sqlite3) section above. + +### Connection timeout errors + +Check your Binance API credentials and network connectivity. + +### Strategy not starting + +1. Verify symbol exists and is tradeable +2. Check account has sufficient balance +3. Confirm leverage is set correctly + +### Position updates delayed + +1. Verify User Data Stream is connected +2. Check for WebSocket reconnection in logs +3. Verify ACCOUNT_UPDATE events are being received + +--- + +## Database Schema + +Key tables: + +| Table | Description | +|-------|-------------| +| `positions` | Current position state | +| `orders` | Order history | +| `strategy_sessions` | Strategy session records | +| `strategy_history` | Click/inversion event log | +| `system_settings` | Configuration key-value store | + +--- + +## Related Documentation + +- 📖 [**Position Service UI**](../Ark.Alliance.Trading.Bot.PositionService.Ui/README.md) - React frontend +- 📖 [**Main Project**](../README.md) - Overall architecture + +--- + +## License + +Proprietary - Ark Alliance + +--- + +**Version:** 2.0.0 +**Last Updated:** 2025-12-24 +**Status:** ✅ Production Ready diff --git a/Ark.Alliance.StartupCms.Ai.Backend/uploads/word/1d04f81e-fd89-4974-878e-449b90adfc5f.docx b/Ark.Alliance.StartupCms.Ai.Backend/uploads/word/1d04f81e-fd89-4974-878e-449b90adfc5f.docx new file mode 100644 index 0000000000000000000000000000000000000000..5bf9cc2b39126d24198bb1805cce49a2c851d002 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Backend/uploads/word/1d04f81e-fd89-4974-878e-449b90adfc5f.docx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73e8c6e9b4c854737fe46be558ed134c61dfdb4bb31babc27a206c2e0137af83 +size 2596865 diff --git a/Ark.Alliance.StartupCms.Ai.Share/README.md b/Ark.Alliance.StartupCms.Ai.Share/README.md new file mode 100644 index 0000000000000000000000000000000000000000..67fed533e59d12b64ab8dd2447b9651410a47044 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/README.md @@ -0,0 +1,342 @@ +# Ark.Portfolio.Share + +
+ +![npm](https://img.shields.io/badge/npm-local%20package-orange?style=for-the-badge&logo=npm) +![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue?style=for-the-badge&logo=typescript) +![DTOs](https://img.shields.io/badge/DTOs-15%2B-green?style=for-the-badge) +![Enums](https://img.shields.io/badge/Enums-60%2B%20values-purple?style=for-the-badge) + +**Shared Contract Layer for Frontend and Backend** + +*Type-Safe DTOs • Enumerations • Constants • Mock Data* + +
+ +--- + +The **Shared Library** serves as the contract layer between Frontend and Backend, ensuring type safety and consistency across the entire solution. It provides Data Transfer Objects (DTOs), Enumerations, Constants, and Mock data. + +--- + +## 📦 Functional Capabilities + +| Domain | Capability | Description | Code Reference | +| :--- | :--- | :--- | :--- | +| **Contracts** | **DTOs** | Centralized interfaces ensuring Frontend/Backend type parity. | `dtos/*.dto.ts` | +| **Type Safety** | **Enums** | Strict enumerations for statuses, technologies (60+ values), and categories. | `enums/*.enum.ts` | +| **Constants** | **Terminology** | Unified UI strings and layout configuration. | `constants/*.ts` | +| **Testing** | **Mock Data** | Static datasets synced with backend seeds for testing without API. | `mocks/*.mock.ts` | +| **Exports** | **Barrel File** | Clean public API via index.ts barrel export. | `index.ts` | + +--- + +## 🏗️ Project Structure + +```text +Ark.Portfolio.Share/ +├── 📁 constants/ # Global UX strings & Layout config +│ ├── terminology.constants.ts # UI text strings +│ └── ui-layout.constants.ts # Layout configuration +│ +├── 📁 dtos/ # Data Transfer Objects (15+ files) +│ ├── auth.dto.ts # Authentication DTOs +│ ├── project.dto.ts # Project & Admin DTOs +│ ├── resume.dto.ts # Resume/CV DTOs +│ ├── ai.dto.ts # AI settings DTOs +│ ├── media.dto.ts # Media/upload DTOs +│ ├── carousel.dto.ts # Carousel DTOs +│ ├── crud-response.dto.ts # API response wrappers +│ └── ... # Additional DTOs +│ +├── 📁 enums/ # Typed Enumerations (5+ files) +│ ├── project-status.enum.ts # IN_PROGRESS, COMPLETED, etc. +│ ├── technology.enum.ts # 60+ technologies +│ ├── skill-level.enum.ts # Beginner to Expert +│ └── ... # Additional enums +│ +├── 📁 mocks/ # Static test data +│ ├── index.ts # Mock exports +│ ├── projects.mock.ts # Project mock data +│ ├── cv.mock.ts # CV/Resume mock data +│ └── profile.mock.ts # Profile mock data +│ +├── 📄 index.ts # Public API barrel +├── 📄 package.json # Package configuration +└── 📄 tsconfig.json # TypeScript config +``` + +--- + +## 📐 Architecture + +### DTO Relationships + +```mermaid +classDiagram + class ProjectDto { + +string id + +string title + +string description + +ProjectStatus status + +Technology[] technologies + +string imageUrl + +ProjectFeatureDto[] features + +ProjectPageDto[] pages + } + + class ProjectFeatureDto { + +string id + +string title + +string description + +string icon + } + + class ProjectPageDto { + +string id + +ProjectSection type + +string title + +string content + +number order + } + + class Technology { + <> + REACT + TYPESCRIPT + NODEJS + CSHARP + PYTHON + ...60+ values + } + + ProjectDto *-- ProjectFeatureDto + ProjectDto *-- ProjectPageDto + ProjectDto ..> Technology +``` + +### Data Flow + +```mermaid +sequenceDiagram + participant DB as Database + participant Backend as Backend Service + participant Share as Shared DTO + participant UI as Frontend + + DB->>Backend: Entity (Raw) + Backend->>Backend: Map to DTO + Backend->>Share: Validate against Interface + Backend->>UI: JSON Response + UI->>UI: TypeScript Autocomplete +``` + +--- + +## 📋 Technology Enum + +The `Technology` enum contains **60+ values** organized by category: + +| Category | Examples | +|----------|----------| +| **Frontend** | React, Angular, Vue, Svelte, Next.js, Three.js | +| **Languages** | TypeScript, Python, C#, Go, Rust, COBOL | +| **Runtimes** | Node.js, .NET 5-8, Unity, Unreal | +| **Databases** | PostgreSQL, MongoDB, Redis, SQLite | +| **Cloud** | AWS, Azure, GCP, DigitalOcean | +| **DevOps** | Docker, Kubernetes, Terraform | +| **AI/ML** | PyTorch, TensorFlow, OpenAI, Anthropic | +| **Patterns** | Microservices, CQRS, GraphQL, gRPC | + +--- + +## 🚀 Usage + +### Installation + +This package is referenced locally within the monorepo: + +```json +// package.json in UI or Backend +{ + "dependencies": { + "@ark/portfolio-share": "file:../Ark.Portfolio.Share" + } +} +``` + +### Import Examples + +```typescript +// Import DTOs +import { ProjectDto, ResumeDto } from '@ark/portfolio-share'; + +// Import Enums +import { ProjectStatus, Technology } from '@ark/portfolio-share'; + +// Import Mock Data +import { MOCK_PROJECTS } from '@ark/portfolio-share'; + +// Create typed object +const project: ProjectDto = { + title: "My Project", + status: ProjectStatus.IN_PROGRESS, + technologies: [Technology.REACT, Technology.TYPESCRIPT], + description: "A great project" +}; +``` + +### Build + +```bash +npm install +npm run build +# Output in /dist +``` + +--- + +## 📦 Package Exports + +The `index.ts` barrel file exports: + +```typescript +// Enums +export * from './enums/project-status.enum'; +export * from './enums/technology.enum'; +export * from './enums/skill-level.enum'; +// ... 5 enum files + +// DTOs +export * from './dtos/project.dto'; +export * from './dtos/resume.dto'; +export * from './dtos/auth.dto'; +// ... 15 DTO files + +// Constants +export * from './constants/terminology.constants'; +export * from './constants/ui-layout.constants'; + +// Mocks +export * from './mocks/projects.mock'; +export * from './mocks/cv.mock'; +export * from './mocks/profile.mock'; +``` + +--- + +## 🧪 Testing + +Mock data is used by the test suite: + +```typescript +import { MOCK_PROJECTS } from '@ark/portfolio-share'; + +describe('ProjectsPage', () => { + it('renders projects from mock data', () => { + // MOCK_PROJECTS contains 4 projects synced with backend seeds + expect(MOCK_PROJECTS.length).toBe(4); + }); +}); +``` + +--- + +## 📝 Adding New Types + +### Adding a New DTO + +1. Create `dtos/new-feature.dto.ts`: + ```typescript + export interface NewFeatureDto { + id: string; + name: string; + // ... + } + ``` + +2. Export from `index.ts`: + ```typescript + export * from './dtos/new-feature.dto'; + ``` + +3. Rebuild: + ```bash + npm run build + ``` + +### Adding a New Technology + +Edit `enums/technology.enum.ts`: + +```typescript +export enum Technology { + // ... existing values + + /** New Technology */ + NEW_TECH = 'New Technology' +} +``` + +--- + +## 📏 Best Practices + +### DTO Naming Conventions + +| Type | Convention | Example | +|------|------------|--------| +| Public DTO | `[Entity]Dto` | `ProjectDto` | +| Admin DTO | `Admin[Entity]Dto` | `AdminProjectDto` | +| Create DTO | `Create[Entity]Dto` | `CreateProjectDto` | +| Response | `CrudResponseDto` | `CrudResponseDto` | + +### Enum Guidelines + +- **Always use string values** for JSON serialization +- **Add JSDoc comments** for each value explaining usage +- **Group related values** with comment headers + +```typescript +export enum ProjectStatus { + /** Project is actively being developed */ + IN_PROGRESS = 'In Progress', + /** Project development is finished */ + COMPLETED = 'Completed', +} +``` + +### Mock Data Sync + +Mock data MUST remain synchronized with backend seed data: + +| Mock File | Backend Seed | +|-----------|-------------| +| `projects.mock.ts` | `projects.json` | +| `profile.mock.ts` | `profile.json` | +| `cv.mock.ts` | `cv.json` | + +> **Important**: When updating backend seeds, update corresponding mocks! + +--- + +## 📚 Related Documentation + +| Document | Location | Purpose | +|----------|----------|--------| +| Tests Layer | `../Ark.Portfolio.Tests/README.md` | Test patterns, mocks usage | +| UI Layer | `../Ark.Portfolio.UI/README.md` | Component consumption | +| Backend Layer | `../Ark.Portfolio.Backend/README.md` | DTO mapping, validation | + +--- + +
+ +**Ark.Portfolio.Share** — Part of the Ark Alliance Ecosystem + + +Armand Richelet-Kleinberg © M2H.IO
+AI-assisted development with Anthropic Claude & Google Gemini +
+ +
diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/App/Background.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/App/Background.PNG new file mode 100644 index 0000000000000000000000000000000000000000..67123bc6c42e1b26eda791bebda2efaf640a0e5c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/App/Background.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fed778eebdd7edd933c3df4d6254899a193b5aa80f4010106ebc0bb47819118 +size 141204 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/App/LogoArkAlliance.png b/Ark.Alliance.StartupCms.Ai.Share/assets/App/LogoArkAlliance.png new file mode 100644 index 0000000000000000000000000000000000000000..4b4ab3a60151b93269082263c697f9315917ac6f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/App/LogoArkAlliance.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8f10a10d57e29ca9dcf71ae9b4dc0023d43ae7e96704364cac29c6aec20afd3 +size 220747 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/Bot10.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot10.PNG new file mode 100644 index 0000000000000000000000000000000000000000..545e15c4e95512074d58c91907db7d9c32986997 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot10.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9975e1830cb087bc177751ac162554ebe09ea1ee02dc881b0499954cc6fa13bc +size 321808 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/Bot11.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot11.PNG new file mode 100644 index 0000000000000000000000000000000000000000..2ec8207c17cd6559d1c86786ecd1e013a71d9142 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot11.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39a5c84b9a23784f6ee68772b3292945d557d4681538e1bbe762a50fbaf49747 +size 378429 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/Bot12.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot12.PNG new file mode 100644 index 0000000000000000000000000000000000000000..2ff7f228e3e9359e8a81847e0877c389b4acb3e0 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot12.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:459bb1c112c7a10eb22eb9c9a0d132c4badeaf78e23319e439c119bdc787f376 +size 155251 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/Bot13.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot13.PNG new file mode 100644 index 0000000000000000000000000000000000000000..068b233c1fca51af8aaceef21e3a1862529f0c43 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot13.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:614315756f28c729ae556a3b284460a0d7f63e8e9e3f00f10d752f9dcd840127 +size 170264 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/Bot14.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot14.PNG new file mode 100644 index 0000000000000000000000000000000000000000..0cf2808307669682bfced7468a04ef85b9ae622a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot14.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ac92fa503174a5c4c6685c731a06b77dfe9aa7ff094f8ab3034d68fc8a0c0e1 +size 459307 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/Bot2.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot2.PNG new file mode 100644 index 0000000000000000000000000000000000000000..75fc7e8910a064c50afad413ef8c27bcbf56ae99 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot2.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c25f04b3a256ba08022b0a14e7f3fb8d4c8d5fdb68ab8e5e9390cee46dc753a +size 465243 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/Bot3.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot3.PNG new file mode 100644 index 0000000000000000000000000000000000000000..a5c10232d8c105d6396bc1d03f9a7cecc61dc804 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot3.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b7e290bd1e3c51a836ab9ab613c594a1aa57315dc9a76f961899692e675ed3a +size 397617 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/Bot4.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot4.PNG new file mode 100644 index 0000000000000000000000000000000000000000..6c845c86ac723bbb1e00fc05a65f4c9ed3273a98 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot4.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f322678e0f5a499352eccd3a6a89e0190274fc19ab03ab08ed36c2d8e0731675 +size 213861 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/Bot5.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot5.PNG new file mode 100644 index 0000000000000000000000000000000000000000..d05306ce7a2306af63c75d852f8deedf9544320f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot5.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a74f2a561083dc5041332b70f1ef267ebb3ba2b2fbf72767a84ff7b7e7dec4c +size 215587 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/Bot6.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot6.PNG new file mode 100644 index 0000000000000000000000000000000000000000..4207ff17a1c808bb56db7d3eb524d98fdd33738e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot6.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66435ea297e9f7466e72a3cd1f4671e1e4da1f914521786a3cdcd4987624aad6 +size 505004 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/Bot7.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot7.PNG new file mode 100644 index 0000000000000000000000000000000000000000..fc843ea6466d799b3b000fdf29411bf71a00e75a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot7.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2cea73cf7bf2a1e4dedadad32fc30e2b6720ed513cb0b3c2ee0679aaea46d5a +size 369687 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/Bot8.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot8.PNG new file mode 100644 index 0000000000000000000000000000000000000000..3d7c3f49fa5ae9495584308cf7b4eb1376640a9a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/Bot8.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b554f447aa539e11f277200d0979cd6409c6f49bbb09968d043efce36b6ac12 +size 271418 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/Capture.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/Capture.PNG new file mode 100644 index 0000000000000000000000000000000000000000..fc843ea6466d799b3b000fdf29411bf71a00e75a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/Capture.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2cea73cf7bf2a1e4dedadad32fc30e2b6720ed513cb0b3c2ee0679aaea46d5a +size 369687 diff --git a/Ark.Alliance.StartupCms.Ai.Share/assets/bot1.PNG b/Ark.Alliance.StartupCms.Ai.Share/assets/bot1.PNG new file mode 100644 index 0000000000000000000000000000000000000000..d44cf89a01e264a1cdb87affc4ac800d3606fc40 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/assets/bot1.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcc256615a841b8686d03fa6159e33d37b1a40ccd0365288c946dd9740f0b2de +size 258622 diff --git a/Ark.Alliance.StartupCms.Ai.Share/constants/ai.constants.ts b/Ark.Alliance.StartupCms.Ai.Share/constants/ai.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..6374aa8f1f415a438e5a6b2fce6d788679a989e9 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/constants/ai.constants.ts @@ -0,0 +1,43 @@ +import { AiProviderEnum } from '../enums/ai-provider.enum'; + +/** + * AI Provider Configuration Data. + * Contains metadata about supported models and provider descriptions. + * Icon rendering is handled by the UI layer to keep this shared layer framework-agnostic. + */ +export const AI_PROVIDER_CONFIG = [ + { + id: AiProviderEnum.OPENAI, + name: 'OpenAI', + models: ['gpt-4', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'], + description: 'Industry-leading AI models with strong reasoning capabilities' + }, + { + id: AiProviderEnum.ANTHROPIC, + name: 'Anthropic', + models: ['claude-3-opus', 'claude-3-sonnet', 'claude-3-haiku', 'claude-3.5-sonnet'], + description: 'Advanced AI with focus on safety and helpfulness' + }, + { + id: AiProviderEnum.GOOGLE, + name: 'Google AI', + models: ['gemini-pro', 'gemini-pro-vision', 'gemini-2.0-flash'], + description: 'Multimodal AI models from Google DeepMind' + }, + { + id: AiProviderEnum.CUSTOM, + name: 'Custom Provider', + models: [], + description: 'Configure your own AI endpoint (OpenAI-compatible)' + } +]; + +/** + * Map of available models per provider for quick lookup. + */ +export const PROVIDER_MODELS: Record = { + [AiProviderEnum.OPENAI]: ['gpt-4', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'], + [AiProviderEnum.ANTHROPIC]: ['claude-3-opus', 'claude-3-sonnet', 'claude-3-haiku', 'claude-3.5-sonnet'], + [AiProviderEnum.GOOGLE]: ['gemini-pro', 'gemini-pro-vision', 'gemini-2.0-flash'], + [AiProviderEnum.CUSTOM]: [] +}; diff --git a/Ark.Alliance.StartupCms.Ai.Share/constants/resume.constants.ts b/Ark.Alliance.StartupCms.Ai.Share/constants/resume.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..547e9782a0de9603a41011a64731e95b926cccc9 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/constants/resume.constants.ts @@ -0,0 +1,140 @@ +/** + * @fileoverview Resume Constants + * Centralized constants for resume-related data. + * + * @author Armand Richelet-Kleinberg + */ + +import { SkillLevel } from '../enums/skill-level.enum'; + +// ============================================================================= +// BUSINESS DOMAIN PRESETS +// ============================================================================= + +/** + * Preset list of valid business domains. + * Used for dropdown selection and backend validation. + * + * @remarks + * Add new domains to this list as needed. + * Backend will validate against this list. + */ +export const BUSINESS_DOMAIN_PRESETS = [ + 'Logistics', + 'Finance', + 'Trading', + 'Insurance', + 'Steel Manufacturing', + 'Theatre', + 'Entertainment', + 'Music Playing', + 'Music Composing', + 'Movie Making', + 'Retail', + 'Banking', + 'Asset Management', + 'Supply Chain', + 'Healthcare', + 'E-Commerce', + 'Telecommunications', + 'Real Estate', + 'Energy', + 'Automotive', + 'Gaming', + 'Education', + 'Government', + 'Non-Profit' +] as const; + +/** + * Type for business domain values. + */ +export type BusinessDomainPreset = typeof BUSINESS_DOMAIN_PRESETS[number]; + +/** + * Check if a domain is valid (in preset list). + * @param domain - Domain to check + * @returns True if valid + */ +export function isValidBusinessDomain(domain: string): boolean { + return BUSINESS_DOMAIN_PRESETS.includes(domain as BusinessDomainPreset); +} + +// ============================================================================= +// BUSINESS DOMAIN LEVELS +// ============================================================================= + +/** + * Proficiency levels for business domains. + * Reuses SkillLevel enum for consistency. + */ +export const BUSINESS_DOMAIN_LEVELS = [ + SkillLevel.BEGINNER, + SkillLevel.INTERMEDIATE, + SkillLevel.ADVANCED, + SkillLevel.EXPERT +] as const; + +// ============================================================================= +// LANGUAGE PROFICIENCY +// ============================================================================= + +/** + * Minimum proficiency level value. + */ +export const MIN_PROFICIENCY_LEVEL = 1; + +/** + * Maximum proficiency level value. + */ +export const MAX_PROFICIENCY_LEVEL = 5; + +/** + * Labels for proficiency levels (1-5). + */ +export const PROFICIENCY_LEVEL_LABELS: Record = { + 1: 'Basic', + 2: 'Elementary', + 3: 'Intermediate', + 4: 'Advanced', + 5: 'Native/Fluent' +}; + +/** + * Get proficiency label by value. + * @param level - Level value (1-5) + * @returns Human-readable label + */ +export function getProficiencyLabelByValue(level: number): string { + return PROFICIENCY_LEVEL_LABELS[level] || 'Unknown'; +} + +// ============================================================================= +// HOBBY ICONS +// ============================================================================= + +/** + * Suggested icons for hobbies (Lucide icon names). + */ +export const HOBBY_ICON_SUGGESTIONS = [ + 'Music', + 'Camera', + 'Gamepad2', + 'Palette', + 'Book', + 'Dumbbell', + 'Bike', + 'Plane', + 'Coffee', + 'Film', + 'Monitor', + 'Heart', + 'Star', + 'Sun', + 'Mountain', + 'Globe', + 'Code', + 'Brush', + 'Mic', + 'Headphones' +] as const; diff --git a/Ark.Alliance.StartupCms.Ai.Share/constants/static-generation.constants.ts b/Ark.Alliance.StartupCms.Ai.Share/constants/static-generation.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..571192a681edba89a6c3819aa63a771074d56584 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/constants/static-generation.constants.ts @@ -0,0 +1,189 @@ +/** + * @fileoverview Static Generation Constants + * Constants and defaults for static site generation. + * + * @author Armand Richelet-Kleinberg + */ + +import { StaticPageType, StaticThemePreset, StaticResumeSectionType } from '../enums/static-generation.enum'; +import { PageDefinitionDto } from '../dtos/static-generation.dto'; + +/** + * Default pages for static export. + */ +export const DEFAULT_STATIC_PAGES: PageDefinitionDto[] = [ + { + pageType: StaticPageType.HOME, + title: 'Home', + route: '/', + isEnabled: true, + displayOrder: 1, + metadata: { + showCarousel: true, + showQuickNav: true + } + }, + { + pageType: StaticPageType.RESUME, + title: 'Resume', + route: '/resume', + sections: [ + StaticResumeSectionType.PROFILE, + StaticResumeSectionType.EXPERIENCE, + StaticResumeSectionType.EDUCATION, + StaticResumeSectionType.SKILLS, + StaticResumeSectionType.LANGUAGES, + StaticResumeSectionType.HOBBIES, + StaticResumeSectionType.BUSINESS_DOMAINS + ], + isEnabled: true, + displayOrder: 2 + }, + { + pageType: StaticPageType.PROJECTS, + title: 'Projects', + route: '/projects', + isEnabled: true, + displayOrder: 3 + }, + { + pageType: StaticPageType.PROJECT_DETAIL, + title: 'Project Detail', + route: '/projects/:id', + isEnabled: true, + displayOrder: 4, + metadata: { + generateSubfolders: true + } + } +]; + +/** + * Default export configuration. + */ +export const DEFAULT_STATIC_EXPORT_CONFIG = { + pages: DEFAULT_STATIC_PAGES, + theme: StaticThemePreset.ARCHITECTURAL, + includeProjectSubfolders: true, + includeAssets: true, + siteMetadata: { + keywords: ['portfolio', 'developer', 'projects', 'resume'] + } +}; + +/** + * Theme CSS variables for each preset. + */ +export const STATIC_THEME_CSS_VARS: Record> = { + [StaticThemePreset.ARCHITECTURAL]: { + '--bg-primary': '#0f172a', + '--bg-secondary': '#1e293b', + '--bg-tertiary': '#334155', + '--text-primary': '#f8fafc', + '--text-secondary': '#cbd5e1', + '--text-muted': '#64748b', + '--color-primary-400': '#60a5fa', + '--color-primary-500': '#3b82f6', + '--color-primary-600': '#2563eb', + '--border-color': 'rgba(148, 163, 184, 0.1)', + '--gradient-primary': 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' + }, + [StaticThemePreset.ALOEVERA]: { + '--bg-primary': '#1a0a2e', + '--bg-secondary': '#2d1b4e', + '--bg-tertiary': '#4a2c7a', + '--text-primary': '#f8fafc', + '--text-secondary': '#d8b4fe', + '--text-muted': '#a78bfa', + '--color-primary-400': '#a78bfa', + '--color-primary-500': '#8b5cf6', + '--color-primary-600': '#7c3aed', + '--border-color': 'rgba(167, 139, 250, 0.1)', + '--gradient-primary': 'linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%)' + }, + [StaticThemePreset.LIGHT]: { + '--bg-primary': '#ffffff', + '--bg-secondary': '#f8fafc', + '--bg-tertiary': '#f1f5f9', + '--text-primary': '#0f172a', + '--text-secondary': '#475569', + '--text-muted': '#94a3b8', + '--color-primary-400': '#60a5fa', + '--color-primary-500': '#3b82f6', + '--color-primary-600': '#2563eb', + '--border-color': 'rgba(15, 23, 42, 0.1)', + '--gradient-primary': 'linear-gradient(135deg, #3b82f6 0%, #06b6d4 100%)' + }, + [StaticThemePreset.CUSTOM]: { + '--bg-primary': '#0f172a', + '--bg-secondary': '#1e293b', + '--text-primary': '#f8fafc', + '--text-secondary': '#94a3b8', + '--color-primary-500': '#3b82f6' + } +}; + +/** + * Vite project dependencies for static export. + */ +export const STATIC_VITE_DEPENDENCIES = { + dependencies: { + 'react': '^18.2.0', + 'react-dom': '^18.2.0', + 'react-router-dom': '^6.20.0', + 'lucide-react': '^0.300.0', + 'ark-alliance-react-ui': '^1.1.5' + }, + devDependencies: { + '@types/react': '^18.2.0', + '@types/react-dom': '^18.2.0', + '@vitejs/plugin-react': '^4.2.0', + 'typescript': '^5.3.0', + 'vite': '^5.0.0' + } +}; + +/** + * Files to exclude from asset collection. + */ +export const STATIC_ASSET_EXCLUDES = [ + '.git', + 'node_modules', + '.env', + '.env.local', + 'dist', + 'coverage', + '*.log', + '*.lock' +]; + +/** + * Maximum file size for individual assets (10MB). + */ +export const MAX_STATIC_ASSET_SIZE_BYTES = 10 * 1024 * 1024; + +/** + * Get display name for a page type. + */ +export function getStaticPageDisplayName(pageType: StaticPageType): string { + const names: Record = { + [StaticPageType.HOME]: 'Home Page', + [StaticPageType.RESUME]: 'Resume / CV', + [StaticPageType.PROJECTS]: 'Projects Catalogue', + [StaticPageType.PROJECT_DETAIL]: 'Project Details' + }; + return names[pageType] || pageType; +} + +/** + * Get icon name for a page type (Lucide icon). + */ +export function getStaticPageIcon(pageType: StaticPageType): string { + const icons: Record = { + [StaticPageType.HOME]: 'Home', + [StaticPageType.RESUME]: 'FileText', + [StaticPageType.PROJECTS]: 'Briefcase', + [StaticPageType.PROJECT_DETAIL]: 'FolderOpen' + }; + return icons[pageType] || 'File'; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/constants/terminology.constants.ts b/Ark.Alliance.StartupCms.Ai.Share/constants/terminology.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..a020465e1fa8fdc22c9a6b3e7b4272725db7f4da --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/constants/terminology.constants.ts @@ -0,0 +1,103 @@ +/** + * @fileoverview Terminology Constants + * Centralized terminology for consistent naming across the application. + * + * Responsibilities: + * - Define standard terms for domain concepts + * - Ensure UI consistency + * - Support internationalization preparation + * + * Inputs: None (pure constants) + * Outputs: Constant terminology values + * Side Effects: None + * Invariants: All terms must be consistent with user-facing documentation + * + * @example + * ```typescript + * import { TERMINOLOGY } from '@ark-alliance-startupcms-ia/share'; + * + * const pageTitle = TERMINOLOGY.RESUME_DISPLAY; // "Resume" + * const apiEndpoint = `/api/${TERMINOLOGY.RESUME}`; // "/api/resume" + * ``` + * + * @author Armand Richelet-Kleinberg + */ + +/** + * Standard terminology for portfolio domain concepts + * + * @remarks + * - Use DISPLAY variants for user-facing text + * - Use lowercase variants for routes and identifiers + * - Maintain backwards compatibility via deprecated aliases + */ +export const TERMINOLOGY = { + // Resume (formerly CV) + RESUME: 'resume', + RESUME_DISPLAY: 'Resume', + RESUME_PLURAL: 'resumes', + + // Experience + EXPERIENCE: 'experience', + EXPERIENCE_DISPLAY: 'Experience', + EXPERIENCE_PLURAL: 'experiences', + + // Education + EDUCATION: 'education', + EDUCATION_DISPLAY: 'Education', + EDUCATION_PLURAL: 'education', + + // Skills + SKILL: 'skill', + SKILL_DISPLAY: 'Skill', + SKILL_PLURAL: 'skills', + + // Projects + PROJECT: 'project', + PROJECT_DISPLAY: 'Project', + PROJECT_PLURAL: 'projects', + + // Media + MEDIA: 'media', + MEDIA_DISPLAY: 'Media', + + // Carousel + CAROUSEL: 'carousel', + CAROUSEL_DISPLAY: 'Carousel', + + // Admin + ADMIN: 'admin', + ADMIN_DISPLAY: 'Admin', + + // Languages + LANGUAGE: 'language', + LANGUAGE_DISPLAY: 'Language', + LANGUAGE_PLURAL: 'languages', + + // Hobbies + HOBBY: 'hobby', + HOBBY_DISPLAY: 'Hobby', + HOBBY_PLURAL: 'hobbies', + + // Business Domains + BUSINESS_DOMAIN: 'business-domain', + BUSINESS_DOMAIN_DISPLAY: 'Business Domain', + BUSINESS_DOMAIN_PLURAL: 'business-domains', + + // Deprecated (for backwards compatibility) + /** @deprecated Use RESUME instead */ + CV: 'cv', + /** @deprecated Use RESUME_DISPLAY instead */ + CV_DISPLAY: 'CV', +} as const; + +/** + * Type for terminology keys + */ +export type TerminologyKey = keyof typeof TERMINOLOGY; + +/** + * Type for terminology values + */ +export type TerminologyValue = typeof TERMINOLOGY[TerminologyKey]; + diff --git a/Ark.Alliance.StartupCms.Ai.Share/constants/ui-layout.constants.ts b/Ark.Alliance.StartupCms.Ai.Share/constants/ui-layout.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..63735c5832e8200095194d34f75888c204fa1350 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/constants/ui-layout.constants.ts @@ -0,0 +1,224 @@ +/** + * @fileoverview UI Layout Constants + * Centralized constants for UI components, layouts, and styling. + * Part of the Share layer for cross-project consistency. + * + * Responsibilities: + * - Define UI-related constants (sizing, spacing, breakpoints) + * - Provide layout configuration constants + * - Define component-specific constants + * + * Inputs: None + * Outputs: Type-safe constant objects + * Side Effects: None (pure constants) + * Invariants: All values must be positive numbers for sizing/spacing + * + * @example + * ```typescript + * import { LAYOUT_CONSTANTS, SIDEBAR_CONSTANTS } from '@ark-alliance-startupcms-ia/share'; + * + * const sidebarWidth = SIDEBAR_CONSTANTS.DEFAULT_WIDTH; + * ``` + * + * @author Armand Richelet-Kleinberg + */ + +/** + * Layout Configuration Constants + * Defines standard layout dimensions and breakpoints + */ +export const LAYOUT_CONSTANTS = { + /** + * Responsive breakpoints (in pixels) + * Based on common device widths + */ + BREAKPOINTS: { + /** Mobile devices (0-640px) */ + MOBILE: 640, + /** Tablets (641-768px) */ + TABLET: 768, + /** Desktops (769-1024px) */ + DESKTOP: 1024, + /** Large screens (1025px+) */ + LARGE: 1280, + }, + + /** + * Container max widths + */ + CONTAINER: { + /** Small containers */ + SM: '640px', + /** Medium containers */ + MD: '768px', + /** Large containers */ + LG: '1024px', + /** Extra large containers */ + XL: '1280px', + /** Full width */ + FULL: '100%', + }, + + /** + * Standard spacing values (in rem) + */ + SPACING: { + /** Extra small spacing (0.25rem / 4px) */ + XS: 0.25, + /** Small spacing (0.5rem / 8px) */ + SM: 0.5, + /** Medium spacing (1rem / 16px) */ + MD: 1, + /** Large spacing (1.5rem / 24px) */ + LG: 1.5, + /** Extra large spacing (2rem / 32px) */ + XL: 2, + /** 2XL spacing (3rem / 48px) */ + XXL: 3, + }, + + /** + * Z-index layers + */ + Z_INDEX: { + /** Base layer */ + BASE: 0, + /** Elevated content */ + ELEVATED: 10, + /** Dropdown menus */ + DROPDOWN: 100, + /** Sticky headers */ + STICKY: 500, + /** Fixed sidebars */ + SIDEBAR: 600, + /** Modals */ + MODAL: 1000, + /** Tooltips */ + TOOLTIP: 1100, + /** Toast notifications */ + TOAST: 1200, + }, +} as const; + +/** + * Sidebar Component Constants + * Configuration for retractable sidebar components + */ +export const SIDEBAR_CONSTANTS = { + /** + * Default sidebar width when expanded (in pixels) + */ + DEFAULT_WIDTH: 280, + + /** + * Sidebar width when collapsed (in pixels) + */ + COLLAPSED_WIDTH: 64, + + /** + * Animation duration for expand/collapse (in milliseconds) + */ + ANIMATION_DURATION: 300, + + /** + * Breakpoint below which sidebar becomes overlay (in pixels) + */ + OVERLAY_BREAKPOINT: 768, + + /** + * Sidebar position options + */ + POSITION: { + /** Left-aligned sidebar */ + LEFT: 'left', + /** Right-aligned sidebar */ + RIGHT: 'right', + } as const, +} as const; + +/** + * Header Component Constants + * Configuration for header/navbar components + */ +export const HEADER_CONSTANTS = { + /** + * Default header height (in pixels) + */ + DEFAULT_HEIGHT: 72, + + /** + * Compact header height when scrolled (in pixels) + */ + COMPACT_HEIGHT: 60, + + /** + * Scroll threshold to activate compact mode (in pixels) + */ + SCROLL_THRESHOLD: 50, +} as const; + +/** + * Animation Constants + * Standard animation timings and easing functions + */ +export const ANIMATION_CONSTANTS = { + /** + * Animation durations (in milliseconds) + */ + DURATION: { + /** Fast animations */ + FAST: 150, + /** Normal speed animations */ + NORMAL: 300, + /** Slow animations */ + SLOW: 500, + }, + + /** + * Easing functions + */ + EASING: { + /** Linear easing */ + LINEAR: 'linear', + /** Ease in (slow start) */ + EASE_IN: 'cubic-bezier(0.4, 0, 1, 1)', + /** Ease out (slow end) */ + EASE_OUT: 'cubic-bezier(0, 0, 0.2, 1)', + /** Ease in-out (slow start and end) */ + EASE_IN_OUT: 'cubic-bezier(0.4, 0, 0.2, 1)', + /** Spring effect */ + SPRING: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', + }, +} as const; + +/** + * Scroll Behavior Constants + * Configuration for smooth scrolling + */ +export const SCROLL_CONSTANTS = { + /** + * Scroll behavior type + */ + BEHAVIOR: { + /** Smooth scrolling */ + SMOOTH: 'smooth', + /** Instant scrolling */ + AUTO: 'auto', + } as const, + + /** + * Offset for anchored scrolling (accounts for fixed header) + */ + ANCHOR_OFFSET: 100, +} as const; + +/** + * Type Exports + * Ensure type safety when using constants + */ +export type LayoutConstants = typeof LAYOUT_CONSTANTS; +export type SidebarConstants = typeof SIDEBAR_CONSTANTS; +export type HeaderConstants = typeof HEADER_CONSTANTS; +export type AnimationConstants = typeof ANIMATION_CONSTANTS; +export type ScrollConstants = typeof SCROLL_CONSTANTS; + diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/admin.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/admin.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8d7f450135f2b915b17394a8fc351e8d13b5d6d --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/admin.dto.ts @@ -0,0 +1,16 @@ +/** + * @fileoverview Admin DTOs + * Aggregates all admin-related DTOs. + * + * @author Armand Richelet-Kleinberg + */ + +export * from './common.dto'; +export * from './project.dto'; +export * from './resume.dto'; +export * from './widget.dto'; +export * from './menu.dto'; +export * from './style.dto'; +export * from './media.dto'; +export * from './carousel.dto'; +export * from './auth.dto'; diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/ai-resume.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/ai-resume.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..8dc253edfb86649cbe8a587fbcce1627a0ae1acc --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/ai-resume.dto.ts @@ -0,0 +1,159 @@ +/** + * @fileoverview AI Resume Assistant DTOs + * Data Transfer Objects for AI-powered resume parsing and generation. + * + * @author Armand Richelet-Kleinberg + */ + +/** + * Extracted profile data from resume. + */ +export interface ExtractedProfileDto { + name?: string; + firstName?: string; + lastName?: string; + title?: string; + email?: string; + phone?: string; + location?: string; + linkedin?: string; + github?: string; + website?: string; + summary?: string; +} + +/** + * Extracted experience entry. + */ +export interface ExtractedExperienceDto { + company: string; + position: string; + startDate?: string; + endDate?: string; + description: string; + technologies?: string[]; + achievements?: string[]; +} + +/** + * Extracted education entry. + */ +export interface ExtractedEducationDto { + institution: string; + degree: string; + fieldOfStudy?: string; + startDate?: string; + endDate?: string; + description?: string; + gpa?: string; +} + +/** + * Extracted skill entry. + */ +export interface ExtractedSkillDto { + name: string; + level?: 'Beginner' | 'Intermediate' | 'Advanced' | 'Expert'; + category?: string; + yearsOfExperience?: number; +} + +/** + * Extracted language entry. + */ +export interface ExtractedLanguageDto { + name: string; + proficiency: string; +} + +/** + * Extracted certification entry. + */ +export interface ExtractedCertificationDto { + name: string; + issuer?: string; + date?: string; + expiryDate?: string; + credentialId?: string; +} + +/** + * Complete extracted resume data. + */ +export interface ExtractedResumeDto { + profile: ExtractedProfileDto; + experiences: ExtractedExperienceDto[]; + education: ExtractedEducationDto[]; + skills: ExtractedSkillDto[]; + languages?: ExtractedLanguageDto[]; + certifications?: ExtractedCertificationDto[]; + rawText?: string; +} + +/** + * Request to parse resume from text. + */ +export interface ParseResumeTextRequestDto { + text: string; +} + +/** + * Response from resume parsing. + */ +export interface ParseResumeResponseDto { + success: boolean; + data?: ExtractedResumeDto; + error?: string; + processingTimeMs?: number; +} + +/** + * Request to regenerate a specific resume section. + */ +export interface RegenerateSectionRequestDto { + section: 'profile' | 'experiences' | 'education' | 'skills'; + currentData: unknown; + originalText: string; +} + +/** + * Response from section regeneration. + */ +export interface RegenerateSectionResponseDto { + success: boolean; + data?: unknown; + error?: string; +} + +/** + * Request to improve text description. + */ +export interface ImproveTextRequestDto { + text: string; + context?: string; +} + +/** + * Response from text improvement. + */ +export interface ImproveTextResponseDto { + success: boolean; + originalText: string; + improvedText: string; + error?: string; +} + +/** + * Supported document types for upload. + */ +export type ResumeDocumentType = 'pdf' | 'docx' | 'doc' | 'txt' | 'md'; + +/** + * Document upload metadata. + */ +export interface ResumeDocumentMetadataDto { + filename: string; + mimeType: string; + size: number; + documentType: ResumeDocumentType; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/ai.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/ai.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..c8647bc8b5a5ac5f82789e286aeeb924db09d773 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/ai.dto.ts @@ -0,0 +1,78 @@ +import { AiProviderEnum } from '../enums/ai-provider.enum'; +import { PROVIDER_MODELS } from '../constants/ai.constants'; + +/** + * @fileoverview AI DTOs + * Data Transfer Objects for AI configuration and operations. + */ + +// Re-export enum for backward compatibility if needed, or just use it directly +export type AiProvider = AiProviderEnum; + +/** + * AI Settings configuration DTO. + */ +export interface AiSettingsDto { + id?: number; + provider: AiProviderEnum; + apiUrl?: string; + /** Raw API key (only for updates, never returned) */ + apiKey?: string; + /** Masked API key for display */ + apiKeyMasked?: string; + model: string; + temperature: number; + maxTokens: number; + isActive: boolean; +} + +/** + * AI completion request. + */ +export interface AiCompletionRequestDto { + prompt: string; + context?: string; + maxTokens?: number; +} + +/** + * AI completion response. + */ +export interface AiCompletionResponseDto { + result: string; + usage?: { + promptTokens: number; + completionTokens: number; + totalTokens: number; + }; + error?: string; +} + +/** + * Skill organization request. + */ +export interface SkillOrganizeRequestDto { + skills: Array<{ id: number; name: string; level?: string }>; +} + +/** + * Skill organization response. + */ +export interface SkillOrganizeResponseDto { + categories: Array<{ name: string; skills: number[] }>; + suggestions: string[]; +} + +/** + * Text improvement request. + */ +export interface TextImproveRequestDto { + text: string; + context?: string; +} + +/** + * Provider models mapping. + * @deprecated Use PROVIDER_MODELS from constants/ai.constants + */ +export const AI_PROVIDER_MODELS = PROVIDER_MODELS; diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/audit-log.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/audit-log.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..ebe2aa981aa471f126b913b358edad2591656ab7 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/audit-log.dto.ts @@ -0,0 +1,106 @@ +/** + * @fileoverview Audit Log DTOs + * Data Transfer Objects for audit log viewing and export. + * + * @author Armand Richelet-Kleinberg + */ + +import { AuditAction } from '../enums/audit-action.enum'; + +/** + * Audit log entry for display. + */ +export interface AuditLogDto { + id: string; + userId?: string; + username?: string; + action: AuditAction; + ipAddress?: string; + userAgent?: string; + success: boolean; + details?: string; + createdAt: string; +} + +/** + * Audit log with parsed details. + */ +export interface AuditLogDetailDto extends AuditLogDto { + parsedDetails?: Record; + user?: { + id: string; + username: string; + email?: string; + }; +} + +/** + * Query parameters for audit log search. + */ +export interface AuditLogQueryDto { + userId?: string; + actions?: AuditAction[]; + category?: string; + startDate?: string; + endDate?: string; + success?: boolean; + ipAddress?: string; + search?: string; + page?: number; + limit?: number; + sortOrder?: 'asc' | 'desc'; +} + +/** + * Paginated audit log response. + */ +export interface AuditLogListResponseDto { + logs: AuditLogDto[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + +/** + * Audit log export request. + */ +export interface AuditLogExportRequestDto { + format: 'json' | 'csv'; + startDate?: string; + endDate?: string; + actions?: AuditAction[]; + userId?: string; + maxRecords?: number; +} + +/** + * Audit log statistics for dashboard. + */ +export interface AuditLogStatsDto { + totalEvents: number; + loginSuccessCount: number; + loginFailureCount: number; + passwordChanges: number; + roleChanges: number; + recentFailedLogins: AuditLogDto[]; + eventsByDay: { + date: string; + count: number; + }[]; + topActions: { + action: AuditAction; + count: number; + }[]; +} + +/** + * User login history entry. + */ +export interface LoginHistoryDto { + timestamp: string; + ipAddress?: string; + userAgent?: string; + success: boolean; + failureReason?: string; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/auth.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/auth.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..089c4db6fee8cb3433e5058db6428f2db8041f2a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/auth.dto.ts @@ -0,0 +1,241 @@ +/** + * @fileoverview Auth DTOs + * Data Transfer Objects for Authentication. + */ + +/** + * @swagger + * components: + * schemas: + * LoginRequest: + * type: object + * required: + * - username + * - password + * properties: + * username: + * type: string + * description: User's username + * example: admin + * password: + * type: string + * description: User's password + * example: Admin1234 + */ +export interface LoginRequestDto { + username: string; + password: string; +} + +/** + * @swagger + * components: + * schemas: + * ChangePasswordRequest: + * type: object + * required: + * - oldPassword + * - newPassword + * properties: + * oldPassword: + * type: string + * description: Current password + * newPassword: + * type: string + * description: New password (min 6 chars) + */ +export interface ChangePasswordRequestDto { + oldPassword: string; + newPassword: string; +} + +/** + * @swagger + * components: + * schemas: + * LoginResponse: + * type: object + * properties: + * token: + * type: string + * description: JWT access token + * user: + * type: object + * properties: + * id: + * type: string + * username: + * type: string + * role: + * type: string + */ +export interface LoginResponseDto { + token: string; + user: { + id: string; + username: string; + role: string; + }; +} + +// ============================================ +// Extended Auth DTOs (V2 - Multi-Role Support) +// ============================================ + +import { Role } from '../enums/role.enum'; + +/** + * Token pair response (access + refresh). + */ +export interface TokenPairDto { + accessToken: string; + refreshToken: string; +} + +/** + * @swagger + * components: + * schemas: + * LoginResponseV2: + * type: object + * properties: + * tokens: + * type: object + * properties: + * accessToken: + * type: string + * refreshToken: + * type: string + * user: + * type: object + * properties: + * id: + * type: string + * username: + * type: string + * email: + * type: string + * roles: + * type: array + * items: + * type: string + */ +export interface LoginResponseV2Dto { + tokens: TokenPairDto; + user: { + id: string; + username: string; + email?: string; + roles: Role[]; + collaboratorId?: string; + }; +} + +/** + * @swagger + * components: + * schemas: + * RegisterRequest: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * format: email + * password: + * type: string + * minLength: 8 + * username: + * type: string + * description: Optional, defaults to email prefix + */ +export interface RegisterRequestDto { + email: string; + password: string; + username?: string; +} + +/** + * Registration response. + */ +export interface RegisterResponseDto { + message: string; + userId: string; + /** Only returned in dev mode for testing */ + confirmToken?: string; +} + +/** + * @swagger + * components: + * schemas: + * RefreshTokenRequest: + * type: object + * required: + * - refreshToken + * properties: + * refreshToken: + * type: string + */ +export interface RefreshTokenRequestDto { + refreshToken: string; +} + +/** + * Refresh token response. + */ +export interface RefreshTokenResponseDto { + tokens: TokenPairDto; +} + +/** + * @swagger + * components: + * schemas: + * PasswordResetRequest: + * type: object + * required: + * - email + * properties: + * email: + * type: string + * format: email + */ +export interface PasswordResetRequestDto { + email: string; +} + +/** + * @swagger + * components: + * schemas: + * PasswordResetComplete: + * type: object + * required: + * - token + * - newPassword + * properties: + * token: + * type: string + * newPassword: + * type: string + * minLength: 8 + */ +export interface PasswordResetCompleteDto { + token: string; + newPassword: string; +} + +/** + * Current user response (from /auth/me). + */ +export interface CurrentUserDto { + id: string; + username: string; + email?: string; + roles: Role[]; + collaboratorId?: string; + emailConfirmed: boolean; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/business-domain.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/business-domain.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ac0d72f98218aaddb1e7dd90e9e1eab5c43c4e1 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/business-domain.dto.ts @@ -0,0 +1,73 @@ +/** + * @fileoverview Business Domain DTO + * Data transfer object for business domain knowledge. + * + * @author Armand Richelet-Kleinberg + */ + +import { SkillLevel } from '../enums/skill-level.enum'; +import { + BUSINESS_DOMAIN_PRESETS, + BusinessDomainPreset, + isValidBusinessDomain, + BUSINESS_DOMAIN_LEVELS +} from '../constants/resume.constants'; + +/** + * Represents business domain expertise. + * + * @remarks + * Domain must be selected from BUSINESS_DOMAIN_PRESETS. + * Level uses SkillLevel enum for consistency with skills. + */ +export interface BusinessDomainDto { + /** Unique identifier */ + id?: number; + + /** Domain name - must be from preset list */ + domain: string; + + /** Proficiency level (uses SkillLevel enum) */ + level?: SkillLevel; + + /** Description of experience in this domain */ + description?: string; + + /** Years of experience in this domain */ + yearsOfExperience?: number; + + /** Lucide icon name for visual representation */ + icon?: string; + + /** Display order for sorting */ + displayOrder?: number; +} + +/** + * Validate a complete BusinessDomainDto. + * @param dto - DTO to validate + * @returns Object with isValid flag and error messages + */ +export function validateBusinessDomainDto(dto: BusinessDomainDto): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!dto.domain || dto.domain.trim() === '') { + errors.push('Domain is required'); + } else if (!isValidBusinessDomain(dto.domain)) { + errors.push(`"${dto.domain}" is not a valid business domain. Please select from the preset list.`); + } + + if (dto.yearsOfExperience !== undefined && dto.yearsOfExperience < 0) { + errors.push('Years of experience cannot be negative'); + } + + return { isValid: errors.length === 0, errors }; +} + +// Re-export constants for convenience +export { + BUSINESS_DOMAIN_PRESETS, + isValidBusinessDomain, + BUSINESS_DOMAIN_LEVELS +}; +export type { BusinessDomainPreset }; diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/carousel.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/carousel.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..d935a68170327b4067368b477a0b1b7329caeafb --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/carousel.dto.ts @@ -0,0 +1,111 @@ +/** + * @fileoverview Carousel DTOs + * Data Transfer Objects for Carousel management. + */ + +/** + * @swagger + * components: + * schemas: + * AdminCarouselItem: + * type: object + * properties: + * id: + * type: integer + * title: + * type: string + * subtitle: + * type: string + * description: + * type: string + * imageUrl: + * type: string + * linkUrl: + * type: string + * linkText: + * type: string + * order: + * type: integer + * isActive: + * type: boolean + * projectId: + * type: string + * ReorderCarousel: + * type: object + * properties: + * itemIds: + * type: array + * items: + * type: integer + */ +export interface AdminCarouselItemDto { + id?: number; + title: string; + subtitle?: string; + description?: string; + imageUrl: string; + linkUrl?: string; + linkText?: string; + order: number; + isActive: boolean; + projectId?: string; +} + +/** + * Reorder carousel items request. + */ +export interface ReorderCarouselDto { + itemIds: number[]; +} + +// ============================================ +// Public Carousel DTOs (for Homepage) +// ============================================ + +/** + * Public carousel slide DTO for homepage display. + * Contains only fields needed for public presentation. + * + * @example + * ```typescript + * const slide: CarouselSlideDto = { + * id: 1, + * title: 'Welcome', + * subtitle: 'Portfolio CMS', + * description: 'Explore my work', + * imageUrl: '/assets/hero.png', + * ctaLabel: 'Learn More', + * ctaLink: '/projects' + * }; + * ``` + */ +export interface CarouselSlideDto { + /** Unique identifier */ + id: number | string; + /** Main title displayed on the slide */ + title: string; + /** Subtitle/tagline (optional) */ + subtitle?: string; + /** Longer description text (optional) */ + description?: string; + /** URL to the background/hero image */ + imageUrl?: string; + /** Call-to-action button label */ + ctaLabel?: string; + /** Call-to-action button link */ + ctaLink?: string; +} + +/** + * Default carousel slides for fallback when API fails. + */ +export const DEFAULT_CAROUSEL_SLIDES: CarouselSlideDto[] = [ + { + id: 'default', + title: 'Welcome to My Portfolio', + subtitle: 'Full-Stack Developer', + description: 'Explore my projects, experience, and technical expertise in building modern web applications.', + ctaLabel: 'View Portfolio', + ctaLink: '/projects', + } +]; diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/collaborator.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/collaborator.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..6df3210e5d7355202899cb7ec774baa3507dfbd4 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/collaborator.dto.ts @@ -0,0 +1,126 @@ +/** + * @fileoverview Collaborator DTOs + * Data Transfer Objects for team member/collaborator management. + * Collaborators are organization team members with optional auth accounts. + * + * @author Armand Richelet-Kleinberg + */ + +/** + * Collaborator data for public display. + */ +export interface CollaboratorDto { + id: string; + firstName: string; + lastName: string; + fullName: string; + email?: string; + position: string; + department?: string; + bio?: string; + avatarUrl?: string; + linkedinUrl?: string; + githubUrl?: string; + twitterUrl?: string; + hireDate?: string; + isActive: boolean; + organizationId: string; + reportsToId?: string; + userId?: string; + createdAt: string; + updatedAt: string; +} + +/** + * Collaborator summary for listings and org chart. + */ +export interface CollaboratorSummaryDto { + id: string; + firstName: string; + lastName: string; + fullName: string; + position: string; + department?: string; + avatarUrl?: string; + reportsToId?: string; + isActive: boolean; +} + +/** + * Node in organization chart tree. + */ +export interface OrgChartNodeDto { + id: string; + name: string; + position: string; + department?: string; + avatarUrl?: string; + email?: string; + children: OrgChartNodeDto[]; +} + +/** + * Complete org chart response. + */ +export interface OrgChartDto { + organizationId: string; + organizationName: string; + rootNodes: OrgChartNodeDto[]; + totalCount: number; +} + +/** + * Request to create a new collaborator. + */ +export interface CreateCollaboratorDto { + firstName: string; + lastName: string; + email?: string; + position: string; + department?: string; + bio?: string; + avatarUrl?: string; + linkedinUrl?: string; + githubUrl?: string; + twitterUrl?: string; + hireDate?: string; + reportsToId?: string; + organizationId: string; +} + +/** + * Request to update a collaborator. + */ +export interface UpdateCollaboratorDto extends Partial> { + isActive?: boolean; +} + +/** + * Request to change collaborator's manager. + */ +export interface UpdateHierarchyDto { + collaboratorId: string; + newReportsToId: string | null; +} + +/** + * Collaborator with direct reports for management views. + */ +export interface CollaboratorWithReportsDto extends CollaboratorDto { + reportsTo?: CollaboratorSummaryDto; + directReports: CollaboratorSummaryDto[]; + directReportsCount: number; +} + +/** + * Collaborator linked with user auth info. + */ +export interface CollaboratorWithUserDto extends CollaboratorDto { + user?: { + id: string; + username: string; + email: string; + roles: string[]; + lastLogin?: string; + }; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/common.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/common.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..125b455294f57596c5d8c55eb8fa713abf298ce0 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/common.dto.ts @@ -0,0 +1,75 @@ +/** + * @fileoverview Common DTOs + * Shared Data Transfer Objects for generic responses. + */ + +/** + * @swagger + * components: + * schemas: + * CrudResponse: + * type: object + * properties: + * success: + * type: boolean + * description: Operation success status + * message: + * type: string + * description: Response message + * data: + * type: object + * description: Response payload + * errors: + * type: array + * items: + * type: string + * description: List of error messages + * timestamp: + * type: string + * format: date-time + * description: Response timestamp + */ +export interface CrudResponseDto { + success: boolean; + message: string; + data?: T; + errors?: string[]; + timestamp: string; +} + +/** + * @swagger + * components: + * schemas: + * PaginatedResponse: + * type: object + * properties: + * items: + * type: array + * items: + * type: object + * description: List of items + * total: + * type: integer + * description: Total number of items + * page: + * type: integer + * description: Current page number + * pageSize: + * type: integer + * description: Items per page + * hasNext: + * type: boolean + * description: Has next page + * hasPrevious: + * type: boolean + * description: Has previous page + */ +export interface PaginatedResponseDto { + items: T[]; + total: number; + page: number; + pageSize: number; + hasNext: boolean; + hasPrevious: boolean; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/dashboard.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/dashboard.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea482742fc293a1a25d0ce098c67549451781dce --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/dashboard.dto.ts @@ -0,0 +1,38 @@ +/** + * Data Transfer Object for dashboard statistics. + * Provides aggregate metrics for the portfolio overview. + */ +export interface DashboardStatsDto { + /** Total number of projects in portfolio */ + totalProjects: number; + /** Total number of skills listed */ + totalSkills: number; + /** Total years of professional experience */ + totalExperienceYears: number; + /** Number of currently active projects */ + activeProjects: number; +} + +/** + * Data Transfer Object for activity graph data. + * Provides time-series data for visualizing portfolio activity. + */ +export interface ActivityGraphDto { + /** X-axis labels (typically months or weeks) */ + labels: string[]; + /** Commit counts corresponding to each label */ + commits: number[]; + /** Complexity scores corresponding to each label */ + complexity: number[]; +} + +/** + * Data Transfer Object for complete dashboard data. + * Combines statistics and activity graph for home page display. + */ +export interface DashboardDataDto { + /** Aggregate statistics */ + stats: DashboardStatsDto; + /** Activity graph time-series data */ + activity: ActivityGraphDto; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/hobby.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/hobby.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..94594513cb8fa651f66ae83bf4761fd88c1b8dc4 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/hobby.dto.ts @@ -0,0 +1,52 @@ +/** + * @fileoverview Hobby DTO + * Data transfer object for hobbies and interests. + * + * @author Armand Richelet-Kleinberg + */ + +import { HOBBY_ICON_SUGGESTIONS } from '../constants/resume.constants'; + +/** + * Represents a hobby or personal interest. + */ +export interface HobbyDto { + /** Unique identifier */ + id?: number; + + /** Hobby name */ + name: string; + + /** Description of the hobby */ + description?: string; + + /** Lucide icon name for visual representation */ + icon?: string; + + /** Display order for sorting */ + displayOrder?: number; +} + +/** + * Validate a complete HobbyDto. + * @param dto - DTO to validate + * @returns Object with isValid flag and error messages + */ +export function validateHobbyDto(dto: HobbyDto): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!dto.name || dto.name.trim() === '') { + errors.push('Hobby name is required'); + } else if (dto.name.length > 100) { + errors.push('Hobby name must be 100 characters or less'); + } + + if (dto.description && dto.description.length > 500) { + errors.push('Description must be 500 characters or less'); + } + + return { isValid: errors.length === 0, errors }; +} + +// Re-export icon suggestions for convenience +export { HOBBY_ICON_SUGGESTIONS }; diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/language.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/language.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..459d3318bae8708fb2d636b9044b0a2136cbabc1 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/language.dto.ts @@ -0,0 +1,90 @@ +/** + * @fileoverview Language DTO + * Data transfer object for language proficiency. + * + * @author Armand Richelet-Kleinberg + */ + +import { + MIN_PROFICIENCY_LEVEL, + MAX_PROFICIENCY_LEVEL, + PROFICIENCY_LEVEL_LABELS, + getProficiencyLabelByValue +} from '../constants/resume.constants'; + +/** + * Represents language proficiency with three dimensions. + * + * @remarks + * Each dimension (speaking, writing, presenting) uses a 1-5 scale: + * - 1 = Basic + * - 2 = Elementary + * - 3 = Intermediate + * - 4 = Advanced + * - 5 = Native/Fluent + */ +export interface LanguageDto { + /** Unique identifier */ + id?: number; + + /** Language name (e.g., "English", "French") */ + language: string; + + /** Speaking proficiency (1-5 scale) */ + speaking: number; + + /** Writing proficiency (1-5 scale) */ + writing: number; + + /** Presenting proficiency (1-5 scale) */ + presenting: number; + + /** Display order for sorting */ + displayOrder?: number; +} + +/** + * Validate a language proficiency value. + * @param value - Value to validate + * @returns True if value is between 1-5 + */ +export function isValidLanguageProficiency(value: number): boolean { + return Number.isInteger(value) && + value >= MIN_PROFICIENCY_LEVEL && + value <= MAX_PROFICIENCY_LEVEL; +} + +/** + * Validate a complete LanguageDto. + * @param dto - DTO to validate + * @returns Object with isValid flag and error messages + */ +export function validateLanguageDto(dto: LanguageDto): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!dto.language || dto.language.trim() === '') { + errors.push('Language name is required'); + } + + if (!isValidLanguageProficiency(dto.speaking)) { + errors.push(`Speaking must be between ${MIN_PROFICIENCY_LEVEL} and ${MAX_PROFICIENCY_LEVEL}`); + } + + if (!isValidLanguageProficiency(dto.writing)) { + errors.push(`Writing must be between ${MIN_PROFICIENCY_LEVEL} and ${MAX_PROFICIENCY_LEVEL}`); + } + + if (!isValidLanguageProficiency(dto.presenting)) { + errors.push(`Presenting must be between ${MIN_PROFICIENCY_LEVEL} and ${MAX_PROFICIENCY_LEVEL}`); + } + + return { isValid: errors.length === 0, errors }; +} + +// Re-export constants for convenience +export { + MIN_PROFICIENCY_LEVEL, + MAX_PROFICIENCY_LEVEL, + PROFICIENCY_LEVEL_LABELS, + getProficiencyLabelByValue +}; diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/media.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/media.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed3a296131b9fb76535b5ccb5162fac6072d410f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/media.dto.ts @@ -0,0 +1,265 @@ +/** + * @fileoverview Media DTOs + * Data Transfer Objects for Media management. + * + * @author Armand Richelet-Kleinberg + */ + +import { MediaTypeEnum, MediaSourceEnum } from '../enums/admin.enums'; + +/** + * Admin media item DTO. + * @remarks Used for displaying and managing media in the admin interface. + */ +export interface AdminMediaDto { + /** Unique identifier (UUID) */ + id?: string; + /** Display name */ + name: string; + /** Unique key for programmatic reference */ + key?: string; + /** Media type classification */ + type: MediaTypeEnum; + /** Source type (URL, upload, S3) */ + source?: MediaSourceEnum; + /** Full URL to the media file */ + url?: string; + /** Original filename */ + originalFileName?: string; + /** File path on server */ + filePath?: string; + /** Alt text for accessibility */ + altText?: string; + /** Caption or description */ + description?: string; + /** Tags for categorization */ + tags?: string[]; + /** Additional metadata (JSON) */ + metadata?: Record; + /** Image/video width in pixels */ + width?: number; + /** Image/video height in pixels */ + height?: number; + /** File size in bytes */ + fileSize?: number; + /** MIME type */ + mimeType?: string; + /** Whether publicly accessible */ + isPublic?: boolean; + /** Creation timestamp */ + createdAt?: string; + /** Last update timestamp */ + updatedAt?: string; +} + +/** + * Media upload request DTO. + * @remarks Used for file uploads from the frontend. + */ +export interface UploadMediaDto { + /** Display name for the media */ + name: string; + /** Unique key for reference */ + key?: string; + /** Media type (auto-detected from file if not provided) */ + type?: MediaTypeEnum; + /** Alt text for accessibility */ + altText?: string; + /** Description */ + description?: string; + /** Tags for categorization */ + tags?: string[]; + /** Additional metadata */ + metadata?: Record; + /** Whether publicly accessible */ + isPublic?: boolean; +} + +/** + * Create media from URL request DTO. + */ +export interface CreateMediaFromUrlDto { + /** External URL */ + url: string; + /** Display name */ + name: string; + /** Unique key for reference */ + key?: string; + /** Media type */ + type: MediaTypeEnum; + /** Alt text */ + altText?: string; + /** Description */ + description?: string; + /** Tags */ + tags?: string[]; + /** Metadata */ + metadata?: Record; +} + +/** + * Media search parameters DTO. + */ +export interface MediaSearchDto { + /** Filter by type */ + type?: MediaTypeEnum; + /** Search in name, description, key */ + search?: string; + /** Filter by tags */ + tags?: string[]; + /** Page number (1-indexed) */ + page?: number; + /** Items per page */ + pageSize?: number; +} + +/** + * Media list response DTO. + */ +export interface MediaListResponseDto { + /** Media items */ + items: AdminMediaDto[]; + /** Total count */ + total: number; + /** Current page */ + page: number; + /** Page size */ + pageSize: number; +} + +/** + * Media binary data exchange DTO. + * @remarks Used for transferring media files as byte arrays in API contracts. + * Supports base64 encoding for JSON transport or raw bytes for multipart/form-data. + */ +export interface MediaBinaryDataDto { + /** Media record ID */ + id: string; + /** Display name */ + name: string; + /** MIME type of the binary data */ + mimeType: string; + /** File size in bytes */ + fileSize: number; + /** Base64-encoded binary data (for JSON transport) */ + base64Data?: string; + /** Original filename */ + originalFileName?: string; +} + +/** + * Media upload with binary data DTO. + * @remarks Used for uploading files with embedded binary data. + */ +export interface MediaUploadBinaryDto extends UploadMediaDto { + /** Base64-encoded file content */ + base64Content: string; + /** MIME type of the file */ + mimeType: string; + /** Original filename */ + originalFileName: string; + /** File size in bytes */ + fileSize: number; +} + +/** + * Media seed data DTO. + * @remarks Used for database initialization with default media assets. + * Maps directly to Media entity fields for seeding. + */ +export interface MediaSeedDto { + /** Display name */ + name: string; + /** Unique key for programmatic reference (kebab-case) */ + key: string; + /** Relative URL path to the asset file */ + url: string; + /** Media type classification */ + type: MediaTypeEnum; + /** MIME type */ + mimeType: string; + /** Original filename */ + originalFileName: string; + /** File size in bytes */ + fileSize: number; + /** Alt text for accessibility */ + altText?: string; + /** Description for AI context and display */ + description?: string; + /** Tags for categorization and filtering */ + tags?: string[]; + /** Whether publicly accessible */ + isPublic?: boolean; +} + +/** + * Default media seed data for database initialization. + * @remarks These entries correspond to files in the backend /Assets folder. + */ +export const DEFAULT_MEDIA_SEED: MediaSeedDto[] = [ + { + name: 'Ark.Portfolio Hero', + key: 'project-ark-portfolio-hero', + url: '/Assets/Projects/Ark.Portfolio/portfolio-hero.png', + type: MediaTypeEnum.IMAGE, + mimeType: 'image/png', + originalFileName: 'portfolio-hero.png', + fileSize: 605232, + altText: 'Ark.Portfolio project hero image', + description: 'AI-Powered Portfolio CMS hero image for carousel and project display', + tags: ['project', 'hero', 'carousel'], + isPublic: true + }, + { + name: 'Ark.Alliance React Component Hero', + key: 'project-ark-react-component-hero', + url: '/Assets/Projects/Ark.Alliance.React.Component/components-hero.png', + type: MediaTypeEnum.IMAGE, + mimeType: 'image/png', + originalFileName: 'components-hero.png', + fileSize: 605096, + altText: 'Ark.Alliance React Component Library hero image', + description: 'Enterprise React component library hero image for carousel and project display', + tags: ['project', 'hero', 'carousel'], + isPublic: true + }, + { + name: 'Ark.Alliance Trading Bot Screenshot', + key: 'project-trading-bot-6', + url: '/Assets/Projects/Ark.Alliance.Trading.Bot/Bot6.PNG', + type: MediaTypeEnum.IMAGE, + mimeType: 'image/png', + originalFileName: 'Bot6.PNG', + fileSize: 505004, + altText: 'Ark.Alliance Trading Bot analytics screenshot', + description: 'Trading Bot analytics and monitoring interface - used for carousel', + tags: ['project', 'trading', 'carousel', 'screenshot'], + isPublic: true + }, + { + name: 'Ark.Alliance Trading Providers Hero', + key: 'project-trading-providers-hero', + url: '/Assets/Projects/Ark.Alliance.Trading.Providers.Lib/trading-hero.png', + type: MediaTypeEnum.IMAGE, + mimeType: 'image/png', + originalFileName: 'trading-hero.png', + fileSize: 695430, + altText: 'Ark.Alliance Trading Providers Library hero image', + description: 'Multi-exchange trading SDK hero image for carousel and project display', + tags: ['project', 'hero', 'carousel'], + isPublic: true + }, + { + name: 'Profile Avatar', + key: 'profile-avatar', + url: '/Assets/Site/Icon.png', + type: MediaTypeEnum.IMAGE, + mimeType: 'image/png', + originalFileName: 'Icon.png', + fileSize: 101421, + altText: 'Armand Richelet-Kleinberg profile avatar', + description: 'Default profile avatar image', + tags: ['profile', 'avatar'], + isPublic: true + } +]; diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/menu.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/menu.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..1c58f17a43f0743cd1c2adcab86e2053cb68f696 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/menu.dto.ts @@ -0,0 +1,63 @@ +/** + * @fileoverview Menu DTOs + * Data Transfer Objects for Menu management. + */ + +import { MenuPositionEnum } from '../enums/admin.enums'; + +/** + * @swagger + * components: + * schemas: + * AdminMenuItem: + * type: object + * properties: + * id: + * type: integer + * label: + * type: string + * icon: + * type: string + * route: + * type: string + * position: + * type: string + * enum: [TOP, SIDE, FOOTER] + * order: + * type: integer + * isVisible: + * type: boolean + * parentId: + * type: integer + * openInNewTab: + * type: boolean + * ReorderMenuItems: + * type: object + * properties: + * position: + * type: string + * enum: [TOP, SIDE, FOOTER] + * itemIds: + * type: array + * items: + * type: integer + */ +export interface AdminMenuItemDto { + id?: number; + label: string; + icon?: string; + route: string; + position: MenuPositionEnum; + order: number; + isVisible: boolean; + parentId?: number | null; + openInNewTab?: boolean; +} + +/** + * Reorder menu items request. + */ +export interface ReorderMenuItemsDto { + position: MenuPositionEnum; + itemIds: number[]; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/organization.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/organization.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..a68700ff73e784fd08534e1ab0594f220a61f2c5 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/organization.dto.ts @@ -0,0 +1,112 @@ +/** + * @fileoverview Organization DTOs + * Data Transfer Objects for organization management. + * + * @author Armand Richelet-Kleinberg + */ + +/** + * GPS coordinates for organization location. + */ +export interface GpsCoordinatesDto { + latitude: number; + longitude: number; +} + +/** + * Social media links for organization. + */ +export interface OrganizationSocialLinksDto { + website?: string; + linkedIn?: string; + twitter?: string; + github?: string; + youtube?: string; + instagram?: string; +} + +/** + * Organization data for public display. + */ +export interface OrganizationDto { + id: string; + name: string; + legalName?: string; + tagline?: string; + mission?: string; + vision?: string; + description?: string; + logoUrl?: string; + iconUrl?: string; + address?: string; + city?: string; + country?: string; + postalCode?: string; + timezone?: string; + gps?: GpsCoordinatesDto; + socialLinks?: OrganizationSocialLinksDto; + foundedYear?: number; + employeeCount?: number; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + +/** + * Organization summary for listings. + */ +export interface OrganizationSummaryDto { + id: string; + name: string; + tagline?: string; + logoUrl?: string; + employeeCount?: number; +} + +/** + * Request to create a new organization. + */ +export interface CreateOrganizationDto { + name: string; + legalName?: string; + tagline?: string; + mission?: string; + vision?: string; + description?: string; + address?: string; + city?: string; + country?: string; + postalCode?: string; + timezone?: string; + gps?: GpsCoordinatesDto; + socialLinks?: OrganizationSocialLinksDto; + foundedYear?: number; +} + +/** + * Request to update an organization. + * Supports both nested structures (gps, socialLinks) AND flat fields for convenience. + */ +export interface UpdateOrganizationDto extends Partial { + logoUrl?: string; + iconUrl?: string; + employeeCount?: number; + isActive?: boolean; + + // Flat convenience fields for API compatibility + website?: string; + linkedinUrl?: string; + linkedIn?: string; + githubUrl?: string; + github?: string; + latitude?: number; + longitude?: number; +} + +/** + * Organization with team count for admin views. + */ +export interface OrganizationWithStatsDto extends OrganizationDto { + collaboratorCount: number; + activeCollaboratorCount: number; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/profile.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/profile.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..56f8d51c0c0df414fe40c26da038b17f75ceb4ca --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/profile.dto.ts @@ -0,0 +1,35 @@ +/** + * Data Transfer Object for profile information. + * Represents the portfolio owner's basic profile and contact details. + * + * @example + * ```typescript + * const profile: ProfileDto = { + * firstName: 'Armand', + * lastName: 'Richelet-Kleinberg', + * title: 'Senior Software Engineer', + * overview: 'Full-stack developer with 15+ years experience', + * email: 'contact@example.com' + * }; + * ``` + */ +export interface ProfileDto { + /** First name of the profile owner */ + firstName: string; + /** Last name of the profile owner */ + lastName: string; + /** Professional title (e.g., 'Senior Software Engineer') */ + title: string; + /** Brief professional overview/bio */ + overview: string; + /** Short bio description */ + bio?: string; + /** Contact email address */ + email: string; + /** LinkedIn profile URL */ + linkedinUrl?: string; + /** GitHub profile URL */ + githubUrl?: string; + /** Profile avatar/photo URL */ + avatarUrl?: string; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/project.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/project.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..166bc3613da7204a2e4e24ffc79791aa5d62b17c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/project.dto.ts @@ -0,0 +1,154 @@ +/** + * @fileoverview Project DTOs + * Data Transfer Objects for Project management. + */ + +import { ProjectStatus } from '../enums/project-status.enum'; + +/** + * Technology entry for projects. + */ +export interface AdminTechnologyDto { + name: string; + category?: string; +} + +/** + * @swagger + * components: + * schemas: + * AdminTechnology: + * type: object + * properties: + * name: + * type: string + * category: + * type: string + * AdminProject: + * type: object + * properties: + * id: + * type: string + * format: uuid + * title: + * type: string + * description: + * type: string + * status: + * type: string + * enum: [PLANNED, IN_PROGRESS, COMPLETED, ON_HOLD, ARCHIVED] + * technologies: + * type: array + * items: + * $ref: '#/components/schemas/AdminTechnology' + * isFeatured: + * type: boolean + * imageUrl: + * type: string + * mermaidDiagram: + * type: string + * repositoryUrl: + * type: string + * packageUrl: + * type: string + * description: NPM/NuGet package URL + * liveUrl: + * type: string + * startDate: + * type: string + * format: date + * endDate: + * type: string + * format: date + */ +export interface AdminProjectDto { + id?: string; + title: string; + description: string; + status: ProjectStatus; + technologies: AdminTechnologyDto[]; + isFeatured: boolean; + imageUrl?: string; + mermaidDiagram?: string; + repositoryUrl?: string; + packageUrl?: string; + liveUrl?: string; + startDate?: string; + endDate?: string; +} + +/** + * @swagger + * components: + * schemas: + * UpdateMermaid: + * type: object + * required: + * - projectId + * - mermaidCode + * properties: + * projectId: + * type: string + * format: uuid + * mermaidCode: + * type: string + */ +export interface UpdateMermaidDto { + projectId: string; + mermaidCode: string; +} + +/** + * Page within a project. + */ +export interface ProjectPageDto { + id: string; + type?: string; + title: string; + content: string; + order?: number; +} + +/** + * Feature highlight for a project. + */ +export interface ProjectFeatureDto { + id?: string; + title: string; + description: string; + icon?: string; + imageUrl?: string; +} + +// ============================================ +// Public DTOs (for frontend consumption) +// ============================================ + +/** + * Full project DTO for public/display use. + * Includes features, pages, and alternate property names for compatibility. + */ +export interface ProjectDto { + id?: string; + title: string; + description: string; + status: ProjectStatus; + technologies: (AdminTechnologyDto | string)[]; + isFeatured?: boolean; + imageUrl?: string; + mermaidDiagram?: string; + /** Repository URL (alias for repositoryUrl) */ + repoUrl?: string; + repositoryUrl?: string; + /** Package URL for libraries (npm, nuget, etc.) */ + packageUrl?: string; + /** Demo/live URL (alias for liveUrl) */ + demoUrl?: string; + liveUrl?: string; + startDate?: Date | string; + endDate?: Date | string; + /** Project features/highlights */ + features?: ProjectFeatureDto[]; + /** Project pages/sections */ + pages?: ProjectPageDto[]; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/resume.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/resume.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb87e1fb26b4dad514f934fb07110332c6259af3 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/resume.dto.ts @@ -0,0 +1,193 @@ +/** + * @fileoverview Resume DTOs + * Data Transfer Objects for Resume/CV management. + */ + +import { SkillLevel } from '../enums/skill-level.enum'; +import { LanguageDto, PROFICIENCY_LEVEL_LABELS } from './language.dto'; +import { HobbyDto } from './hobby.dto'; +import { BusinessDomainDto, BUSINESS_DOMAIN_PRESETS, BUSINESS_DOMAIN_LEVELS } from './business-domain.dto'; + +/** + * @swagger + * components: + * schemas: + * AdminProfile: + * type: object + * properties: + * firstName: + * type: string + * lastName: + * type: string + * title: + * type: string + * overview: + * type: string + * email: + * type: string + * linkedinUrl: + * type: string + * githubUrl: + * type: string + * avatarUrl: + * type: string + * phoneNumber: + * type: string + * location: + * type: string + * AdminExperience: + * type: object + * properties: + * id: + * type: integer + * company: + * type: string + * position: + * type: string + * description: + * type: string + * startDate: + * type: string + * format: date + * endDate: + * type: string + * format: date + * technologies: + * type: array + * items: + * type: string + * isHighlighted: + * type: boolean + * AdminSkill: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * level: + * type: string + * enum: [Beginner, Intermediate, Advanced, Expert, Master] + * category: + * type: string + * yearsOfExperience: + * type: number + * AdminEducation: + * type: object + * properties: + * id: + * type: integer + * institution: + * type: string + * degree: + * type: string + * fieldOfStudy: + * type: string + * startDate: + * type: string + * format: date + * endDate: + * type: string + * format: date + * description: + * type: string + * AdminResume: + * type: object + * properties: + * profile: + * $ref: '#/components/schemas/AdminProfile' + * experiences: + * type: array + * items: + * $ref: '#/components/schemas/AdminExperience' + * skills: + * type: array + * items: + * $ref: '#/components/schemas/AdminSkill' + * education: + * type: array + * items: + * $ref: '#/components/schemas/AdminEducation' + */ +export interface AdminProfileDto { + firstName: string; + lastName: string; + title: string; + overview: string; + email: string; + linkedinUrl?: string; + githubUrl?: string; + avatarUrl?: string; + phoneNumber?: string; + location?: string; +} + +/** + * Experience entry for CV. + */ +export interface AdminExperienceDto { + id?: number; + company: string; + position: string; + description: string; + startDate: string; + endDate?: string | null; + technologies: string[]; + isHighlighted?: boolean; +} + +/** + * Skill entry for CV. + */ +export interface AdminSkillDto { + id?: number; + name: string; + level: SkillLevel; + category: string; + yearsOfExperience?: number; +} + +/** + * Education entry for CV. + */ +export interface AdminEducationDto { + id?: number; + institution: string; + degree: string; + fieldOfStudy: string; + startDate: string; + endDate?: string | null; + description?: string; +} + +/** + * Full Resume management DTO. + */ +export interface AdminResumeDto { + profile: AdminProfileDto; + experiences: AdminExperienceDto[]; + skills: AdminSkillDto[]; + education: AdminEducationDto[]; + languages?: LanguageDto[]; + hobbies?: HobbyDto[]; + businessDomains?: BusinessDomainDto[]; +} + +// Re-export new DTOs for convenience +export type { LanguageDto } from './language.dto'; +export { PROFICIENCY_LEVEL_LABELS } from './language.dto'; +export type { HobbyDto } from './hobby.dto'; +export type { BusinessDomainDto } from './business-domain.dto'; +export { BUSINESS_DOMAIN_PRESETS, BUSINESS_DOMAIN_LEVELS } from './business-domain.dto'; + +// ============================================ +// Public Type Aliases +// ============================================ +/** @deprecated Use AdminResumeDto for admin operations */ +export type ResumeDto = AdminResumeDto; +/** @deprecated Use AdminExperienceDto for admin operations */ +export type ExperienceDto = AdminExperienceDto; +/** @deprecated Use AdminEducationDto for admin operations */ +export type EducationDto = AdminEducationDto; +/** @deprecated Use AdminSkillDto for admin operations */ +export type SkillDto = AdminSkillDto; diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/skill-category.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/skill-category.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d6c122fa6828dad88e20e4b77d01513293e4b8b --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/skill-category.dto.ts @@ -0,0 +1,39 @@ +/** + * @fileoverview Skill Category DTOs + * Data Transfer Objects for skill category management. + */ + +/** + * Skill category DTO for CRUD operations. + */ +export interface SkillCategoryDto { + id?: number; + name: string; + description?: string; + icon?: string; + color?: string; + displayOrder?: number; +} + +/** + * Reorder skill categories request. + */ +export interface ReorderSkillCategoriesDto { + categoryIds: number[]; +} + +/** + * Reorder skills within category request. + */ +export interface ReorderSkillsDto { + categoryId: number; + skillIds: number[]; +} + +/** + * Move skill to category request. + */ +export interface MoveSkillToCategoryDto { + skillId: number; + targetCategoryId: number; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/static-generation.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/static-generation.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..220f287fec778c90540ba3483ebef9ecd62ae7de --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/static-generation.dto.ts @@ -0,0 +1,140 @@ +/** + * @fileoverview Static Generation DTOs + * Data Transfer Objects for static site generation. + * + * @author Armand Richelet-Kleinberg + */ + +import { StaticPageType, StaticExportStatus, StaticThemePreset, StaticResumeSectionType } from '../enums/static-generation.enum'; + +/** + * Configuration for a page in the static export. + */ +export interface PageDefinitionDto { + /** Unique identifier */ + id?: number; + /** Page type from enum */ + pageType: StaticPageType; + /** Page title for navigation */ + title: string; + /** URL route path */ + route: string; + /** Sections to include (for resume page) */ + sections?: StaticResumeSectionType[]; + /** Whether page is enabled for export */ + isEnabled: boolean; + /** Display order in navigation */ + displayOrder: number; + /** Additional metadata */ + metadata?: Record; +} + +/** + * Configuration for static site export. + */ +export interface StaticExportConfigDto { + /** Pages to include in export */ + pages: PageDefinitionDto[]; + /** Theme preset or custom */ + theme: StaticThemePreset; + /** Custom theme overrides (if theme is CUSTOM) */ + customTheme?: { + primaryColor?: string; + secondaryColor?: string; + accentColor?: string; + backgroundColor?: string; + textColor?: string; + fontFamily?: string; + }; + /** Include project subfolders */ + includeProjectSubfolders: boolean; + /** Include all media assets */ + includeAssets: boolean; + /** Site metadata */ + siteMetadata: { + title?: string; + description?: string; + author?: string; + keywords?: string[]; + }; +} + +/** + * Result of static site generation. + */ +export interface StaticExportResultDto { + /** Export status */ + status: StaticExportStatus; + /** Generated filename */ + filename?: string; + /** File size in bytes */ + sizeBytes?: number; + /** Number of pages generated */ + pageCount?: number; + /** Number of assets included */ + assetCount?: number; + /** Generation timestamp */ + generatedAt?: string; + /** Error message if failed */ + error?: string; +} + +/** + * Preview data before export. + */ +export interface StaticExportPreviewDto { + /** Profile name for export */ + profileName: string; + /** Number of projects */ + projectCount: number; + /** Number of experience entries */ + experienceCount: number; + /** Number of skills */ + skillCount: number; + /** Number of languages */ + languageCount: number; + /** Number of hobbies */ + hobbyCount: number; + /** Number of business domains */ + domainCount: number; + /** Current theme */ + theme: string; + /** Estimated ZIP size */ + estimatedSize: string; + /** Pages to be exported */ + pages: PageDefinitionDto[]; +} + +/** + * Request to generate static site. + */ +export interface GenerateStaticSiteRequestDto { + /** Export configuration */ + config?: Partial; + /** Force regeneration even if cached */ + forceRegenerate?: boolean; +} + +/** + * Validation for StaticExportConfigDto. + */ +export function validateStaticExportConfig(config: StaticExportConfigDto): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!config.pages || config.pages.length === 0) { + errors.push('At least one page must be configured for export.'); + } + + if (!config.theme) { + errors.push('Theme must be specified.'); + } + + if (config.theme === StaticThemePreset.CUSTOM && !config.customTheme) { + errors.push('Custom theme settings required when using CUSTOM theme.'); + } + + return { + valid: errors.length === 0, + errors + }; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/style.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/style.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..aedc0fbf053e6557f4a8673fad3c813e92a5337e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/style.dto.ts @@ -0,0 +1,103 @@ +/** + * @fileoverview Style DTOs + * Data Transfer Objects for Style management. + */ + +import { ThemeColorSchemeEnum, FontWeightEnum } from '../enums/admin.enums'; + +/** + * @swagger + * components: + * schemas: + * ColorPaletteEntry: + * type: object + * properties: + * name: + * type: string + * value: + * type: string + * description: + * type: string + * FontConfig: + * type: object + * properties: + * family: + * type: string + * fallback: + * type: string + * weights: + * type: array + * items: + * type: string + * googleFontUrl: + * type: string + * AdminStyleConfig: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * colorScheme: + * type: string + * enum: [LIGHT, DARK, SYSTEM] + * primaryColor: + * type: string + * secondaryColor: + * type: string + * accentColor: + * type: string + * backgroundColor: + * type: string + * textColor: + * type: string + * colorPalette: + * type: array + * items: + * $ref: '#/components/schemas/ColorPaletteEntry' + * headingFont: + * $ref: '#/components/schemas/FontConfig' + * bodyFont: + * $ref: '#/components/schemas/FontConfig' + * baseFontSize: + * type: integer + * borderRadius: + * type: integer + * isActive: + * type: boolean + */ +export interface ColorPaletteEntryDto { + name: string; + value: string; + description?: string; +} + +/** + * Font configuration. + */ +export interface FontConfigDto { + family: string; + fallback: string; + weights: FontWeightEnum[]; + googleFontUrl?: string; +} + +/** + * Full style configuration DTO. + */ +export interface AdminStyleConfigDto { + id?: number; + name: string; + colorScheme: ThemeColorSchemeEnum; + primaryColor: string; + secondaryColor: string; + accentColor: string; + backgroundColor: string; + textColor: string; + colorPalette: ColorPaletteEntryDto[]; + headingFont: FontConfigDto; + bodyFont: FontConfigDto; + baseFontSize: number; + borderRadius: number; + isActive: boolean; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/task.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/task.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b904790f8d517f06d1ae61622d2e7bbbfaf07a6 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/task.dto.ts @@ -0,0 +1,155 @@ +/** + * @fileoverview Task DTOs + * Data transfer objects for tasks with separate public and internal views. + * + * Philosophy: + * - PublicTaskDto: Shows only achievements and lessons (recognition & emulation) + * - TaskDto: Full internal access for authenticated collaborators + * + * @author Armand Richelet-Kleinberg + */ + +import { TaskStatus } from '../enums/task-status.enum'; + +// ═══════════════════════════════════════════════════════════════════════════ +// PUBLIC DTOs (External Resume View) +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Public task DTO for external visibility. + * Only includes Achieved tasks and Mistakes with lessons learned. + * Emphasizes recognition and constructive growth. + */ +export interface PublicTaskDto { + id: string; + title: string; + description?: string; + + /** Role played in this task (e.g., "Lead Developer") */ + role?: string; + + /** Status (only 'achieved' or 'mistake' with lesson) */ + status: TaskStatus.Achieved | TaskStatus.Mistake; + + /** Related project name */ + projectName?: string; + + /** Related project ID for linking */ + projectId?: string; + + /** Peer evaluation rating (1-5) */ + averageRating: number; + + /** Number of ratings received */ + ratingCount: number; + + // Deadline Performance (Recognition) + + /** Hours ahead (+) or behind (-) schedule */ + deadlinePerformanceHours?: number; + + /** Whether a valuable lesson was learned */ + lessonLearned: boolean; + + /** Public-safe lesson title (if lessonLearned) */ + lessonTitle?: string; + + /** Completion date */ + closingDate?: string; + + /** Whether to highlight prominently */ + isHighlighted: boolean; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// INTERNAL DTOs (Authenticated Access) +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Full task DTO for internal/authenticated view. + * Includes all fields including ongoing work and private details. + */ +export interface TaskDto extends Omit { + /** Full status including backlog/ongoing */ + status: TaskStatus; + + // Time tracking + estimatedDurationHours?: number; + actualDurationHours?: number; + dueDate?: string; + + /** Collaborator who owns this task */ + collaboratorId: string; + collaboratorName?: string; + + /** Detailed lesson description (internal only) */ + lessonDescription?: string; + + /** Display order */ + displayOrder: number; + + createdAt: string; + updatedAt: string; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// INPUT DTOs +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * DTO for creating a new task. + */ +export interface CreateTaskDto { + title: string; + description?: string; + role?: string; + status?: TaskStatus; + estimatedDurationHours?: number; + dueDate?: string; + projectId?: string; + displayOrder?: number; +} + +/** + * DTO for updating a task. + */ +export interface UpdateTaskDto extends Partial { + actualDurationHours?: number; + closingDate?: string; + lessonLearned?: boolean; + lessonTitle?: string; + lessonDescription?: string; + isHighlighted?: boolean; +} + +/** + * DTO for adding a rating to a task. + */ +export interface RateTaskDto { + rating: number; // 1-5 + comment?: string; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// SUMMARY DTOs +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Summary statistics for public accomplishments section. + */ +export interface AccomplishmentsSummaryDto { + /** Total completed tasks */ + totalAchieved: number; + + /** Average rating across all rated tasks */ + overallAverageRating: number; + + /** Total hours saved (positive deadline performance) */ + totalHoursSaved: number; + + /** Number of valuable lessons learned */ + lessonsLearnedCount: number; + + /** Number of highlighted accomplishments */ + highlightedCount: number; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/technology.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/technology.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ab9eaa5bf925edc588b57c6e8775d594bc223f0 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/technology.dto.ts @@ -0,0 +1,73 @@ +/** + * @fileoverview Technology DTOs + * Data transfer objects for technology/framework information + * + * @module dtos/technology.dto + * @author Armand Richelet-Kleinberg + */ + +/** + * Technology DTO + * Represents a single technology/framework with its metadata + */ +export interface TechnologyDto { + /** Unique identifier key (e.g., "react", "typescript") */ + key: string; + + /** Display name (e.g., "React", "TypeScript") */ + name: string; + + /** Full label (e.g., "React.js", "TypeScript") */ + label: string; + + /** Category ID this technology belongs to */ + category: string; + + /** Brief description of the technology */ + description: string; + + /** Icon class name (Font Awesome or Devicon) */ + icon: string; + + /** Brand color in hex format */ + color: string; + + /** Official website URL (optional) */ + website?: string; + + /** Supported/used versions (optional) */ + versions?: string[]; +} + +/** + * Technology Category DTO + * Represents a category grouping multiple technologies + */ +export interface TechnologyCategoryDto { + /** Unique category ID (e.g., "frontend", "backend") */ + id: string; + + /** Display name (e.g., "Frontend Frameworks & Libraries") */ + name: string; + + /** Category description */ + description: string; + + /** Display order */ + order: number; + + /** Technologies in this category */ + technologies: TechnologyDto[]; +} + +/** + * Technologies Response DTO + * Full response containing all categories and technologies + */ +export interface TechnologiesResponseDto { + /** All technology categories with their technologies */ + categories: TechnologyCategoryDto[]; + + /** Flat array of all technologies for quick lookup */ + technologies: TechnologyDto[]; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/user-management.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/user-management.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..7502725b88565d1af65bc33acdcadc249438e4e8 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/user-management.dto.ts @@ -0,0 +1,143 @@ +/** + * @fileoverview User Management DTOs + * Data Transfer Objects for admin user management. + * + * @author Armand Richelet-Kleinberg + */ + +import { Role } from '../enums/role.enum'; + +/** + * User data for admin management views. + */ +export interface UserDto { + id: string; + username: string; + email?: string; + avatarUrl?: string; + roles: Role[]; + isActive: boolean; + emailConfirmed: boolean; + lastLogin?: string; + failedLoginAttempts: number; + isLocked: boolean; + lockoutUntil?: string; + collaboratorId?: string; + createdAt: string; + updatedAt: string; +} + +/** + * User with linked collaborator data. + */ +export interface UserWithCollaboratorDto extends UserDto { + collaborator?: { + id: string; + firstName: string; + lastName: string; + position: string; + avatarUrl?: string; + }; +} + +/** + * User summary for listings. + */ +export interface UserSummaryDto { + id: string; + username: string; + email?: string; + avatarUrl?: string; + roles: Role[]; + isActive: boolean; + lastLogin?: string; +} + +/** + * Request to assign a role to a user. + */ +export interface AssignRoleDto { + userId: string; + role: Role; + /** Required when assigning COLLABORATOR role */ + reportsToId?: string; +} + +/** + * Request to revoke a role from a user. + */ +export interface RevokeRoleDto { + userId: string; + role: Role; +} + +/** + * Role assignment history entry. + */ +export interface RoleAssignmentDto { + id: number; + userId: string; + role: Role; + assignedAt: string; + assignedBy?: { + id: string; + username: string; + }; +} + +/** + * Request to create a user (admin action). + */ +export interface AdminCreateUserDto { + username: string; + email: string; + password: string; + roles: Role[]; + collaboratorId?: string; + sendWelcomeEmail?: boolean; +} + +/** + * Request to update user (admin action). + */ +export interface AdminUpdateUserDto { + username?: string; + email?: string; + avatarUrl?: string; + isActive?: boolean; + emailConfirmed?: boolean; + collaboratorId?: string; +} + +/** + * Request to unlock a locked user account. + */ +export interface UnlockUserDto { + userId: string; + resetFailedAttempts?: boolean; +} + +/** + * User list query parameters. + */ +export interface UserListQueryDto { + search?: string; + role?: Role; + isActive?: boolean; + isLocked?: boolean; + page?: number; + limit?: number; + sortBy?: 'username' | 'email' | 'lastLogin' | 'createdAt'; + sortOrder?: 'asc' | 'desc'; +} + +/** + * Paginated user list response. + */ +export interface UserListResponseDto { + users: UserSummaryDto[]; + total: number; + page: number; + limit: number; + totalPages: number; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/dtos/widget.dto.ts b/Ark.Alliance.StartupCms.Ai.Share/dtos/widget.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..6de310856d6963e4be0f5bf106cadb8e6d43222a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/dtos/widget.dto.ts @@ -0,0 +1,59 @@ +/** + * @fileoverview Widget DTOs + * Data Transfer Objects for Widget management. + */ + +import { WidgetTypeEnum } from '../enums/admin.enums'; + +/** + * @swagger + * components: + * schemas: + * AdminWidget: + * type: object + * properties: + * id: + * type: integer + * type: + * type: string + * enum: [TEXT, IMAGE, CODE, CAROUSEL, SPACER, DIVIDER] + * title: + * type: string + * content: + * type: string + * order: + * type: integer + * pageId: + * type: string + * isActive: + * type: boolean + * config: + * type: object + * ReorderWidgets: + * type: object + * properties: + * pageId: + * type: string + * widgetIds: + * type: array + * items: + * type: integer + */ +export interface AdminWidgetDto { + id?: number; + type: WidgetTypeEnum; + title?: string; + content?: string; + order: number; + pageId: string; + isActive: boolean; + config?: Record; +} + +/** + * Reorder widgets request. + */ +export interface ReorderWidgetsDto { + pageId: string; + widgetIds: number[]; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/enums/admin.enums.ts b/Ark.Alliance.StartupCms.Ai.Share/enums/admin.enums.ts new file mode 100644 index 0000000000000000000000000000000000000000..13972dee75361bf31673f5e2e558a57c76381691 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/enums/admin.enums.ts @@ -0,0 +1,127 @@ +/** + * @fileoverview Admin Enums + * Enumerations for the administration system. + * + * @author Armand Richelet-Kleinberg + */ + +/** + * Widget display types for front page components. + */ +export enum WidgetTypeEnum { + HERO = 'hero', + STATS = 'stats', + CAROUSEL = 'carousel', + TIMELINE = 'timeline', + GRID = 'grid', + TEXT = 'text', + IMAGE = 'image', + VIDEO = 'video', + SKILLS = 'skills', + CONTACT = 'contact' +} + +/** + * Menu item positioning options. + */ +export enum MenuPositionEnum { + HEADER = 'header', + FOOTER = 'footer', + RADIAL = 'radial', + SIDEBAR = 'sidebar', + MOBILE = 'mobile' +} + +/** + * Media asset types supported by the system. + * @remarks Used for filtering and rendering appropriate previews. + */ +export enum MediaTypeEnum { + /** Image files (JPEG, PNG, GIF, WebP, SVG) */ + IMAGE = 'image', + /** Video files (MP4, WebM, MOV) */ + VIDEO = 'video', + /** Audio files (MP3, WAV, OGG) */ + AUDIO = 'audio', + /** PDF documents */ + PDF = 'pdf', + /** Word documents (DOC, DOCX) */ + WORD = 'word', + /** Excel spreadsheets (XLS, XLSX) */ + EXCEL = 'excel', + /** Markdown files */ + MARKDOWN = 'markdown', + /** JSON files */ + JSON = 'json', + /** Icon files */ + ICON = 'icon', + /** Other file types */ + OTHER = 'other' +} + +/** + * Media source types. + * @remarks Indicates where the media is stored. + */ +export enum MediaSourceEnum { + /** External URL */ + URL = 'url', + /** Local file upload */ + UPLOAD = 'upload', + /** AWS S3 or compatible storage */ + S3 = 's3' +} + +/** + * Theme color scheme options. + */ +export enum ThemeColorSchemeEnum { + LIGHT = 'light', + DARK = 'dark', + AUTO = 'auto' +} + +/** + * Font weight options. + */ +export enum FontWeightEnum { + THIN = '100', + LIGHT = '300', + REGULAR = '400', + MEDIUM = '500', + SEMIBOLD = '600', + BOLD = '700', + EXTRABOLD = '800', + BLACK = '900' +} + +/** + * Admin operation types for audit logs. + */ +export enum AdminOperationEnum { + CREATE = 'create', + UPDATE = 'update', + DELETE = 'delete', + REORDER = 'reorder', + TOGGLE = 'toggle', + UPLOAD = 'upload' +} + +/** + * Content entity types for admin operations. + */ +export enum AdminEntityTypeEnum { + PROJECT = 'project', + EXPERIENCE = 'experience', + SKILL = 'skill', + EDUCATION = 'education', + LANGUAGE = 'language', + HOBBY = 'hobby', + BUSINESS_DOMAIN = 'business_domain', + WIDGET = 'widget', + MENU_ITEM = 'menu_item', + STYLE = 'style', + MEDIA = 'media', + PROFILE = 'profile' +} + diff --git a/Ark.Alliance.StartupCms.Ai.Share/enums/ai-provider.enum.ts b/Ark.Alliance.StartupCms.Ai.Share/enums/ai-provider.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..cadda5afccc25580b4d962486dad3f8e629c33aa --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/enums/ai-provider.enum.ts @@ -0,0 +1,9 @@ +/** + * Supported AI Providers. + */ +export enum AiProviderEnum { + OPENAI = 'openai', + ANTHROPIC = 'anthropic', + GOOGLE = 'google', + CUSTOM = 'custom' +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/enums/audit-action.enum.ts b/Ark.Alliance.StartupCms.Ai.Share/enums/audit-action.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..3abcf0b26d9e5d2bbf163b9312e937047f966b9e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/enums/audit-action.enum.ts @@ -0,0 +1,163 @@ +/** + * @fileoverview Audit Action Enum + * Defines all auditable events in the system. + * Used by AuditLogService to track security-relevant activities. + * + * @author Armand Richelet-Kleinberg + */ + +/** + * Actions tracked in the audit log. + * @remarks + * All authentication, role management, and security-sensitive + * operations are logged for compliance and monitoring. + */ +export enum AuditAction { + // ============================================ + // Authentication Events + // ============================================ + + /** Successful user login */ + LOGIN_SUCCESS = 'LOGIN_SUCCESS', + + /** Failed login attempt (wrong password or non-existent user) */ + LOGIN_FAILURE = 'LOGIN_FAILURE', + + /** User logged out */ + LOGOUT = 'LOGOUT', + + /** Access token refreshed using refresh token */ + TOKEN_REFRESH = 'TOKEN_REFRESH', + + // ============================================ + // Password Events + // ============================================ + + /** User changed their own password */ + PASSWORD_CHANGE = 'PASSWORD_CHANGE', + + /** Password reset email requested */ + PASSWORD_RESET_REQUEST = 'PASSWORD_RESET_REQUEST', + + /** Password reset completed via email link */ + PASSWORD_RESET_COMPLETE = 'PASSWORD_RESET_COMPLETE', + + // ============================================ + // Registration & Email + // ============================================ + + /** New user registered */ + REGISTRATION = 'REGISTRATION', + + /** Email address confirmed via link */ + EMAIL_CONFIRMED = 'EMAIL_CONFIRMED', + + // ============================================ + // Role Management + // ============================================ + + /** Role assigned to user */ + ROLE_ASSIGNED = 'ROLE_ASSIGNED', + + /** Role revoked from user */ + ROLE_REVOKED = 'ROLE_REVOKED', + + /** User submitted role upgrade request */ + ROLE_REQUEST_SUBMITTED = 'ROLE_REQUEST_SUBMITTED', + + /** Role upgrade request approved */ + ROLE_REQUEST_APPROVED = 'ROLE_REQUEST_APPROVED', + + /** Role upgrade request rejected */ + ROLE_REQUEST_REJECTED = 'ROLE_REQUEST_REJECTED', + + // ============================================ + // Profile & Account + // ============================================ + + /** User profile updated */ + PROFILE_UPDATED = 'PROFILE_UPDATED', + + /** Avatar/profile picture uploaded */ + AVATAR_UPLOADED = 'AVATAR_UPLOADED', + + // ============================================ + // Account Security + // ============================================ + + /** Account locked due to failed login attempts */ + ACCOUNT_LOCKED = 'ACCOUNT_LOCKED', + + /** Account unlocked (by admin or timeout) */ + ACCOUNT_UNLOCKED = 'ACCOUNT_UNLOCKED', + + /** Refresh token manually revoked */ + TOKEN_REVOKED = 'TOKEN_REVOKED', + + /** All user sessions terminated */ + ALL_SESSIONS_REVOKED = 'ALL_SESSIONS_REVOKED', + + // ============================================ + // Organization & Team + // ============================================ + + /** Organization settings updated */ + ORGANIZATION_UPDATED = 'ORGANIZATION_UPDATED', + + /** Collaborator added to organization */ + COLLABORATOR_ADDED = 'COLLABORATOR_ADDED', + + /** Collaborator removed from organization */ + COLLABORATOR_REMOVED = 'COLLABORATOR_REMOVED', + + /** Collaborator hierarchy changed (reports to) */ + HIERARCHY_CHANGED = 'HIERARCHY_CHANGED' +} + +/** + * Group audit actions by category for filtering. + */ +export const AuditActionCategories: Record = { + 'Authentication': [ + AuditAction.LOGIN_SUCCESS, + AuditAction.LOGIN_FAILURE, + AuditAction.LOGOUT, + AuditAction.TOKEN_REFRESH + ], + 'Password': [ + AuditAction.PASSWORD_CHANGE, + AuditAction.PASSWORD_RESET_REQUEST, + AuditAction.PASSWORD_RESET_COMPLETE + ], + 'Registration': [ + AuditAction.REGISTRATION, + AuditAction.EMAIL_CONFIRMED + ], + 'Role Management': [ + AuditAction.ROLE_ASSIGNED, + AuditAction.ROLE_REVOKED, + AuditAction.ROLE_REQUEST_SUBMITTED, + AuditAction.ROLE_REQUEST_APPROVED, + AuditAction.ROLE_REQUEST_REJECTED + ], + 'Account Security': [ + AuditAction.ACCOUNT_LOCKED, + AuditAction.ACCOUNT_UNLOCKED, + AuditAction.TOKEN_REVOKED, + AuditAction.ALL_SESSIONS_REVOKED + ], + 'Organization': [ + AuditAction.ORGANIZATION_UPDATED, + AuditAction.COLLABORATOR_ADDED, + AuditAction.COLLABORATOR_REMOVED, + AuditAction.HIERARCHY_CHANGED + ] +}; + +/** + * Get display label for an audit action. + */ +export function getAuditActionLabel(action: AuditAction): string { + return action.replace(/_/g, ' ').toLowerCase() + .replace(/\b\w/g, c => c.toUpperCase()); +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/enums/proficiency-level.enum.ts b/Ark.Alliance.StartupCms.Ai.Share/enums/proficiency-level.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..8fe22ca62b3ae776f59714d79c01528bc181e996 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/enums/proficiency-level.enum.ts @@ -0,0 +1,57 @@ +/** + * @fileoverview Proficiency Level Enum + * Represents proficiency levels on a 1-5 scale for language skills. + * + * @author Armand Richelet-Kleinberg + */ + +/** + * Language proficiency level enumeration. + * Used for speaking, writing, and presenting abilities. + * + * @remarks + * Values are integers from 1-5 for easy storage and comparison. + * Use PROFICIENCY_LABELS for display text. + */ +export enum ProficiencyLevel { + /** Basic understanding, limited practical ability */ + BASIC = 1, + /** Elementary level, can handle simple situations */ + ELEMENTARY = 2, + /** Intermediate level, can handle most situations */ + INTERMEDIATE = 3, + /** Advanced level, can handle complex situations */ + ADVANCED = 4, + /** Native or fully fluent */ + NATIVE = 5 +} + +/** + * Human-readable labels for proficiency levels. + * Use for display in UI. + */ +export const PROFICIENCY_LABELS: Record = { + [ProficiencyLevel.BASIC]: 'Basic', + [ProficiencyLevel.ELEMENTARY]: 'Elementary', + [ProficiencyLevel.INTERMEDIATE]: 'Intermediate', + [ProficiencyLevel.ADVANCED]: 'Advanced', + [ProficiencyLevel.NATIVE]: 'Native/Fluent' +}; + +/** + * Get label for a proficiency level value. + * @param level - Proficiency level (1-5) + * @returns Human-readable label + */ +export function getProficiencyLabel(level: number): string { + return PROFICIENCY_LABELS[level as ProficiencyLevel] || 'Unknown'; +} + +/** + * Validate that a value is a valid proficiency level. + * @param value - Value to validate + * @returns True if valid (1-5), false otherwise + */ +export function isValidProficiencyLevel(value: number): boolean { + return Number.isInteger(value) && value >= 1 && value <= 5; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/enums/project-section.enum.ts b/Ark.Alliance.StartupCms.Ai.Share/enums/project-section.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffb581a9c3f238b1ad969765ba035a094bc3540f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/enums/project-section.enum.ts @@ -0,0 +1,68 @@ +/** + * @fileoverview Project Presentation Section Enumeration + * Defines the available sections in a project presentation/detail page. + * + * Responsibilities: + * - Define valid section identifiers for project presentation + * - Support type-safe section navigation + * - Maintain consistent section naming across frontend and backend + * + * Inputs: None (enum definition) + * Outputs: Type-safe enum values + * Side Effects: None + * Invariants: Section values must match route segments and API expectations + * + * @example + * ```typescript + * import { ProjectSection } from '@ark-alliance-startupcms-ia/share'; + * + * const currentSection: ProjectSection = ProjectSection.OVERVIEW; + * ``` + * + * @author Armand Richelet-Kleinberg + */ + +/** + * Project Presentation Section Enumeration + * + * Represents the different sections/tabs available in a project detail view. + * These correspond to content pages within a project's documentation. + * + * @remarks + * - Values are lowercase for use in URLs and API calls + * - Display names should be derived from TERMINOLOGY constants + * - Order reflects recommended navigation sequence + */ +export enum ProjectSection { + /** Project overview and summary */ + OVERVIEW = 'overview', + + /** Architecture diagrams and system design */ + ARCHITECTURE = 'architecture', + + /** Functional requirements and user stories */ + FUNCTIONAL = 'functional', + + /** Features and functionality demonstration */ + FEATURES = 'features', + + /** Technical implementation details */ + TECHNICAL = 'technical', + + /** Development roadmap and future plans */ + ROADMAP = 'roadmap', + + /** Media gallery (screenshots, videos, demos) */ + GALLERY = 'gallery', +} + +/** + * Default section to display when loading a project + */ +export const DEFAULT_PROJECT_SECTION = ProjectSection.OVERVIEW; + +/** + * Maximum number of images allowed in gallery section + */ +export const MAX_GALLERY_IMAGES = 20; + diff --git a/Ark.Alliance.StartupCms.Ai.Share/enums/project-status.enum.ts b/Ark.Alliance.StartupCms.Ai.Share/enums/project-status.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..f991448e791b414ecad54eaafc36941078ab589a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/enums/project-status.enum.ts @@ -0,0 +1,14 @@ +/** + * Project status enumeration. + * Represents the current lifecycle state of a portfolio project. + */ +export enum ProjectStatus { + /** Project is actively being developed */ + IN_PROGRESS = 'In Progress', + /** Project development is finished */ + COMPLETED = 'Completed', + /** Project is in maintenance mode (bug fixes, updates) */ + MAINTENANCE = 'Maintenance', + /** Project is no longer actively maintained */ + ARCHIVED = 'Archived' +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/enums/resume-tab.enum.ts b/Ark.Alliance.StartupCms.Ai.Share/enums/resume-tab.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..4368ae6116e3a1b38ed7a5560b2179515af05fe6 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/enums/resume-tab.enum.ts @@ -0,0 +1,14 @@ +/** + * Tabs available in the Resume Manager. + * Represents the different sections of the resume data. + */ +export enum ResumeTabEnum { + PROFILE = 'profile', + EXPERIENCE = 'experience', + EDUCATION = 'education', + SKILLS = 'skills', + LANGUAGES = 'languages', + HOBBIES = 'hobbies', + BUSINESS_DOMAINS = 'business-domains', + AI_SETTINGS = 'ai-settings' +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/enums/role.enum.ts b/Ark.Alliance.StartupCms.Ai.Share/enums/role.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..81ff654cede20f80480c2603806fc589b9d9f8fa --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/enums/role.enum.ts @@ -0,0 +1,93 @@ +/** + * @fileoverview Role Enum + * Defines user roles for multi-role RBAC. + * Users can have multiple roles simultaneously. + * Permissions are the union of all assigned roles. + * + * @author Armand Richelet-Kleinberg + */ + +/** + * User roles for access control. + * @remarks + * - ADMIN: Full system access + * - SUPERVISOR: User management, role assignment (except admin), audit viewing + * - COLLABORATOR: Part of org hierarchy, full resume access, AI assistant + * - MEMBER: Full resume access, AI assistant, not in hierarchy + * - GUEST: View-only access, basic profile editing + */ +export enum Role { + /** Full system access - manages all users, settings, and content */ + ADMIN = 'admin', + + /** User management, role assignment (except admin), audit viewing */ + SUPERVISOR = 'supervisor', + + /** Part of org hierarchy, full resume access, AI assistant */ + COLLABORATOR = 'collaborator', + + /** Full resume access, AI assistant, not in hierarchy */ + MEMBER = 'member', + + /** View-only access, basic profile editing */ + GUEST = 'guest' +} + +/** + * Role hierarchy for display ordering. + * Higher number = higher privilege level. + */ +export const RolePriority: Record = { + [Role.GUEST]: 1, + [Role.MEMBER]: 2, + [Role.COLLABORATOR]: 3, + [Role.SUPERVISOR]: 4, + [Role.ADMIN]: 5 +}; + +/** + * Roles that Supervisors can assign/revoke. + * Supervisors cannot assign ADMIN or SUPERVISOR roles. + */ +export const SupervisorAssignableRoles: Role[] = [ + Role.GUEST, + Role.MEMBER, + Role.COLLABORATOR +]; + +/** + * Roles that require "Reports To" (hierarchy membership). + * Collaborators must have a manager assigned. + */ +export const HierarchyRoles: Role[] = [Role.COLLABORATOR]; + +/** + * Roles that have access to resume editing and AI assistant. + */ +export const ResumeAccessRoles: Role[] = [ + Role.ADMIN, + Role.SUPERVISOR, + Role.COLLABORATOR, + Role.MEMBER +]; + +/** + * Check if a role has higher or equal priority than another. + */ +export function hasHigherOrEqualPriority(role: Role, thanRole: Role): boolean { + return RolePriority[role] >= RolePriority[thanRole]; +} + +/** + * Get display label for a role. + */ +export function getRoleLabel(role: Role): string { + const labels: Record = { + [Role.ADMIN]: 'Administrator', + [Role.SUPERVISOR]: 'Supervisor', + [Role.COLLABORATOR]: 'Collaborator', + [Role.MEMBER]: 'Member', + [Role.GUEST]: 'Guest' + }; + return labels[role] || role; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/enums/settings-tab.enum.ts b/Ark.Alliance.StartupCms.Ai.Share/enums/settings-tab.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..4de19f4be1e893f0d593dd4af9cb0ee60d963269 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/enums/settings-tab.enum.ts @@ -0,0 +1,55 @@ +/** + * @fileoverview Organization Settings Tab Enum + * Defines tabs for organization settings page. + * + * @author Armand Richelet-Kleinberg + */ + +/** + * Organization settings page tabs. + */ +export enum SettingsTab { + /** General organization information */ + GENERAL = 'general', + + /** Location and address settings */ + LOCATION = 'location', + + /** Content like description, mission, vision */ + CONTENT = 'content', + + /** Branding and media assets */ + MEDIA = 'media' +} + +/** + * Tab configuration with labels for UI display. + */ +export const SettingsTabConfig: Record = { + [SettingsTab.GENERAL]: { + label: 'General', + description: 'Basic organization information' + }, + [SettingsTab.LOCATION]: { + label: 'Location', + description: 'Address and geographic details' + }, + [SettingsTab.CONTENT]: { + label: 'Content', + description: 'Mission, vision, and description' + }, + [SettingsTab.MEDIA]: { + label: 'Media', + description: 'Logo, icons, and branding' + } +}; + +/** + * Get all settings tabs in display order. + */ +export const SETTINGS_TABS_ORDER: SettingsTab[] = [ + SettingsTab.GENERAL, + SettingsTab.LOCATION, + SettingsTab.CONTENT, + SettingsTab.MEDIA +]; diff --git a/Ark.Alliance.StartupCms.Ai.Share/enums/skill-level.enum.ts b/Ark.Alliance.StartupCms.Ai.Share/enums/skill-level.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..cbd67b9a06f6f004115173540a6b370f295b867a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/enums/skill-level.enum.ts @@ -0,0 +1,16 @@ +/** + * Skill proficiency level enumeration. + * Represents the level of expertise in a particular skill. + */ +export enum SkillLevel { + /** Basic understanding, limited practical experience */ + BEGINNER = 'Beginner', + /** Working knowledge, can apply with guidance */ + INTERMEDIATE = 'Intermediate', + /** Strong proficiency, can work independently */ + ADVANCED = 'Advanced', + /** Deep expertise, can mentor others */ + EXPERT = 'Expert', + /** Recognized authority, sets standards */ + MASTER = 'Master' +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/enums/static-generation.enum.ts b/Ark.Alliance.StartupCms.Ai.Share/enums/static-generation.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e4b326176bb81ef41239ee1e414553e69de0cb9 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/enums/static-generation.enum.ts @@ -0,0 +1,77 @@ +/** + * @fileoverview Static Generation Enums + * Enums for static site generation configuration and status. + * + * @author Armand Richelet-Kleinberg + */ + +/** + * Types of pages that can be included in static export. + */ +export enum StaticPageType { + /** Home page with carousel and quick nav */ + HOME = 'home', + /** Resume page with experience, education, skills */ + RESUME = 'resume', + /** Projects catalogue page */ + PROJECTS = 'projects', + /** Individual project detail page */ + PROJECT_DETAIL = 'project_detail' +} + +/** + * Status of static export generation process. + */ +export enum StaticExportStatus { + /** Export not started */ + PENDING = 'pending', + /** Export in progress */ + GENERATING = 'generating', + /** Export completed successfully */ + COMPLETED = 'completed', + /** Export failed */ + FAILED = 'failed' +} + +/** + * Sections that can be included in the resume export. + */ +export enum StaticResumeSectionType { + PROFILE = 'profile', + EXPERIENCE = 'experience', + EDUCATION = 'education', + SKILLS = 'skills', + LANGUAGES = 'languages', + HOBBIES = 'hobbies', + BUSINESS_DOMAINS = 'business_domains' +} + +/** + * Asset types for static export. + */ +export enum StaticAssetType { + /** Image files (png, jpg, webp, svg) */ + IMAGE = 'image', + /** Font files (woff, woff2, ttf) */ + FONT = 'font', + /** Icon files */ + ICON = 'icon', + /** CSS stylesheets */ + STYLESHEET = 'stylesheet', + /** Other assets */ + OTHER = 'other' +} + +/** + * Theme presets for static export. + */ +export enum StaticThemePreset { + /** Dark architectural theme (default) */ + ARCHITECTURAL = 'architectural', + /** Purple aloevera theme */ + ALOEVERA = 'aloevera', + /** Light modern theme */ + LIGHT = 'light', + /** Custom user-defined theme */ + CUSTOM = 'custom' +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/enums/task-status.enum.ts b/Ark.Alliance.StartupCms.Ai.Share/enums/task-status.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..4658a6d8d02fba8986872ccd4ee5c129bc9dc13c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/enums/task-status.enum.ts @@ -0,0 +1,61 @@ +/** + * @fileoverview Task Status Enum + * Defines workflow states for tasks with public visibility rules. + * + * Visibility Philosophy: + * - Backlog/Ongoing: Private (psychological safety for work-in-progress) + * - Achieved: Public (recognition of accomplishments) + * - Mistake with lesson: Public (celebrates growth and learning) + * - Mistake without lesson: Private (preserved for internal reflection) + * + * @author Armand Richelet-Kleinberg + */ + +/** + * Task workflow status enumeration. + * Used for filtering public vs internal visibility. + */ +export enum TaskStatus { + /** Task is queued but not started - PRIVATE */ + Backlog = 'backlog', + + /** Task is actively being worked on - PRIVATE */ + Ongoing = 'ongoing', + + /** Task completed successfully - PUBLIC */ + Achieved = 'achieved', + + /** Task encountered issues - PUBLIC only if lessonLearned=true */ + Mistake = 'mistake' +} + +/** + * Helper to check if a task status qualifies for public visibility. + * @param status - The task status + * @param lessonLearned - Whether a lesson was learned (for Mistake status) + */ +export function isPubliclyVisible(status: TaskStatus, lessonLearned: boolean): boolean { + if (status === TaskStatus.Achieved) return true; + if (status === TaskStatus.Mistake && lessonLearned) return true; + return false; +} + +/** + * Status labels for display + */ +export const TASK_STATUS_LABELS: Record = { + [TaskStatus.Backlog]: 'Backlog', + [TaskStatus.Ongoing]: 'En cours', + [TaskStatus.Achieved]: 'Réalisé', + [TaskStatus.Mistake]: 'Leçon apprise' +}; + +/** + * Status colors for UI badges + */ +export const TASK_STATUS_COLORS: Record = { + [TaskStatus.Backlog]: '#6b7280', // Gray + [TaskStatus.Ongoing]: '#3b82f6', // Blue + [TaskStatus.Achieved]: '#10b981', // Green + [TaskStatus.Mistake]: '#f59e0b' // Amber (growth-focused, not red) +}; diff --git a/Ark.Alliance.StartupCms.Ai.Share/index.ts b/Ark.Alliance.StartupCms.Ai.Share/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8784d0d8eda2466e467960f732b3987a46e00b76 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/index.ts @@ -0,0 +1,55 @@ +// Enums +export * from './enums/project-status.enum'; +export * from './enums/project-section.enum'; +export * from './enums/skill-level.enum'; +export * from './enums/admin.enums'; +export * from './enums/proficiency-level.enum'; +export * from './enums/static-generation.enum'; +export * from './enums/resume-tab.enum'; +export * from './enums/ai-provider.enum'; +export * from './enums/role.enum'; +export * from './enums/audit-action.enum'; +export * from './enums/settings-tab.enum'; +export * from './enums/task-status.enum'; + +// DTOs +export * from './dtos/admin.dto'; +export * from './dtos/ai.dto'; +export * from './dtos/auth.dto'; +export * from './dtos/business-domain.dto'; +export * from './dtos/carousel.dto'; +export * from './dtos/common.dto'; +export * from './dtos/dashboard.dto'; +export * from './dtos/hobby.dto'; +export * from './dtos/language.dto'; +export * from './dtos/media.dto'; +export * from './dtos/menu.dto'; +export * from './dtos/profile.dto'; +export * from './dtos/project.dto'; +export * from './dtos/resume.dto'; +export * from './dtos/skill-category.dto'; +export * from './dtos/static-generation.dto'; +export * from './dtos/style.dto'; +export * from './dtos/technology.dto'; +export * from './dtos/widget.dto'; +export * from './dtos/organization.dto'; +export * from './dtos/collaborator.dto'; +export * from './dtos/user-management.dto'; +export * from './dtos/audit-log.dto'; +export * from './dtos/ai-resume.dto'; +export * from './dtos/task.dto'; + +// Constants +export * from './constants/terminology.constants'; +export * from './constants/ui-layout.constants'; +export * from './constants/resume.constants'; +export * from './constants/static-generation.constants'; +export * from './constants/ai.constants'; + +// Mocks +export * from './mocks/resume.mock'; +export * from './mocks/profile.mock'; +export * from './mocks/projects.mock'; +export * from './mocks/technology.mock'; +export * from './mocks/seed-data'; + diff --git a/Ark.Alliance.StartupCms.Ai.Share/mocks/profile.mock.ts b/Ark.Alliance.StartupCms.Ai.Share/mocks/profile.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca6debe66e401388f6f79cd6732b18ea2c094937 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/mocks/profile.mock.ts @@ -0,0 +1,24 @@ +/** + * @fileoverview Profile Mock Data + * Mock data for profile matching backend seed data. + * + * @author Armand Richelet-Kleinberg + */ + +import { ProfileDto } from '../dtos/profile.dto'; +import { SEED_PROFILE } from './seed-data'; + +/** + * Profile mock using centralized seed data. + * Matches: InitDbAsset/JsonDatas/profile.json + */ +export const PROFILE_MOCK: ProfileDto = { + firstName: SEED_PROFILE.firstName, + lastName: SEED_PROFILE.lastName, + title: SEED_PROFILE.title, + overview: SEED_PROFILE.overview, + email: SEED_PROFILE.email, + githubUrl: SEED_PROFILE.githubUrl, + linkedinUrl: SEED_PROFILE.linkedinUrl, + avatarUrl: SEED_PROFILE.avatarUrl +}; diff --git a/Ark.Alliance.StartupCms.Ai.Share/mocks/projects.mock.ts b/Ark.Alliance.StartupCms.Ai.Share/mocks/projects.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..bcf4dd9c1398242e1ee7ac0295e940de58d10bc6 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/mocks/projects.mock.ts @@ -0,0 +1,199 @@ +/** + * @fileoverview Project Mock Data + * Uses the actual seed data from projects.json as the single source of truth. + * This ensures tests and fallback data are always aligned with the actual database seed data. + * + * @author Armand Richelet-Kleinberg + */ + +import { ProjectDto, ProjectFeatureDto } from '../dtos/project.dto'; +import { ProjectStatus } from '../enums/project-status.enum'; + +/** + * Raw project data from the backend seed file. + * This structure matches the JSON format used in projects.json. + */ +interface RawProjectData { + title: string; + description: string; + status: string; + isFeatured?: boolean; + imageUrl?: string; + repoUrl?: string; + demoUrl?: string; + packageUrl?: string; + technologies: string[]; + features?: Array<{ + title: string; + description: string; + icon?: string; + imageUrl?: string; + }>; + pages?: Array<{ + title: string; + type: string; + content: string; + }>; +} + +/** + * Seed data matching projects.json in the Backend. + * Updated to match the latest backend seed data. + */ +const SEED_PROJECTS: RawProjectData[] = [ + { + title: "Ark.Alliance Trading Ecosystem", + description: "A high-frequency crypto trading bot and ecosystem featuring a cyberpunk dashboard, real-time WebSocket streaming, and AI-driven trend detection. Built on a microservices architecture with a focus on resilience and speed.", + status: "In Progress", + isFeatured: true, + imageUrl: "/assets/Bot10.PNG", + repoUrl: "https://github.com/ark/alliance", + demoUrl: "https://ark-alliance.demo", + technologies: ["C#", "React", "TypeScript", "Python", "Docker", "Kubernetes", "Three.js", "OpenAI"], + features: [ + { title: "Strategy Command Center", description: "The central nerve center of the operation. Visualizes PnL in real-time with sub-millisecond updates via WebSockets.", icon: "monitor", imageUrl: "/assets/Bot10.PNG" }, + { title: "3D Market Topography", description: "Interactive Three.js visualization of the order book. Peaks and valleys represent buy/sell walls.", icon: "box", imageUrl: "/assets/Bot6.PNG" }, + { title: "AI Logic Analysis", description: "Real-time introspection into the 'Gemini' AI Strategy. Shows the calculated volatility threshold and trend confidence.", icon: "cpu", imageUrl: "/assets/Bot2.PNG" }, + { title: "Logistics Matrix", description: "A grid view for managing hundreds of concurrent 'Worker' instances. Each cell represents a Docker container.", icon: "grid", imageUrl: "/assets/Bot13.PNG" }, + { title: "Configuration Console", description: "Fine-grained control over trading parameters. Changes propagate to active instances immediately.", icon: "sliders", imageUrl: "/assets/Bot5.PNG" }, + { title: "Execution Logs", description: "Live stream of order execution events. Tracks latency statistics and API weight usage.", icon: "activity", imageUrl: "/assets/Bot4.PNG" } + ] + }, + { + title: "Logistics Orchestration Platform", + description: "A comprehensive logistics supply chain platform for Ahold Delhaize, integrating TMS (Transport Management Systems), SAP, and real-time delivery tracking.", + status: "Completed", + isFeatured: true, + imageUrl: "/assets/Bot13.PNG", + technologies: ["C#", ".NET 8", "Blazor", "Azure", "SAP", "Microservices", "CQRS"], + features: [ + { title: "TMS Integration", description: "Seamless connecting with Ortec & Axiodis TMS.", icon: "truck" }, + { title: "Global Tracking", description: "Real-time visibility into delivery status across Europe.", icon: "map" } + ] + }, + { + title: "Live Show Control System", + description: "Real-time audiovisual synchronization system for immersive live performances.", + status: "Completed", + imageUrl: "/assets/Bot6.PNG", + technologies: ["C++", "Python", "Unity", "C#"], + features: [ + { title: "AV Sync", description: "Frame-perfect synchronization.", icon: "music" }, + { title: "3D Visuals", description: "Real-time stereoscopic 3D content.", icon: "box" } + ] + }, + { + title: "Asset Position Integration", + description: "Financial system integration bridging legacy Mainframe data with modern web interfaces.", + status: "Completed", + imageUrl: "/assets/Bot12.PNG", + technologies: ["C#", ".NET 5", "Docker", "MQSeries", "Cobol"], + features: [ + { title: "Legacy Bridge", description: "Secure communication with Mainframe.", icon: "server" } + ] + }, + // Additional portfolio projects for comprehensive mock data + { + title: "Ark.Portfolio - AI-Powered Portfolio CMS", + description: "Enterprise-grade, AI-powered portfolio platform combining comprehensive content management with multi-provider AI integration (OpenAI, Anthropic, Google Gemini), static site export capabilities, and end-to-end type safety.", + status: "Featured", + isFeatured: true, + imageUrl: "/Assets/Projects/Ark.Portfolio/portfolio-hero.png", + repoUrl: "https://github.com/M2H-Machine-to-Human-Race/Ark.Alliance.Portfolio", + technologies: ["react", "typescript", "nodejs", "express", "sqlite", "rest", "openai", "anthropic", "gemini"], + features: [ + { title: "Multi-Provider AI Integration", description: "Seamlessly integrated with OpenAI GPT-4, Anthropic Claude 3, and Google Gemini", icon: "brain" }, + { title: "Comprehensive CMS", description: "Full-featured content management system with 20 TypeORM entities", icon: "database" }, + { title: "Static Site Export", description: "One-click generation of deployable static websites from CMS content", icon: "download" }, + { title: "Enterprise Architecture", description: "Clean three-tier architecture with MVVM pattern", icon: "sitemap" }, + { title: "Production-Grade Security", description: "JWT authentication, bcrypt password hashing, Helmet security headers", icon: "shield-alt" }, + { title: "Dynamic Theming", description: "Runtime theme switching with CSS variable injection", icon: "palette" } + ] + }, + { + title: "Ark.Alliance.React.Component.UI", + description: "Enterprise-grade React component library with MVVM architecture spanning 40 component categories across Finance/Trading, Healthcare, Logistics, E-Commerce, AI/ML, and more.", + status: "In Progress", + imageUrl: "/Assets/Projects/Ark.Alliance.React.Component/components-hero.png", + repoUrl: "https://github.com/ArmandRicheletKleinberg/Ark.Alliance.React.Component.UI", + packageUrl: "https://www.npmjs.com/package/@ark-alliance/react-ui", + technologies: ["react", "typescript", "tailwind", "threejs", "docker"], + features: [ + { title: "MVVM Architecture Pattern", description: "Strict Model-View-ViewModel separation with Zod schema-based models", icon: "layer-group" }, + { title: "40 Component Categories", description: "Comprehensive component suite spanning Buttons, Cards, Gauges, Input variants", icon: "th" }, + { title: "Multi-Domain Support", description: "Components designed for Finance/Trading, Healthcare, Logistics, and more", icon: "industry" }, + { title: "Runtime Zod Validation", description: "All models use Zod 4 schemas for runtime type checking", icon: "check-circle" }, + { title: "Premium Visual Modes", description: "Four distinct visual modes: Normal, Neon, Minimal, and Glass", icon: "palette" }, + { title: "100% Test Coverage", description: "258 tests passing with Vitest and React Testing Library", icon: "vial" } + ] + }, + { + title: "Ark.Alliance.Trading.Providers.Lib", + description: "Production-ready TypeScript SDK unifying cryptocurrency trading across multiple exchanges (Binance Futures, Deribit) with a single, elegant API.", + status: "In Progress", + imageUrl: "/Assets/Projects/Ark.Alliance.Trading.Providers.Lib/trading-hero.png", + repoUrl: "https://github.com/ArmandRicheletKleinberg/Ark.Alliance.Trading.Providers.Lib", + packageUrl: "https://www.npmjs.com/package/ark-alliance-trading-providers-lib", + technologies: ["typescript", "nodejs", "docker", "binance", "rest"], + features: [ + { title: "Multi-Provider Abstraction", description: "Unified interface for Binance Futures and Deribit exchanges", icon: "plug" }, + { title: "Result Pattern Error Handling", description: "Type-safe Result pattern for functional error handling", icon: "check-circle" }, + { title: "Real-Time WebSocket Streams", description: "Low-latency market data via WebSocket connections", icon: "wifi" }, + { title: "Secure Authentication", description: "HMAC-SHA256 and Ed25519 signature generation", icon: "lock" }, + { title: "Comprehensive Testing", description: "70+ test scenarios with ReflectionTestEngine", icon: "vial" }, + { title: "Clean Architecture", description: "Domain-driven design with clear layer separation", icon: "sitemap" } + ] + } +]; + +/** + * Maps status string from seed data to ProjectStatus enum. + */ +function mapStatus(status: string): ProjectStatus { + switch (status.toLowerCase()) { + case 'featured': return ProjectStatus.IN_PROGRESS; + case 'in progress': return ProjectStatus.IN_PROGRESS; + case 'completed': return ProjectStatus.COMPLETED; + case 'maintenance': return ProjectStatus.MAINTENANCE; + case 'archived': return ProjectStatus.ARCHIVED; + default: return ProjectStatus.IN_PROGRESS; + } +} + +/** + * Converts raw seed data to ProjectDto format. + */ +function toProjectDto(raw: RawProjectData, index: number): ProjectDto { + return { + id: String(index + 1), + title: raw.title, + description: raw.description, + status: mapStatus(raw.status), + isFeatured: raw.isFeatured, + imageUrl: raw.imageUrl, + repoUrl: raw.repoUrl, + demoUrl: raw.demoUrl, + packageUrl: raw.packageUrl, + technologies: raw.technologies, + features: raw.features?.map((f, i) => ({ + id: `f${index + 1}-${i + 1}`, + title: f.title, + description: f.description, + icon: f.icon, + imageUrl: f.imageUrl + } as ProjectFeatureDto)) + }; +} + +/** + * Mock projects data derived from the backend seed file (projects.json). + * This is the SINGLE SOURCE OF TRUTH for test data and fallback. + * + * Includes both the original backend seed projects and portfolio showcase projects. + */ +export const MOCK_PROJECTS: ProjectDto[] = SEED_PROJECTS.map(toProjectDto); + +/** + * Get featured projects only. + */ +export const MOCK_FEATURED_PROJECTS: ProjectDto[] = MOCK_PROJECTS.filter(p => p.isFeatured); diff --git a/Ark.Alliance.StartupCms.Ai.Share/mocks/resume.mock.ts b/Ark.Alliance.StartupCms.Ai.Share/mocks/resume.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b82f854a832c15fe9e9ab8dfe8745dd22ab3567 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/mocks/resume.mock.ts @@ -0,0 +1,92 @@ +/** + * @fileoverview Resume Mock Data + * Mock data for Resume matching backend seed data. + * + * @author Armand Richelet-Kleinberg + */ + +import { ResumeDto } from '../dtos/resume.dto'; +import { SkillLevel } from '../enums/skill-level.enum'; +import { SEED_PROFILE, SEED_EXPERIENCES, SEED_SKILLS } from './seed-data'; + +/** + * Helper to parse period string to start date. + * E.g., "Dec 2022 - Present" -> "2022-12-01" + */ +function parseStartDate(period: string): string { + const monthMap: Record = { + 'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', + 'May': '05', 'Jun': '06', 'Jul': '07', 'Aug': '08', + 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12' + }; + const parts = period.split(' - ')[0].split(' '); + const month = monthMap[parts[0]] || '01'; + const year = parts[1] || '2020'; + return `${year}-${month}-01`; +} + +/** + * Helper to parse period string to end date. + * Returns undefined if "Present" + */ +function parseEndDate(period: string): string | undefined { + const monthMap: Record = { + 'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', + 'May': '05', 'Jun': '06', 'Jul': '07', 'Aug': '08', + 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12' + }; + const endPart = period.split(' - ')[1]; + if (!endPart || endPart === 'Present') return undefined; + const parts = endPart.split(' '); + const month = monthMap[parts[0]] || '01'; + const year = parts[1] || '2020'; + return `${year}-${month}-01`; +} + +/** + * Resume mock using centralized seed data. + * Matches: InitDbAsset/JsonDatas/profile.json, experience.json, skills.json + */ +export const RESUME_MOCK: ResumeDto = { + profile: { + firstName: SEED_PROFILE.firstName, + lastName: SEED_PROFILE.lastName, + title: SEED_PROFILE.title, + overview: SEED_PROFILE.overview, + email: SEED_PROFILE.email, + linkedinUrl: SEED_PROFILE.linkedinUrl, + githubUrl: SEED_PROFILE.githubUrl + }, + experiences: SEED_EXPERIENCES.map(exp => ({ + company: exp.company, + position: exp.role, + startDate: parseStartDate(exp.period), + endDate: parseEndDate(exp.period), + description: exp.desc, + technologies: exp.tech.split(', ') + })), + skills: [ + // Languages + ...SEED_SKILLS.languages.map(name => ({ name, category: 'Languages', level: SkillLevel.EXPERT })), + // Frameworks + ...SEED_SKILLS.frameworks.map(name => ({ name, category: 'Frameworks', level: SkillLevel.EXPERT })), + // Databases + ...SEED_SKILLS.databases.map(name => ({ name, category: 'Databases', level: SkillLevel.EXPERT })), + // Tools + ...SEED_SKILLS.tools.map(name => ({ name, category: 'Tools', level: SkillLevel.EXPERT })), + // Methodologies + ...SEED_SKILLS.methodologies.map(name => ({ name, category: 'Methodologies', level: SkillLevel.EXPERT })) + ], + education: [ + { + degree: "Master in Computer Science", + institution: "ULB (Université Libre de Bruxelles)", + startDate: "1999-09-01", + endDate: "2004-06-30", + fieldOfStudy: "Computer Science", + description: "Specialization in Algorithmic and Software Engineering." + } + ] +}; + +// Backwards compatibility if needed, but per request we avoid obsolete elements. diff --git a/Ark.Alliance.StartupCms.Ai.Share/mocks/seed-data.ts b/Ark.Alliance.StartupCms.Ai.Share/mocks/seed-data.ts new file mode 100644 index 0000000000000000000000000000000000000000..acfa27e5caf9fb307bfc2e881ca6c8d97d5662ce --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/mocks/seed-data.ts @@ -0,0 +1,388 @@ +/** + * @fileoverview Centralized Seed Data + * Single source of truth for mock/fallback data, matching backend JSON seed files. + * + * This data mirrors: + * - InitDbAsset/JsonDatas/profile.json + * - InitDbAsset/JsonDatas/skills.json + * - InitDbAsset/JsonDatas/technologies.json + * - InitDbAsset/JsonDatas/experience.json + * + * @author Armand Richelet-Kleinberg + */ + +// ============================================================================= +// PROFILE SEED DATA (from profile.json) +// ============================================================================= + +export const SEED_PROFILE = { + firstName: "Armand", + lastName: "Richelet-Kleinberg", + title: "AI Principal Solutions Architect - Full Stack Dev - Business & Technical Analyst", + overview: "Experienced AI Principal Architect with 20+ years in software development and system architecture, focused on the AI industry. Specialized in designing and delivering AI-driven solutions, MLOps pipelines, and cloud-native systems using C#, TypeScript (React), Js, Python. Proven ability to build scalable, event-driven architectures and integrate advanced AI frameworks. Expert in Agile/DevOps environments, translating complex AI business needs into robust, production-grade platforms.", + email: "arkleinberg@gmail.com", + githubUrl: "https://github.com/ArmandRicheletKleinberg", + linkedinUrl: "https://do.linkedin.com/in/arkleinberg/es", + avatarUrl: "/Assets/Site/Icon.png" +} as const; + +// ============================================================================= +// SKILLS SEED DATA (from skills.json) +// ============================================================================= + +export const SEED_SKILLS = { + languages: [ + "C#", + "Python", + "Java", + "TypeScript", + "JavaScript", + "C", + "C++", + "T-SQL", + "PL-SQL" + ], + frameworks: [ + ".NET up to 10", + "Entity Framework", + "Python Ecosystem", + "Unity", + "Adobe Suite", + "Cinema 4D" + ], + databases: [ + "SQL Server", + "SQLite", + "PostgreSQL", + "MongoDB", + "DB2", + "Oracle 9i", + "Sybase" + ], + tools: [ + "Docker", + "Kubernetes", + "Azure Services", + "GitHub", + "PyTorch", + "RabbitMQ", + "AWS Services", + "Unreal/Three.js" + ], + methodologies: [ + "DDD", + "CQRS", + "Event-Driven Architecture", + "Microservices", + "MLOps", + "Agile/Scrum", + "DevOps" + ] +} as const; + +// ============================================================================= +// EXPERIENCE SEED DATA (from experience.json) +// ============================================================================= + +export interface SeedExperience { + company: string; + project: string; + period: string; + role: string; + tech: string; + desc: string; +} + +export const SEED_EXPERIENCES: SeedExperience[] = [ + { + company: "M2H / AI and Technology", + project: "Ark.Alliance Ecosystem; Mindful AI Initiatives", + period: "Dec 2022 - Present", + role: "AI Lead & Principal Solution Architect", + tech: "C#, React, TypeScript, Python, Three.js, Docker, K8s, SLM, OpenAI, Anthropic, Mistral", + desc: "Pioneered mindful AI solutions and the Ark.Alliance Ecosystem. Architected prompt libraries and guidelines for consistent AI experiences. Led full-stack teams (C#, React, Python) in startup environments, implementing resilient patterns (Circuit Breaker, Bulkhead) and robust incident response processes. Built scalable, cloud-native systems from scratch using Agile/DevOps." + }, + { + company: "Ahold Delhaize / Logistics Supply Chain", + project: "New Logistics Systems Design & Optimization", + period: "Jan 2021 - Feb 2025", + role: "Solution & Software Architect", + tech: "C#, .NET 8, Blazor, Microservices, Azure, SAP, CQRS, GitHub, OpenAI API", + desc: "Architected and delivered critical logistics systems (TMS integration, delivery tracking). Led technical analysis and acceptance testing. Designed generic integration tools. Coached cross-functional and offshore teams to ensure high-quality delivery during cutover and HyperCare periods." + }, + { + company: "Candriam / Asset Management", + project: "Asset Position Integration System", + period: "Apr 2020 - Dec 2020", + role: "Software Architect & Full Stack Dev", + tech: "C#, .NET 5, Docker, MQSeries, DB2, Cobol, Microservices, SQL Server, CQRS", + desc: "Reverse-engineered legacy Mainframe systems to modern .NET microservices. Designed and rewrote the asset position integration system (shares, funds) for financial account managers. Executed a seamless migration with zero downtime during cutover." + }, + { + company: "Liberty Steel / Steel Industry", + project: "Web-Based Application Portfolio & SAP Integration", + period: "May 2019 - Apr 2020", + role: "Business Analyst & Full Stack Dev", + tech: "C#, .NET Core, TypeScript, Python, Azure, SAP ERP, Clean Architecture, Blazor", + desc: "Analyzed business flows and developed web-based microservices for steel production logistics. Migrated legacy services to WCF and rewrote production planning schedulers. Integrated deeply with SAP and Mainframe systems using Azure cloud services." + }, + { + company: "Delhaize Group / Retail Logistics", + project: "Logistics Systems Design & Cloud Transformation", + period: "Sep 2011 - Apr 2019", + role: "Full Stack Developer & IT Owner", + tech: "C#, .NET, Entity Framework, Azure, Java, SAP ERP, WMS, IoT, SQL Server/Oracle", + desc: "Owned IT delivery for global logistics reporting and supplier management apps. Modernized legacy systems with SAP and WMS integrations (dock scheduling, EDIFACT). Provided L3 support and implemented ITIL processes for resilient 24/7 logistics operations." + }, + { + company: "Spectacles Charles Kleinberg", + project: "Live Show Control Systems", + period: "Jan 2005 - Nov 2015", + role: "Solution Architect - Developer & Artist", + tech: "C#, Python, C++, DirectX, Unity, Three.js, Max/Msp, Maya, Real-time Systems", + desc: "Architected real-time show control systems synchronizing audio, MIDI, lighting, and 3D Stereoscopic visuals. Developed high-performance interactive solutions in Agile environments. Blended technical engineering with artistic direction for immersive live performances." + }, + { + company: "BNP Paribas Fortis / Banking", + project: "Voice over IP Systems", + period: "Nov 2010 - Feb 2011", + role: "ICT Consultant", + tech: ".NET (C#/VB), SQL Server, SOAP", + desc: "Consulted on the architecture and full-stack development of robust Voice over IP systems for banking infrastructure." + }, + { + company: "Mastercard / Financial Services", + project: "Credit Card Chip Validation", + period: "Nov 2004 - Feb 2005", + role: "Software Engineer", + tech: "C#, C, SQL Server, Assembly", + desc: "Developed critical testing and reporting systems for credit card chip compliance. Collaborated on feature launches to enhance global transaction security." + } +]; + +// ============================================================================= +// HOBBIES SEED DATA (from hobbies.json) +// ============================================================================= + +export interface SeedHobby { + name: string; + description: string; + icon: string; +} + +export const SEED_HOBBIES: SeedHobby[] = [ + { + name: "Music Production", + description: "Electronic music composition and production using DAWs, synthesizers, and audio engineering techniques.", + icon: "Music" + }, + { + name: "3D Graphics & Animation", + description: "Creating 3D models, animations, and visual effects using Cinema 4D, Maya, and Three.js.", + icon: "Box" + }, + { + name: "Gaming", + description: "Strategy games, simulation, and game development exploration.", + icon: "Gamepad2" + }, + { + name: "Photography", + description: "Digital photography with focus on architectural and landscape subjects.", + icon: "Camera" + }, + { + name: "AI Research", + description: "Exploring cutting-edge AI technologies, prompt engineering, and building AI-powered applications.", + icon: "Brain" + } +]; + +// ============================================================================= +// LANGUAGES SEED DATA (from languages.json) +// ============================================================================= + +export interface SeedLanguage { + language: string; + speaking: number; + writing: number; + presenting: number; +} + +export const SEED_LANGUAGES: SeedLanguage[] = [ + { language: "French", speaking: 5, writing: 5, presenting: 5 }, + { language: "English", speaking: 5, writing: 5, presenting: 5 }, + { language: "Spanish", speaking: 4, writing: 3, presenting: 3 }, + { language: "Dutch", speaking: 2, writing: 1, presenting: 1 } +]; + +// ============================================================================= +// EDUCATION SEED DATA (from education.json) +// ============================================================================= + +export interface SeedEducation { + degree: string; + institution: string; + fieldOfStudy: string; + startYear: number; + endYear: number; + description: string; +} + +export const SEED_EDUCATION: SeedEducation[] = [ + { + degree: "Master in Computer Science", + institution: "ULB (Université Libre de Bruxelles)", + fieldOfStudy: "Computer Science - Algorithmic and Software Engineering", + startYear: 1999, + endYear: 2004, + description: "Specialization in Algorithmic and Software Engineering. Comprehensive curriculum covering data structures, algorithms, software design patterns, database systems, and artificial intelligence fundamentals." + } +]; + +// ============================================================================= +// BUSINESS DOMAINS SEED DATA (from business-domains.json) +// ============================================================================= + +export interface SeedBusinessDomain { + domain: string; + level: string; + yearsOfExperience: number; + description: string; + icon: string; +} + +export const SEED_BUSINESS_DOMAINS: SeedBusinessDomain[] = [ + { + domain: "Logistics", + level: "Expert", + yearsOfExperience: 12, + description: "Supply chain management, warehouse systems, TMS integration, delivery tracking, and last-mile logistics optimization.", + icon: "Truck" + }, + { + domain: "Finance", + level: "Expert", + yearsOfExperience: 8, + description: "Asset management, portfolio systems, trading platforms, regulatory compliance, and financial data integration.", + icon: "TrendingUp" + }, + { + domain: "Trading", + level: "Expert", + yearsOfExperience: 5, + description: "Algorithmic trading systems, market data processing, order management, and cryptocurrency platforms.", + icon: "LineChart" + }, + { + domain: "Banking", + level: "Advanced", + yearsOfExperience: 3, + description: "Core banking systems, payment processing, VoIP infrastructure for banking operations.", + icon: "Building" + }, + { + domain: "Asset Management", + level: "Expert", + yearsOfExperience: 4, + description: "Investment portfolio systems, fund management, asset position tracking, and regulatory reporting.", + icon: "Briefcase" + }, + { + domain: "Steel Manufacturing", + level: "Advanced", + yearsOfExperience: 2, + description: "Production planning, SAP ERP integration, supply chain for steel industry, and manufacturing logistics.", + icon: "Factory" + }, + { + domain: "Retail", + level: "Expert", + yearsOfExperience: 10, + description: "E-commerce platforms, inventory management, supplier systems, and omnichannel retail solutions.", + icon: "ShoppingCart" + }, + { + domain: "Entertainment", + level: "Expert", + yearsOfExperience: 10, + description: "Live show control systems, real-time audio/visual synchronization, and interactive performance technology.", + icon: "Clapperboard" + }, + { + domain: "Theatre", + level: "Expert", + yearsOfExperience: 10, + description: "Stage automation, lighting control systems, MIDI integration, and 3D stereoscopic visual effects for live performances.", + icon: "Drama" + }, + { + domain: "Music Composing", + level: "Advanced", + yearsOfExperience: 15, + description: "Original music composition for performances, electronic music production, and sound design.", + icon: "Music2" + } +]; + +// ============================================================================= +// TECHNOLOGY CATEGORIES SEED DATA (from technologies.json - categories only) +// ============================================================================= + +export interface SeedTechnologyCategory { + id: string; + name: string; + description: string; + order: number; +} + +export const SEED_TECHNOLOGY_CATEGORIES: SeedTechnologyCategory[] = [ + { id: "frontend", name: "Frontend Frameworks & Libraries", description: "Client-side web development technologies", order: 1 }, + { id: "languages", name: "Programming Languages", description: "Core programming languages", order: 2 }, + { id: "runtimes", name: "Runtimes & Platforms", description: "Runtime environments and development platforms", order: 3 }, + { id: "backend", name: "Backend Frameworks", description: "Server-side frameworks and libraries", order: 4 }, + { id: "databases", name: "Databases", description: "Relational and NoSQL database systems", order: 5 }, + { id: "cloud", name: "Cloud Platforms", description: "Cloud computing providers and services", order: 6 }, + { id: "devops", name: "DevOps & Infrastructure", description: "CI/CD, containerization, and infrastructure tools", order: 7 }, + { id: "messaging", name: "Message Brokers & Queues", description: "Event streaming and message queue systems", order: 8 }, + { id: "ai", name: "AI/ML & Data Science", description: "Artificial intelligence and machine learning technologies", order: 9 }, + { id: "enterprise", name: "Enterprise Systems", description: "ERP, CRM, and enterprise software platforms", order: 10 }, + { id: "patterns", name: "Architecture Patterns", description: "Architectural patterns and API specifications", order: 11 }, + { id: "apis", name: "External APIs & Services", description: "Third-party APIs and integration services", order: 12 }, + { id: "testing", name: "Testing & Quality", description: "Testing frameworks and quality assurance tools", order: 13 }, + { id: "mobile", name: "Mobile Development", description: "Mobile app development frameworks", order: 14 }, + { id: "styling", name: "Styling & Design", description: "CSS frameworks and design tools", order: 15 } +]; + +// ============================================================================= +// KEY TECHNOLOGIES SEED DATA (subset from technologies.json) +// Note: This is a representative subset. Full tech list is too large for mock data. +// ============================================================================= + +export interface SeedTechnology { + key: string; + name: string; + label: string; + category: string; + description: string; + icon: string; + color: string; + website?: string; + versions?: string[]; +} + +export const SEED_TECHNOLOGIES: SeedTechnology[] = [ + { key: "react", name: "React", label: "React.js", category: "frontend", description: "A JavaScript library for building user interfaces with component-based architecture", icon: "fab fa-react", color: "#61DAFB", website: "https://react.dev", versions: ["16", "17", "18", "19"] }, + { key: "typescript", name: "TypeScript", label: "TypeScript", category: "languages", description: "A strongly typed programming language that builds on JavaScript", icon: "devicon-typescript-plain", color: "#3178C6", website: "https://www.typescriptlang.org" }, + { key: "csharp", name: "C#", label: "C#", category: "languages", description: "A modern, object-oriented programming language developed by Microsoft", icon: "devicon-csharp-plain", color: "#239120", website: "https://docs.microsoft.com/en-us/dotnet/csharp/" }, + { key: "python", name: "Python", label: "Python", category: "languages", description: "A versatile programming language known for its readability and extensive libraries", icon: "fab fa-python", color: "#3776AB", website: "https://www.python.org" }, + { key: "nodejs", name: "Node.js", label: "Node.js", category: "runtimes", description: "A JavaScript runtime built on Chrome's V8 engine for server-side development", icon: "fab fa-node-js", color: "#339933", website: "https://nodejs.org" }, + { key: "dotnet", name: ".NET", label: ".NET", category: "runtimes", description: "A free, cross-platform, open-source developer platform by Microsoft", icon: "devicon-dotnetcore-plain", color: "#512BD4", website: "https://dotnet.microsoft.com", versions: ["5", "6", "7", "8", "9", "10"] }, + { key: "docker", name: "Docker", label: "Docker", category: "devops", description: "A platform for developing, shipping, and running applications in containers", icon: "fab fa-docker", color: "#2496ED", website: "https://www.docker.com" }, + { key: "kubernetes", name: "Kubernetes", label: "Kubernetes (K8s)", category: "devops", description: "An open-source system for automating deployment and management of containerized applications", icon: "devicon-kubernetes-plain", color: "#326CE5", website: "https://kubernetes.io" }, + { key: "azure", name: "Azure", label: "Microsoft Azure", category: "cloud", description: "Microsoft's cloud computing platform for building, testing, and managing applications", icon: "devicon-azure-plain", color: "#0078D4", website: "https://azure.microsoft.com" }, + { key: "postgresql", name: "PostgreSQL", label: "PostgreSQL", category: "databases", description: "A powerful, open-source object-relational database system", icon: "devicon-postgresql-plain", color: "#4169E1", website: "https://www.postgresql.org" }, + { key: "mongodb", name: "MongoDB", label: "MongoDB", category: "databases", description: "A document-based NoSQL database for modern applications", icon: "devicon-mongodb-plain", color: "#47A248", website: "https://www.mongodb.com" }, + { key: "openai", name: "OpenAI", label: "OpenAI API", category: "ai", description: "AI models and APIs including GPT-4, DALL-E, and Whisper", icon: "fas fa-robot", color: "#412991", website: "https://openai.com" }, + { key: "pytorch", name: "PyTorch", label: "PyTorch", category: "ai", description: "An open-source machine learning framework based on the Torch library", icon: "devicon-pytorch-original", color: "#EE4C2C", website: "https://pytorch.org" } +]; diff --git a/Ark.Alliance.StartupCms.Ai.Share/mocks/technology.mock.ts b/Ark.Alliance.StartupCms.Ai.Share/mocks/technology.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..1babf84e1096bb4c5768a82423b16db833b5de16 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/mocks/technology.mock.ts @@ -0,0 +1,147 @@ +/** + * @fileoverview Technology Mock Data + * Mock data for technologies and categories matching backend seed data. + * Used as fallback when backend/database is unavailable. + * + * @author Armand Richelet-Kleinberg + */ + +import { TechnologyDto } from '../dtos/technology.dto'; + +// ============================================================================= +// TECHNOLOGY CATEGORY MOCK (simplified without nested technologies) +// ============================================================================= + +/** + * Simple category info for UI display (without nested technologies array). + */ +export interface MockTechnologyCategory { + id: string; + name: string; + description: string; + order: number; +} + +export const MOCK_TECHNOLOGY_CATEGORIES: MockTechnologyCategory[] = [ + { id: 'frontend', name: 'Frontend Frameworks & Libraries', description: 'Client-side web development technologies', order: 1 }, + { id: 'languages', name: 'Programming Languages', description: 'Core programming languages', order: 2 }, + { id: 'runtimes', name: 'Runtimes & Platforms', description: 'Runtime environments and development platforms', order: 3 }, + { id: 'backend', name: 'Backend Frameworks', description: 'Server-side frameworks and libraries', order: 4 }, + { id: 'databases', name: 'Databases', description: 'Relational and NoSQL database systems', order: 5 }, + { id: 'cloud', name: 'Cloud Platforms', description: 'Cloud computing providers and services', order: 6 }, + { id: 'devops', name: 'DevOps & Infrastructure', description: 'CI/CD, containerization, and infrastructure tools', order: 7 }, + { id: 'messaging', name: 'Message Brokers & Queues', description: 'Event streaming and message queue systems', order: 8 }, + { id: 'ai', name: 'AI/ML & Data Science', description: 'Artificial intelligence and machine learning technologies', order: 9 }, + { id: 'enterprise', name: 'Enterprise Systems', description: 'ERP, CRM, and enterprise software platforms', order: 10 }, + { id: 'patterns', name: 'Architecture Patterns', description: 'Architectural patterns and API specifications', order: 11 }, + { id: 'apis', name: 'External APIs & Services', description: 'Third-party APIs and integration services', order: 12 }, + { id: 'testing', name: 'Testing & Quality', description: 'Testing frameworks and quality assurance tools', order: 13 }, + { id: 'mobile', name: 'Mobile Development', description: 'Mobile app development frameworks', order: 14 }, + { id: 'styling', name: 'Styling & Design', description: 'CSS frameworks and design tools', order: 15 } +]; + +// ============================================================================= +// CORE TECHNOLOGIES (subset for fallback - matching projects) +// ============================================================================= + +export const MOCK_TECHNOLOGIES: TechnologyDto[] = [ + // Frontend + { key: 'react', name: 'React', label: 'React.js', category: 'frontend', description: 'A JavaScript library for building user interfaces with component-based architecture', icon: 'fab fa-react', color: '#61DAFB', website: 'https://react.dev', versions: ['16', '17', '18', '19'] }, + { key: 'angular', name: 'Angular', label: 'Angular', category: 'frontend', description: 'A TypeScript-based web application framework led by Google', icon: 'fab fa-angular', color: '#DD0031', website: 'https://angular.io' }, + { key: 'vue', name: 'Vue.js', label: 'Vue.js', category: 'frontend', description: 'A progressive JavaScript framework for building user interfaces', icon: 'fab fa-vuejs', color: '#4FC08D', website: 'https://vuejs.org' }, + { key: 'threejs', name: 'Three.js', label: 'Three.js', category: 'frontend', description: 'A cross-browser JavaScript library for creating 3D graphics in web browsers', icon: 'devicon-threejs-original', color: '#000000', website: 'https://threejs.org' }, + { key: 'blazor', name: 'Blazor', label: 'Blazor', category: 'frontend', description: 'A framework for building interactive web UIs using C# instead of JavaScript', icon: 'devicon-dotnetcore-plain', color: '#512BD4', website: 'https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor' }, + + // Languages + { key: 'typescript', name: 'TypeScript', label: 'TypeScript', category: 'languages', description: 'A strongly typed programming language that builds on JavaScript', icon: 'devicon-typescript-plain', color: '#3178C6', website: 'https://www.typescriptlang.org' }, + { key: 'javascript', name: 'JavaScript', label: 'JavaScript', category: 'languages', description: 'A dynamic programming language that powers the web', icon: 'fab fa-js-square', color: '#F7DF1E', website: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript' }, + { key: 'python', name: 'Python', label: 'Python', category: 'languages', description: 'A versatile programming language known for its readability and extensive libraries', icon: 'fab fa-python', color: '#3776AB', website: 'https://www.python.org' }, + { key: 'csharp', name: 'C#', label: 'C#', category: 'languages', description: 'A modern, object-oriented programming language developed by Microsoft', icon: 'devicon-csharp-plain', color: '#239120', website: 'https://docs.microsoft.com/en-us/dotnet/csharp/' }, + { key: 'cpp', name: 'C++', label: 'C++', category: 'languages', description: 'A high-performance programming language for systems and applications', icon: 'devicon-cplusplus-plain', color: '#00599C', website: 'https://isocpp.org' }, + { key: 'java', name: 'Java', label: 'Java', category: 'languages', description: 'A class-based, object-oriented programming language for enterprise applications', icon: 'fab fa-java', color: '#007396', website: 'https://www.java.com' }, + { key: 'cobol', name: 'COBOL', label: 'COBOL', category: 'languages', description: 'A business-oriented programming language used in mainframe systems', icon: 'fas fa-code', color: '#005CA5', website: 'https://www.ibm.com/products/cobol-compiler-zos' }, + + // Runtimes + { key: 'nodejs', name: 'Node.js', label: 'Node.js', category: 'runtimes', description: 'A JavaScript runtime built on Chrome\'s V8 engine for server-side development', icon: 'fab fa-node-js', color: '#339933', website: 'https://nodejs.org' }, + { key: 'dotnet', name: '.NET', label: '.NET', category: 'runtimes', description: 'A free, cross-platform, open-source developer platform by Microsoft', icon: 'devicon-dotnetcore-plain', color: '#512BD4', website: 'https://dotnet.microsoft.com', versions: ['5', '6', '7', '8', '9', '10'] }, + { key: 'unity', name: 'Unity', label: 'Unity Engine', category: 'runtimes', description: 'A cross-platform game engine for 2D/3D games and interactive experiences', icon: 'devicon-unity-original', color: '#000000', website: 'https://unity.com' }, + + // Backend + { key: 'express', name: 'Express.js', label: 'Express.js', category: 'backend', description: 'A fast, unopinionated, minimalist web framework for Node.js', icon: 'devicon-express-original', color: '#000000', website: 'https://expressjs.com' }, + { key: 'nestjs', name: 'NestJS', label: 'NestJS', category: 'backend', description: 'A progressive Node.js framework for building efficient server-side applications', icon: 'devicon-nestjs-plain', color: '#E0234E', website: 'https://nestjs.com' }, + { key: 'fastapi', name: 'FastAPI', label: 'FastAPI', category: 'backend', description: 'A modern, fast Python web framework for building APIs with automatic OpenAPI docs', icon: 'devicon-fastapi-plain', color: '#009688', website: 'https://fastapi.tiangolo.com' }, + { key: 'aspnetcore', name: 'ASP.NET Core', label: 'ASP.NET Core', category: 'backend', description: 'A cross-platform, high-performance framework for building modern web apps', icon: 'devicon-dotnetcore-plain', color: '#512BD4', website: 'https://docs.microsoft.com/en-us/aspnet/core' }, + + // Databases + { key: 'sqlite', name: 'SQLite', label: 'SQLite', category: 'databases', description: 'A lightweight, serverless, self-contained SQL database engine', icon: 'devicon-sqlite-plain', color: '#003B57', website: 'https://www.sqlite.org' }, + { key: 'postgresql', name: 'PostgreSQL', label: 'PostgreSQL', category: 'databases', description: 'A powerful, open-source object-relational database system', icon: 'devicon-postgresql-plain', color: '#4169E1', website: 'https://www.postgresql.org' }, + { key: 'mongodb', name: 'MongoDB', label: 'MongoDB', category: 'databases', description: 'A document-based NoSQL database for modern applications', icon: 'devicon-mongodb-plain', color: '#47A248', website: 'https://www.mongodb.com' }, + { key: 'redis', name: 'Redis', label: 'Redis', category: 'databases', description: 'An in-memory data structure store used as database, cache, and message broker', icon: 'devicon-redis-plain', color: '#DC382D', website: 'https://redis.io' }, + { key: 'sqlserver', name: 'SQL Server', label: 'SQL Server', category: 'databases', description: 'Microsoft\'s enterprise-grade relational database management system', icon: 'devicon-microsoftsqlserver-plain', color: '#CC2927', website: 'https://www.microsoft.com/sql-server' }, + + // Cloud + { key: 'aws', name: 'AWS', label: 'Amazon Web Services', category: 'cloud', description: 'The world\'s leading cloud computing platform by Amazon', icon: 'fab fa-aws', color: '#FF9900', website: 'https://aws.amazon.com' }, + { key: 'azure', name: 'Azure', label: 'Microsoft Azure', category: 'cloud', description: 'Microsoft\'s cloud computing platform for building, testing, and managing applications', icon: 'devicon-azure-plain', color: '#0078D4', website: 'https://azure.microsoft.com' }, + { key: 'gcp', name: 'Google Cloud', label: 'Google Cloud Platform', category: 'cloud', description: 'Google\'s suite of cloud computing services', icon: 'devicon-googlecloud-plain', color: '#4285F4', website: 'https://cloud.google.com' }, + + // DevOps + { key: 'docker', name: 'Docker', label: 'Docker', category: 'devops', description: 'A platform for developing, shipping, and running applications in containers', icon: 'fab fa-docker', color: '#2496ED', website: 'https://www.docker.com' }, + { key: 'kubernetes', name: 'Kubernetes', label: 'Kubernetes (K8s)', category: 'devops', description: 'An open-source system for automating deployment and management of containerized applications', icon: 'devicon-kubernetes-plain', color: '#326CE5', website: 'https://kubernetes.io' }, + { key: 'github-actions', name: 'GitHub Actions', label: 'GitHub Actions', category: 'devops', description: 'CI/CD platform integrated with GitHub for automating workflows', icon: 'fab fa-github', color: '#2088FF', website: 'https://github.com/features/actions' }, + + // Messaging + { key: 'rabbitmq', name: 'RabbitMQ', label: 'RabbitMQ', category: 'messaging', description: 'An open-source message broker implementing AMQP protocol', icon: 'devicon-rabbitmq-original', color: '#FF6600', website: 'https://www.rabbitmq.com' }, + { key: 'kafka', name: 'Kafka', label: 'Apache Kafka', category: 'messaging', description: 'A distributed event streaming platform for high-throughput data pipelines', icon: 'devicon-apachekafka-original', color: '#231F20', website: 'https://kafka.apache.org' }, + { key: 'mqseries', name: 'MQSeries', label: 'IBM MQ', category: 'messaging', description: 'IBM\'s enterprise message-oriented middleware for reliable messaging', icon: 'fas fa-exchange-alt', color: '#052FAD', website: 'https://www.ibm.com/products/mq' }, + + // AI + { key: 'openai', name: 'OpenAI', label: 'OpenAI API', category: 'ai', description: 'AI models and APIs including GPT-4, DALL-E, and Whisper', icon: 'fas fa-robot', color: '#412991', website: 'https://openai.com' }, + { key: 'anthropic', name: 'Anthropic', label: 'Claude API', category: 'ai', description: 'Anthropic\'s Claude AI assistant and API for safe, helpful AI', icon: 'fas fa-brain', color: '#CC9B7A', website: 'https://www.anthropic.com' }, + { key: 'gemini', name: 'Gemini', label: 'Google Gemini', category: 'ai', description: 'Google\'s multimodal AI model for text, images, and code', icon: 'fas fa-gem', color: '#4285F4', website: 'https://deepmind.google/technologies/gemini' }, + { key: 'pytorch', name: 'PyTorch', label: 'PyTorch', category: 'ai', description: 'An open-source machine learning framework based on the Torch library', icon: 'devicon-pytorch-original', color: '#EE4C2C', website: 'https://pytorch.org' }, + + // Enterprise + { key: 'sap', name: 'SAP', label: 'SAP ERP', category: 'enterprise', description: 'Enterprise resource planning software for business operations', icon: 'fas fa-building', color: '#0FAAFF', website: 'https://www.sap.com' }, + { key: 'salesforce', name: 'Salesforce', label: 'Salesforce', category: 'enterprise', description: 'The world\'s leading customer relationship management (CRM) platform', icon: 'devicon-salesforce-plain', color: '#00A1E0', website: 'https://www.salesforce.com' }, + + // Patterns + { key: 'rest', name: 'REST', label: 'RESTful API', category: 'patterns', description: 'Representational State Transfer - an architectural style for distributed systems', icon: 'fas fa-exchange-alt', color: '#4CAF50' }, + { key: 'graphql', name: 'GraphQL', label: 'GraphQL', category: 'patterns', description: 'A query language for APIs and a runtime for fulfilling those queries', icon: 'devicon-graphql-plain', color: '#E10098', website: 'https://graphql.org' }, + { key: 'microservices', name: 'Microservices', label: 'Microservices Architecture', category: 'patterns', description: 'An architectural style that structures an application as loosely coupled services', icon: 'fas fa-cubes', color: '#6C5CE7' }, + { key: 'cqrs', name: 'CQRS', label: 'CQRS', category: 'patterns', description: 'Command Query Responsibility Segregation - separating read and write operations', icon: 'fas fa-exchange-alt', color: '#00B894' }, + + // APIs + { key: 'binance', name: 'Binance', label: 'Binance API', category: 'apis', description: 'APIs for the world\'s largest cryptocurrency exchange', icon: 'fas fa-coins', color: '#F0B90B', website: 'https://www.binance.com' }, + + // Styling + { key: 'tailwind', name: 'Tailwind CSS', label: 'Tailwind CSS', category: 'styling', description: 'A utility-first CSS framework for rapid UI development', icon: 'devicon-tailwindcss-plain', color: '#06B6D4', website: 'https://tailwindcss.com' } +]; + +/** + * Get technology by key. + * Used to lookup technology details for TechBadge and detail modals. + */ +export function getTechnologyByKey(key: string): TechnologyDto | undefined { + return MOCK_TECHNOLOGIES.find(t => t.key.toLowerCase() === key.toLowerCase()); +} + +/** + * Get technologies by category. + */ +export function getTechnologiesByCategory(category: string): TechnologyDto[] { + return MOCK_TECHNOLOGIES.filter(t => t.category === category); +} + +/** + * Get all technologies grouped by category. + */ +export function getMockTechnologiesGroupedByCategory(): Record { + const grouped: Record = {}; + for (const tech of MOCK_TECHNOLOGIES) { + if (!grouped[tech.category]) { + grouped[tech.category] = []; + } + grouped[tech.category].push(tech); + } + return grouped; +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/package-lock.json b/Ark.Alliance.StartupCms.Ai.Share/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..0e621c2b7ffc66feeeaf797e6ed666047a7f4c3c --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/package-lock.json @@ -0,0 +1,563 @@ +{ + "name": "@arkalliance/startupcms-ai-share", + "version": "1.0.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@arkalliance/startupcms-ai-share", + "version": "1.0.2", + "license": "MIT", + "devDependencies": { + "rimraf": "^5.0.0", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/Ark.Alliance.StartupCms.Ai.Share/package.json b/Ark.Alliance.StartupCms.Ai.Share/package.json new file mode 100644 index 0000000000000000000000000000000000000000..6a5ad2d45a70648b6d166aba6096bf5edac9cb87 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/package.json @@ -0,0 +1,72 @@ +{ + "name": "@arkalliance/startupcms-ai-share", + "version": "1.0.3", + "description": "Shared DTOs, Enums, and Type definitions for Ark.Alliance.Portfolio ecosystem", + "author": "Armand Richelet-Kleinberg ", + "license": "MIT", + "homepage": "https://github.com/ark-alliance/ark-portfolio#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ark-alliance/ark-portfolio.git", + "directory": "Ark.Portfolio.Share" + }, + "bugs": { + "url": "https://github.com/ark-alliance/ark-portfolio/issues" + }, + "keywords": [ + "ark-alliance", + "portfolio", + "dto", + "typescript", + "shared", + "types" + ], + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.js" + }, + "./dtos/*": { + "types": "./dist/dtos/*.d.ts", + "import": "./dist/dtos/*.js" + }, + "./enums/*": { + "types": "./dist/enums/*.d.ts", + "import": "./dist/enums/*.js" + }, + "./mocks/*": { + "types": "./dist/mocks/*.d.ts", + "import": "./dist/mocks/*.js" + }, + "./constants/*": { + "types": "./dist/constants/*.d.ts", + "import": "./dist/constants/*.js" + } + }, + "files": [ + "dist/**/*", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "tsc", + "clean": "rimraf dist", + "prepublishOnly": "npm run clean && npm run build", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "rimraf": "^5.0.0", + "typescript": "^5.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.Share/rename-cv-to-resume.ps1 b/Ark.Alliance.StartupCms.Ai.Share/rename-cv-to-resume.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..883f44c7e8966a0f4ccf8c3b3d5529c9b47c2e6d --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/rename-cv-to-resume.ps1 @@ -0,0 +1,15 @@ +# PowerShell script to rename CV to Resume in resume.dto.ts + +$filePath = "c:/Users/Criprtoswiss/source/repos/Ark.Portfolio/Ark.Portfolio.Share/dtos/resume.dto.ts" +$content = Get-Content $filePath -Raw + +# Replace all variations of Cv with Resume +$content = $content -replace "AdminCv", "AdminResume" +$content = $content -replace "CvDto", "ResumeDto" +$content = $content -replace "CV/Resume", "Resume/CV" +$content = $content -replace " CV ", " Resume " +$content = $content -replace "CV DTOs", "Resume DTOs" + +Set-Content -Path $filePath -Value $content -NoNewline + +Write-Host "Renamed CV references to Resume in resume.dto.ts" diff --git a/Ark.Alliance.StartupCms.Ai.Share/tsconfig.json b/Ark.Alliance.StartupCms.Ai.Share/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..0d99df4486be2e216a4101bb3f32247cc67e8ed5 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.Share/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "declaration": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "./**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.UI/.env b/Ark.Alliance.StartupCms.Ai.UI/.env new file mode 100644 index 0000000000000000000000000000000000000000..9eda5d60479f76b2a8744f2c253d199887df2304 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/.env @@ -0,0 +1,5 @@ +# API Configuration +VITE_API_BASE_URL=/api + +# Disable mock data fallback - use real backend +VITE_USE_MOCKS=false diff --git a/Ark.Alliance.StartupCms.Ai.UI/.env.example b/Ark.Alliance.StartupCms.Ai.UI/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..3c461abdb37c3a555f8af1d3fdd60d789b048b43 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/.env.example @@ -0,0 +1,29 @@ +# ═══════════════════════════════════════════════════════════════════════ +# Ark.Portfolio UI - Environment Configuration +# ═══════════════════════════════════════════════════════════════════════ +# Copy this file to .env and customize as needed + +# ───────────────────────────────────────────────────────────────────────── +# API Configuration +# ───────────────────────────────────────────────────────────────────────── +# Backend API URL (default: http://localhost:3085/api) +# For HTTPS: VITE_API_URL=https://localhost:3085/api +VITE_API_URL=http://localhost:3085/api + +# Use mock data instead of real API (default: false) +VITE_USE_MOCK_DATA=false + +# ───────────────────────────────────────────────────────────────────────── +# Feature Flags +# ───────────────────────────────────────────────────────────────────────── +# Enable AI features in admin dashboard +VITE_ENABLE_AI_FEATURES=true + +# Enable static site export +VITE_ENABLE_STATIC_EXPORT=true + +# ───────────────────────────────────────────────────────────────────────── +# Theme Configuration +# ───────────────────────────────────────────────────────────────────────── +# Default theme: architectural | aloe-vera +VITE_DEFAULT_THEME=architectural diff --git a/Ark.Alliance.StartupCms.Ai.UI/.env.local.template b/Ark.Alliance.StartupCms.Ai.UI/.env.local.template new file mode 100644 index 0000000000000000000000000000000000000000..dad5ffbd7b06b568ad06a9b9a10a2543e55abaac --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/.env.local.template @@ -0,0 +1,9 @@ +# Environment Variables for Next.js UI + +# API Configuration +NEXT_PUBLIC_API_URL=http://localhost:3085/api +NEXT_PUBLIC_SITE_URL=http://localhost:3000 + +# Site Configuration (for SEO) +NEXT_PUBLIC_SITE_NAME=Ark Alliance CMS +NEXT_PUBLIC_COMPANY_NAME=Ark Alliance diff --git a/Ark.Alliance.StartupCms.Ai.UI/.gitignore b/Ark.Alliance.StartupCms.Ai.UI/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..4141209d82936b9889827d1582a886cabff3e4b0 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/.gitignore @@ -0,0 +1,48 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build +/dist + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# vite (legacy) +dist-ssr +*.local + +# IDE +.idea +.vscode +*.swp +*.swo +*~ diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot10.PNG b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot10.PNG new file mode 100644 index 0000000000000000000000000000000000000000..545e15c4e95512074d58c91907db7d9c32986997 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot10.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9975e1830cb087bc177751ac162554ebe09ea1ee02dc881b0499954cc6fa13bc +size 321808 diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot11.PNG b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot11.PNG new file mode 100644 index 0000000000000000000000000000000000000000..2ec8207c17cd6559d1c86786ecd1e013a71d9142 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot11.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39a5c84b9a23784f6ee68772b3292945d557d4681538e1bbe762a50fbaf49747 +size 378429 diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot12.PNG b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot12.PNG new file mode 100644 index 0000000000000000000000000000000000000000..2ff7f228e3e9359e8a81847e0877c389b4acb3e0 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot12.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:459bb1c112c7a10eb22eb9c9a0d132c4badeaf78e23319e439c119bdc787f376 +size 155251 diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot13.PNG b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot13.PNG new file mode 100644 index 0000000000000000000000000000000000000000..068b233c1fca51af8aaceef21e3a1862529f0c43 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot13.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:614315756f28c729ae556a3b284460a0d7f63e8e9e3f00f10d752f9dcd840127 +size 170264 diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot14.PNG b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot14.PNG new file mode 100644 index 0000000000000000000000000000000000000000..0cf2808307669682bfced7468a04ef85b9ae622a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot14.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ac92fa503174a5c4c6685c731a06b77dfe9aa7ff094f8ab3034d68fc8a0c0e1 +size 459307 diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot2.PNG b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot2.PNG new file mode 100644 index 0000000000000000000000000000000000000000..75fc7e8910a064c50afad413ef8c27bcbf56ae99 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot2.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c25f04b3a256ba08022b0a14e7f3fb8d4c8d5fdb68ab8e5e9390cee46dc753a +size 465243 diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot3.PNG b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot3.PNG new file mode 100644 index 0000000000000000000000000000000000000000..a5c10232d8c105d6396bc1d03f9a7cecc61dc804 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot3.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b7e290bd1e3c51a836ab9ab613c594a1aa57315dc9a76f961899692e675ed3a +size 397617 diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot4.PNG b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot4.PNG new file mode 100644 index 0000000000000000000000000000000000000000..6c845c86ac723bbb1e00fc05a65f4c9ed3273a98 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot4.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f322678e0f5a499352eccd3a6a89e0190274fc19ab03ab08ed36c2d8e0731675 +size 213861 diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot5.PNG b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot5.PNG new file mode 100644 index 0000000000000000000000000000000000000000..d05306ce7a2306af63c75d852f8deedf9544320f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot5.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a74f2a561083dc5041332b70f1ef267ebb3ba2b2fbf72767a84ff7b7e7dec4c +size 215587 diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot6.PNG b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot6.PNG new file mode 100644 index 0000000000000000000000000000000000000000..4207ff17a1c808bb56db7d3eb524d98fdd33738e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot6.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66435ea297e9f7466e72a3cd1f4671e1e4da1f914521786a3cdcd4987624aad6 +size 505004 diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot7.PNG b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot7.PNG new file mode 100644 index 0000000000000000000000000000000000000000..fc843ea6466d799b3b000fdf29411bf71a00e75a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot7.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2cea73cf7bf2a1e4dedadad32fc30e2b6720ed513cb0b3c2ee0679aaea46d5a +size 369687 diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot8.PNG b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot8.PNG new file mode 100644 index 0000000000000000000000000000000000000000..3d7c3f49fa5ae9495584308cf7b4eb1376640a9a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Bot8.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b554f447aa539e11f277200d0979cd6409c6f49bbb09968d043efce36b6ac12 +size 271418 diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Capture.PNG b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Capture.PNG new file mode 100644 index 0000000000000000000000000000000000000000..fc843ea6466d799b3b000fdf29411bf71a00e75a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/Capture.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2cea73cf7bf2a1e4dedadad32fc30e2b6720ed513cb0b3c2ee0679aaea46d5a +size 369687 diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/bot1.PNG b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/bot1.PNG new file mode 100644 index 0000000000000000000000000000000000000000..d44cf89a01e264a1cdb87affc4ac800d3606fc40 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/Ark.Alliance.Trading.Bot/bot1.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcc256615a841b8686d03fa6159e33d37b1a40ccd0365288c946dd9740f0b2de +size 258622 diff --git a/Ark.Alliance.StartupCms.Ai.UI/Assets/icon/LogoArkAlliance.png b/Ark.Alliance.StartupCms.Ai.UI/Assets/icon/LogoArkAlliance.png new file mode 100644 index 0000000000000000000000000000000000000000..4b4ab3a60151b93269082263c697f9315917ac6f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/Assets/icon/LogoArkAlliance.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8f10a10d57e29ca9dcf71ae9b4dc0023d43ae7e96704364cac29c6aec20afd3 +size 220747 diff --git a/Ark.Alliance.StartupCms.Ai.UI/README.md b/Ark.Alliance.StartupCms.Ai.UI/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6890a9b95ac4036fd99c7da8c18d3ab32abfd5d4 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/README.md @@ -0,0 +1,302 @@ +# Ark.Alliance.StartupCms.AI.UI + +
+ +![Vite](https://img.shields.io/badge/Vite-5.0-646CFF?style=for-the-badge&logo=vite) +![React](https://img.shields.io/badge/React-18-61DAFB?style=for-the-badge&logo=react) +![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue?style=for-the-badge&logo=typescript) +![MVVM](https://img.shields.io/badge/Architecture-MVVM-green?style=for-the-badge) + +**Modern React Frontend for Ark.Alliance.StartupCms.AI** + +*MVVM Architecture • Dynamic Theming • Team Organigram • Resume System • Admin CMS* + +
+ +--- + +The **Frontend** is a modern React application built with Vite and TypeScript. It features a polished, responsive user interface with MVVM architecture, hierarchical team visualization, comprehensive resume pages, and a complete admin dashboard with CMS capabilities. + +## 📦 Functional Capabilities + +The frontend delivers a premium, human-centric experience with deep administrative control. + +| Domain | Capability | Description | Code Reference | +| :--- | :--- | :--- | :--- | +| **Team** | **Organization Chart** | Interactive hierarchical team visualization with reporting relationships. | `src/pages/Team/TeamPage.tsx` | +| **Resume** | **Profile Pages** | Comprehensive resumes with experiences, skills, languages, hobbies, domains. | `src/pages/ResumeV2/ResumePageV2.tsx` | +| **Profiles** | **Collaborator Profiles** | Full team member profiles with avatars, bio, accomplishments. | `src/pages/CollaboratorProfile/` | +| **UX/UI** | **Dynamic Theming** | Runtime theme switching (Cyber, Architectural, Aloe Vera) using CSS variables. | `src/styles/*-theme.css` | +| **Routing** | **Deep Linking** | URL-based state recovery for shareable deep links. | `src/pages/` | +| **CMS** | **Admin Dashboard** | Protected route suite for managing content without code changes. | `src/pages/Admin/` | +| **AI** | **AI Configuration** | Configure and test AI providers for profile generation. | `src/pages/Admin/AiSettings/` | +| **Security** | **Auth Context** | Global authentication with role-based access control. | `src/contexts/AuthContext.tsx` | +| **Design** | **Component Library** | Reusable V2 components with strict design tokens. | `src/components/` | + +--- + +## 🏗️ Project Structure + +```text +Ark.Alliance.StartupCms.Ai.UI/ +├── 📁 src/ +│ ├── components/ # Reusable UI components (MVVM) +│ │ ├── HeaderV2/ # Navigation header +│ │ ├── CarouselV2/ # Hero carousel +│ │ ├── TimelineV2/ # Resume timeline +│ │ ├── TechBadge/ # Technology/skill badges +│ │ ├── Toast/ # Notifications +│ │ └── generic/ # Base components (GlassCard, Panel) +│ │ +│ ├── contexts/ # Global state providers +│ │ ├── AuthContext.tsx # Authentication state +│ │ ├── ThemeContext.tsx # Theme switching +│ │ └── ToastContext.tsx # Toast notifications +│ │ +│ ├── pages/ # Route views +│ │ ├── Team/ # 👥 Organization chart +│ │ │ ├── TeamPage.tsx +│ │ │ └── TeamPage.styles.css # Org chart customization +│ │ ├── ResumeV2/ # 📋 Resume display +│ │ │ ├── ResumePageV2.tsx +│ │ │ ├── ResumePageV2.model.ts +│ │ │ └── ResumePageV2.styles.css +│ │ ├── CollaboratorProfile/ # 👤 Individual profiles +│ │ │ └── CollaboratorProfilePage.tsx +│ │ ├── Admin/ # 🔐 CMS Dashboard (Protected) +│ │ │ ├── Dashboard/ +│ │ │ ├── Projects/ # Project management +│ │ │ ├── Resume/ # Resume editor +│ │ │ ├── AiSettings/ # AI configuration +│ │ │ └── MediaManager/ # Asset management +│ │ ├── HomeV2/ # Landing page +│ │ ├── ProjectsV2/ # Projects grid +│ │ └── ProjectDetails/ # Project presentation +│ │ +│ ├── services/ # Business logic layer +│ │ ├── auth.service.ts # Authentication +│ │ ├── resume.service.ts # Resume data +│ │ ├── startup.service.ts # Organization/team +│ │ └── project.service.ts # Projects +│ │ +│ ├── styles/ # CSS themes and global styles +│ │ ├── cyber-theme.css # Cyber futuristic theme +│ │ ├── architectural-theme.css +│ │ ├── aloevera-theme.css +│ │ └── design-system.css +│ │ +│ ├── utils/ # Utility functions +│ ├── App.tsx # Router & Provider setup +│ └── main.tsx # Entry point +│ +├── 📁 public/ # Static assets +│ └── Assets/ # Images, icons +└── 📁 dist/ # Production build output +``` + +--- + +## 🧠 MVVM Architecture + +The application strictly adheres to the **Model-View-ViewModel (MVVM)** design pattern for scalability, testability, and separation of concerns. + +### Pattern Structure + +| Layer | File | Responsibility | +|-------|------|----------------| +| **Model** | `*.model.ts` | Type definitions, interfaces, state shape | +| **ViewModel** | `use*Model` hook | State management, side effects, business logic | +| **View** | `*.tsx` | Pure presentation, consumes ViewModel | +| **Styles** | `*.styles.css` | Component-specific styling | + +### Component File Structure + +``` +ComponentName/ +├── ComponentName.tsx # View (presentation only) +├── ComponentName.model.ts # ViewModel (state & logic) +├── ComponentName.styles.css # Styles +└── index.ts # Barrel export +``` + +### Data Flow + +```mermaid +graph LR + subgraph "MVVM Pattern" + V[View
📄 .tsx] + VM[ViewModel
🧠 useModel] + M[Model
📋 .model.ts] + end + + V -->|User Action| VM + VM -->|Updates State| VM + VM -->|Re-renders| V + VM -.->|Implements| M + + style V fill:#61DAFB + style VM fill:#4CAF50 + style M fill:#FF9800 +``` + +--- + +## 📐 Component Hierarchy + +```mermaid +graph TD + App[App.tsx] + AuthProvider[AuthProvider] + ThemeProvider[ThemeProvider] + Router[BrowserRouter] + + App --> AuthProvider + AuthProvider --> ThemeProvider + ThemeProvider --> Router + + subgraph "Public Routes" + Router --> HomePage[HomePageV2] + Router --> TeamPage[TeamPage
Organization Chart] + Router --> ResumePage[ResumePageV2
Default: Armand] + Router --> CollabProfile[CollaboratorProfile] + Router --> ProjectsPage[ProjectsPageV2] + Router --> ProjectDetail[ProjectPresentation] + end + + subgraph "Protected Admin Routes" + Router --> Dashboard[DashboardPage] + Dashboard --> ProjectMan[ProjectManager] + Dashboard --> AiSettings[AiSettingsPage] + Dashboard --> MediaMan[MediaManager] + end + + style TeamPage fill:#FFD700 + style ResumePage fill:#FFD700 + style CollabProfile fill:#FFD700 + style Dashboard fill:#FF6B6B +``` + +--- + +## ⚙️ Configuration + +### Environment Variables + +Create a `.env` file in the UI root: + +```env +# API Configuration +VITE_API_URL=http://localhost:3085/api + +# Feature Flags +VITE_ENABLE_AI_FEATURES=true +``` + +--- + +## 🚀 Usage + +### Prerequisites + +- Node.js v18+ +- Backend running (recommended) or use with mock data + +### Development Server + +```bash +npm install +npm run dev +# Server runs on http://localhost:3080 +``` + +### Production Build + +```bash +npm run build +# Output in /dist +``` + +### Preview Production Build + +```bash +npm run preview +``` + +--- + +## 🎨 Theming + +Three built-in themes are available: + +| Theme | Description | Primary Colors | +|-------|-------------|----------------| +| **Cyber** | Futuristic, neon-accented | Cyan, Blue, Purple | +| **Architectural** | Clean, structural aesthetic | Gray, Black, White | +| **Aloe Vera** | Organic, nature-inspired | Green, Earth tones | + +Toggle themes runtime via the Theme Context or user preferences. + +--- + +## 📱 Responsive Design + +The UI is fully responsive with breakpoints: + +| Breakpoint | Width | Layout | +|------------|-------|--------| +| Mobile | `< 768px` | Single column, stacked org chart | +| Tablet | `768px - 1024px` | Two columns, condensed spacing | +| Desktop | `> 1024px` | Full layout, expanded org chart | + +**Team Page Optimizations:** +- Centered CEO card with increased horizontal spacing +- Larger avatars (80px) with glowing borders +- Responsive vertical spacing (2rem mobile, 4rem desktop) + +--- + +## 🎯 Key Features + +### Team Organization Chart + +- Hierarchical visualization with CEO at top +- Reporting relationships via visual connecting lines +- Avatar-based cards with hover effects +- Click-through to collaborator full resume +- Responsive: tree on desktop, stacked cards on mobile + +### Resume System + +- **Header**: Ar avatar + name + title (horizontally centered) +- **Experiences**: Timeline with technologies, highlights, date ranges +- **Skills**: Categorized grid (Backend, Frontend, Database, DevOps, Specialized) +- **Education**: Academic background with descriptions +- **Languages**: 3-level proficiency (speaking, writing, presenting) on 1-5 scale +- **Hobbies**: Personal interests with Lucide icons +- **Business Domains**: Industry expertise with years and proficiency levels + +**Performance**: Sub-second load times via indexed database queries and parallel data fetching. + +--- + +## 📚 Related Documentation + +| Document | Location | Purpose | +|----------|----------|---------| +| Main README | `../README.md` | Project overview, philosophy, architecture | +| Share Layer | `../Ark.Alliance.StartupCms.Ai.Share/README.md` | DTOs, enums, types | +| Backend Layer | `../Ark.Alliance.StartupCms.Ai.Backend/README.md` | API endpoints, database schema | +| Tests Layer | `../Ark.Alliance.StartupCms.Ai.Tests/README.md` | Test patterns, coverage | + +--- + +
+ +**Ark.Alliance.StartupCms.AI.UI** — Part of the Ark Alliance Ecosystem + + +Armand Richelet-Kleinberg © M2H.IO
+AI-assisted development with Anthropic Claude & Google Gemini +
+ +
diff --git a/Ark.Alliance.StartupCms.Ai.UI/index.html b/Ark.Alliance.StartupCms.Ai.UI/index.html new file mode 100644 index 0000000000000000000000000000000000000000..35afd9b39c8bd90511ad5d3dce64ec9a19e1f999 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/index.html @@ -0,0 +1,108 @@ + + + + + + + Ark Portfolio + + + + + +
+ +

Ark.Alliance.Portfolio

+

Portfolio Content Management System

+
+
+ +
+ + + + \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.UI/next.config.js b/Ark.Alliance.StartupCms.Ai.UI/next.config.js new file mode 100644 index 0000000000000000000000000000000000000000..8069ff3ac48731c491ee3f0a9cf04dde5a15a635 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/next.config.js @@ -0,0 +1,92 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + // Enable App Router (default in Next.js 14) + reactStrictMode: true, + + // Output configuration + output: 'standalone', + + // Disable SWC minification for better debugging (optional) + swcMinify: true, + + // API proxy to backend + async rewrites() { + return [ + { + source: '/api/:path*', + destination: process.env.NEXT_PUBLIC_API_URL + ? `${process.env.NEXT_PUBLIC_API_URL}/:path*` + : 'http://localhost:3085/api/:path*', + }, + { + source: '/robots.txt', + destination: process.env.NEXT_PUBLIC_API_URL + ? `${process.env.NEXT_PUBLIC_API_URL}/robots.txt` + : 'http://localhost:3085/api/robots.txt', + }, + ]; + }, + + // Image optimization + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '**', + }, + { + protocol: 'http', + hostname: 'localhost', + }, + ], + formats: ['image/avif', 'image/webp'], + }, + + // Environment variables + env: { + NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3085/api', + NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000', + }, + + // Webpack configuration (for compatibility with existing Vite setup) + webpack: (config, { isServer }) => { + // Handle CSS modules from component library + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + net: false, + tls: false, + }; + + return config; + }, + + // Experimental features + experimental: { + // Enable server actions if needed + serverActions: true, + }, + + // Headers for security + async headers() { + return [ + { + source: '/:path*', + headers: [ + { + key: 'X-DNS-Prefetch-Control', + value: 'on', + }, + { + key: 'X-Frame-Options', + value: 'SAMEORIGIN', + }, + ], + }, + ]; + }, +}; + +module.exports = nextConfig; diff --git a/Ark.Alliance.StartupCms.Ai.UI/package-lock.json b/Ark.Alliance.StartupCms.Ai.UI/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..1f0fc27fb4e2336c35abdb8cdb5406696913baef --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/package-lock.json @@ -0,0 +1,7705 @@ +{ + "name": "@ark-alliance-startupcms-ia/ui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@ark-alliance-startupcms-ia/ui", + "version": "1.0.0", + "dependencies": { + "ark-alliance-react-ui": "^1.4.0", + "axios": "^1.3.5", + "chart.js": "^4.5.1", + "chartjs-plugin-zoom": "^2.2.0", + "class-variance-authority": "^0.7.1", + "clsx": "^1.2.1", + "framer-motion": "^12.23.26", + "lucide-react": "^0.562.0", + "mermaid": "^11.12.2", + "next": "^14.2.18", + "react": "19.2.3", + "react-chartjs-2": "^5.3.1", + "react-dom": "19.2.3", + "react-markdown": "^10.1.0", + "react-router-dom": "^6.10.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-highlight": "^7.0.2", + "rehype-katex": "^7.0.1", + "rehype-slug": "^6.0.0", + "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", + "tailwind-merge": "^1.14.0", + "zod": "^4.3.5" + }, + "devDependencies": { + "@types/react": "19.2.7", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-basic-ssl": "^2.0.0", + "@vitejs/plugin-react": "^4.0.0", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.21", + "tailwindcss": "^3.3.0", + "typescript": "^5.0.0", + "vite": "^7.3.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", + "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "license": "Apache-2.0" + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/@emotion/css": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz", + "integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==", + "license": "MIT", + "dependencies": { + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz", + "integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz", + "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.1.0.tgz", + "integrity": "sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.1.0.tgz", + "integrity": "sha512-0e2fdEyB4AR+e6kU4yxwA/MonnYcw/CsMEP9lH82ORFi9svA6/RhDyhxIv5mlJaldmaHLLYVTb+3iEr+PDSZuQ==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.1.0.tgz", + "integrity": "sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-3.1.1.tgz", + "integrity": "sha512-EDllr9hpodc21odmUywHS1alXNiCd4E8sp5GJ5s7wYINz8vSmMiNWpALTiuYODb865YyQ/NlyiN4mbXp7HCNqg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~6 || ~7", + "react": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "license": "Apache-2.0" + }, + "node_modules/@mermaid-js/parser": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz", + "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", + "license": "MIT", + "dependencies": { + "langium": "3.3.1" + } + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", + "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/@next/env": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.18.tgz", + "integrity": "sha512-2vWLOUwIPgoqMJKG6dt35fVXVhgM09tw4tK3/Q34GFXDrfiHlG7iS33VA4ggnjWxjiz9KV5xzfsQzJX6vGAekA==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.18.tgz", + "integrity": "sha512-tOBlDHCjGdyLf0ube/rDUs6VtwNOajaWV+5FV/ajPgrvHeisllEdymY/oDgv2cx561+gJksfMUtqf8crug7sbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.18.tgz", + "integrity": "sha512-uJCEjutt5VeJ30jjrHV1VIHCsbMYnEqytQgvREx+DjURd/fmKy15NaVK4aR/u98S1LGTnjq35lRTnRyygglxoA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.18.tgz", + "integrity": "sha512-IL6rU8vnBB+BAm6YSWZewc+qvdL1EaA+VhLQ6tlUc0xp+kkdxQrVqAnh8Zek1ccKHlTDFRyAft0e60gteYmQ4A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.18.tgz", + "integrity": "sha512-RCaENbIZqKKqTlL8KNd+AZV/yAdCsovblOpYFp0OJ7ZxgLNbV5w23CUU1G5On+0fgafrsGcW+GdMKdFjaRwyYA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.18.tgz", + "integrity": "sha512-3kmv8DlyhPRCEBM1Vavn8NjyXtMeQ49ID0Olr/Sut7pgzaQTo4h01S7Z8YNE0VtbowyuAL26ibcz0ka6xCTH5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.18.tgz", + "integrity": "sha512-mliTfa8seVSpTbVEcKEXGjC18+TDII8ykW4a36au97spm9XMPqQTpdGPNBJ9RySSFw9/hLuaCMByluQIAnkzlw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.18.tgz", + "integrity": "sha512-J5g0UFPbAjKYmqS3Cy7l2fetFmWMY9Oao32eUsBPYohts26BdrMUyfCJnZFQkX9npYaHNDOWqZ6uV9hSDPw9NA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.18.tgz", + "integrity": "sha512-Ynxuk4ZgIpdcN7d16ivJdjsDG1+3hTvK24Pp8DiDmIa2+A4CfhJSEHHVndCHok6rnLUzAZD+/UOKESQgTsAZGg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.18.tgz", + "integrity": "sha512-dtRGMhiU9TN5nyhwzce+7c/4CCeykYS+ipY/4mIrGzJ71+7zNo55ZxCB7cAVuNqdwtYniFNR2c9OFQ6UdFIMcg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", + "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.182.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz", + "integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==", + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.22.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", + "integrity": "sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.68", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.68.tgz", + "integrity": "sha512-3ab1B59Ojb6RwjOspYLsTpCzbNB3ZaamIAxBMmvnNkiDoLTZUOBXZ9p5nAYVEkQlDdf6qAZWi1pqj9+ypiqznA==", + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ark-alliance-react-ui": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ark-alliance-react-ui/-/ark-alliance-react-ui-1.4.0.tgz", + "integrity": "sha512-D4Nn/hEO9QOqY26AyITyBvY9ITXHIlefMeJWkflpjv2/tgGKPqts66MR/mbuZEV1KAZ6zHKE+/AXbZ4MXXTurg==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^7.1.0", + "@fortawesome/free-brands-svg-icons": "^7.1.0", + "@fortawesome/free-regular-svg-icons": "^7.1.0", + "@fortawesome/free-solid-svg-icons": "^7.1.0", + "@fortawesome/react-fontawesome": "^3.1.1", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.4.2", + "@react-three/postprocessing": "^3.0.4", + "mermaid": "^11.12.2", + "react-markdown": "^10.1.0", + "react-organizational-chart": "^2.2.1", + "remark-gfm": "^4.0.1", + "three": "^0.182.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "zod": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/ark-alliance-react-ui/node_modules/@react-three/drei": { + "version": "10.7.7", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz", + "integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^3.1.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.8.3", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.4", + "tunnel-rat": "^0.1.2", + "use-sync-external-store": "^1.4.0", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19", + "react-dom": "^19", + "three": ">=0.159" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/ark-alliance-react-ui/node_modules/@react-three/fiber": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz", + "integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.27.0", + "suspend-react": "^0.1.3", + "use-sync-external-store": "^1.4.0", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=19 <19.3", + "react-dom": ">=19 <19.3", + "react-native": ">=0.78", + "three": ">=0.156" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/ark-alliance-react-ui/node_modules/@react-three/fiber/node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/ark-alliance-react-ui/node_modules/@react-three/postprocessing": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@react-three/postprocessing/-/postprocessing-3.0.4.tgz", + "integrity": "sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==", + "license": "MIT", + "dependencies": { + "maath": "^0.6.0", + "n8ao": "^1.9.4", + "postprocessing": "^6.36.6" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19.0", + "three": ">= 0.156.0" + } + }, + "node_modules/ark-alliance-react-ui/node_modules/@react-three/postprocessing/node_modules/maath": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.6.0.tgz", + "integrity": "sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.144.0", + "three": ">=0.144.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/camera-controls": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", + "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==", + "license": "MIT", + "engines": { + "node": ">=22.0.0", + "npm": ">=10.5.1" + }, + "peerDependencies": { + "three": ">=0.126.1" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001761", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", + "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-plugin-zoom": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz", + "integrity": "sha512-in6kcdiTlP6npIVLMd4zXZ08PDUXC52gZ4FAy5oyjk1zX3gKarXMAof7B9eFiisf9WOC3bh2saHg+J5WtLXZeA==", + "license": "MIT", + "dependencies": { + "@types/hammerjs": "^2.0.45", + "hammerjs": "^2.0.8" + }, + "peerDependencies": { + "chart.js": ">=3.2.0" + } + }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/chevrotain/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/class-variance-authority/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", + "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "license": "MIT", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.23.26", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz", + "integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "node_modules/hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/hls.js": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", + "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==", + "license": "Apache-2.0" + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/katex": { + "version": "0.16.27", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.27.tgz", + "integrity": "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/langium": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", + "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.22", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz", + "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lowlight": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "highlight.js": "~11.11.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "11.12.2", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz", + "integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.1", + "@mermaid-js/parser": "^0.6.3", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.13", + "dayjs": "^1.11.18", + "dompurify": "^3.2.5", + "katex": "^0.16.22", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^16.2.1", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz", + "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==", + "license": "MIT" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/n8ao": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.10.1.tgz", + "integrity": "sha512-hhI1pC+BfOZBV1KMwynBrVlIm8wqLxj/abAWhF2nZ0qQKyzTSQa1QtLVS2veRiuoBQXojxobcnp0oe+PUoxf/w==", + "license": "ISC", + "peerDependencies": { + "postprocessing": ">=6.30.0", + "three": ">=0.137" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.18.tgz", + "integrity": "sha512-H9qbjDuGivUDEnK6wa+p2XKO+iMzgVgyr9Zp/4Iv29lKa+DYaxJGjOeEA+5VOvJh/M7HLiskehInSa0cWxVXUw==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.18", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.18", + "@next/swc-darwin-x64": "14.2.18", + "@next/swc-linux-arm64-gnu": "14.2.18", + "@next/swc-linux-arm64-musl": "14.2.18", + "@next/swc-linux-x64-gnu": "14.2.18", + "@next/swc-linux-x64-musl": "14.2.18", + "@next/swc-win32-arm64-msvc": "14.2.18", + "@next/swc-win32-ia32-msvc": "14.2.18", + "@next/swc-win32-x64-msvc": "14.2.18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postprocessing": { + "version": "6.38.2", + "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.38.2.tgz", + "integrity": "sha512-7DwuT7Tkst41ZjSj287g7C9c5/D3Xx5rMgBosg0dadbUPoZD2HNzkadKPol1d2PJAoI9f+Jeh1/v9YfLzpFGVw==", + "license": "Zlib", + "peerDependencies": { + "three": ">= 0.157.0 < 0.183.0" + } + }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-organizational-chart": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-organizational-chart/-/react-organizational-chart-2.2.1.tgz", + "integrity": "sha512-JORmpLeYzCVtztdqCHsnKL8H3WiLRPHjohgh/PxQoszLuaQ+l3F8YefKSfpcBPZJhHwy3SlqjFjPC28a3Hh3QQ==", + "license": "MIT", + "dependencies": { + "@emotion/css": "^11.7.1" + }, + "engines": { + "node": ">=16", + "npm": ">=8" + }, + "peerDependencies": { + "react": ">= 16.12.0", + "react-dom": ">= 16.12.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", + "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", + "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.1", + "react-router": "6.30.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rehype-autolink-headings": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz", + "integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-highlight": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", + "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-text": "^4.0.0", + "lowlight": "^3.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-katex": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", + "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "katex": "^0.16.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-math": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", + "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "license": "MIT", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "license": "MIT" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/tailwind-merge": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", + "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/three": { + "version": "0.182.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz", + "integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==", + "license": "MIT" + }, + "node_modules/three-mesh-bvh": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz", + "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.1", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", + "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", + "license": "MIT", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/troika-three-text": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", + "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.4", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", + "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", + "license": "MIT" + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz", + "integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/Ark.Alliance.StartupCms.Ai.UI/package.json b/Ark.Alliance.StartupCms.Ai.UI/package.json new file mode 100644 index 0000000000000000000000000000000000000000..c8869a9f0b6406026069527f44441db7a251eece --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/package.json @@ -0,0 +1,48 @@ +{ + "name": "@ark-alliance-startupcms-ia/ui", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 3080", + "build": "tsc && vite build", + "preview": "vite preview", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "ark-alliance-react-ui": "^1.4.0", + "axios": "^1.3.5", + "chart.js": "^4.5.1", + "chartjs-plugin-zoom": "^2.2.0", + "class-variance-authority": "^0.7.1", + "clsx": "^1.2.1", + "framer-motion": "^12.23.26", + "lucide-react": "^0.562.0", + "mermaid": "^11.12.2", + "next": "^14.2.18", + "react": "19.2.3", + "react-chartjs-2": "^5.3.1", + "react-dom": "19.2.3", + "react-markdown": "^10.1.0", + "react-router-dom": "^6.10.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-highlight": "^7.0.2", + "rehype-katex": "^7.0.1", + "rehype-slug": "^6.0.0", + "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", + "tailwind-merge": "^1.14.0", + "zod": "^4.3.5" + }, + "devDependencies": { + "@types/react": "19.2.7", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-basic-ssl": "^2.0.0", + "@vitejs/plugin-react": "^4.0.0", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.21", + "tailwindcss": "^3.3.0", + "typescript": "^5.0.0", + "vite": "^7.3.0" + } +} diff --git a/Ark.Alliance.StartupCms.Ai.UI/postcss.config.js b/Ark.Alliance.StartupCms.Ai.UI/postcss.config.js new file mode 100644 index 0000000000000000000000000000000000000000..b0babe02f809222a4a1c27c63c34178adfd78315 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/LogoArkAlliance.png b/Ark.Alliance.StartupCms.Ai.UI/public/LogoArkAlliance.png new file mode 100644 index 0000000000000000000000000000000000000000..4b4ab3a60151b93269082263c697f9315917ac6f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/LogoArkAlliance.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8f10a10d57e29ca9dcf71ae9b4dc0023d43ae7e96704364cac29c6aec20afd3 +size 220747 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/App/LogoArkAlliance.png b/Ark.Alliance.StartupCms.Ai.UI/public/assets/App/LogoArkAlliance.png new file mode 100644 index 0000000000000000000000000000000000000000..4b4ab3a60151b93269082263c697f9315917ac6f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/App/LogoArkAlliance.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8f10a10d57e29ca9dcf71ae9b4dc0023d43ae7e96704364cac29c6aec20afd3 +size 220747 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot10.PNG b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot10.PNG new file mode 100644 index 0000000000000000000000000000000000000000..545e15c4e95512074d58c91907db7d9c32986997 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot10.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9975e1830cb087bc177751ac162554ebe09ea1ee02dc881b0499954cc6fa13bc +size 321808 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot11.PNG b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot11.PNG new file mode 100644 index 0000000000000000000000000000000000000000..2ec8207c17cd6559d1c86786ecd1e013a71d9142 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot11.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39a5c84b9a23784f6ee68772b3292945d557d4681538e1bbe762a50fbaf49747 +size 378429 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot12.PNG b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot12.PNG new file mode 100644 index 0000000000000000000000000000000000000000..2ff7f228e3e9359e8a81847e0877c389b4acb3e0 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot12.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:459bb1c112c7a10eb22eb9c9a0d132c4badeaf78e23319e439c119bdc787f376 +size 155251 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot13.PNG b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot13.PNG new file mode 100644 index 0000000000000000000000000000000000000000..068b233c1fca51af8aaceef21e3a1862529f0c43 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot13.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:614315756f28c729ae556a3b284460a0d7f63e8e9e3f00f10d752f9dcd840127 +size 170264 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot14.PNG b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot14.PNG new file mode 100644 index 0000000000000000000000000000000000000000..0cf2808307669682bfced7468a04ef85b9ae622a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot14.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ac92fa503174a5c4c6685c731a06b77dfe9aa7ff094f8ab3034d68fc8a0c0e1 +size 459307 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot2.PNG b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot2.PNG new file mode 100644 index 0000000000000000000000000000000000000000..75fc7e8910a064c50afad413ef8c27bcbf56ae99 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot2.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c25f04b3a256ba08022b0a14e7f3fb8d4c8d5fdb68ab8e5e9390cee46dc753a +size 465243 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot3.PNG b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot3.PNG new file mode 100644 index 0000000000000000000000000000000000000000..a5c10232d8c105d6396bc1d03f9a7cecc61dc804 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot3.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b7e290bd1e3c51a836ab9ab613c594a1aa57315dc9a76f961899692e675ed3a +size 397617 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot4.PNG b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot4.PNG new file mode 100644 index 0000000000000000000000000000000000000000..6c845c86ac723bbb1e00fc05a65f4c9ed3273a98 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot4.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f322678e0f5a499352eccd3a6a89e0190274fc19ab03ab08ed36c2d8e0731675 +size 213861 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot5.PNG b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot5.PNG new file mode 100644 index 0000000000000000000000000000000000000000..d05306ce7a2306af63c75d852f8deedf9544320f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot5.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a74f2a561083dc5041332b70f1ef267ebb3ba2b2fbf72767a84ff7b7e7dec4c +size 215587 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot6.PNG b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot6.PNG new file mode 100644 index 0000000000000000000000000000000000000000..4207ff17a1c808bb56db7d3eb524d98fdd33738e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot6.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66435ea297e9f7466e72a3cd1f4671e1e4da1f914521786a3cdcd4987624aad6 +size 505004 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot7.PNG b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot7.PNG new file mode 100644 index 0000000000000000000000000000000000000000..fc843ea6466d799b3b000fdf29411bf71a00e75a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot7.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2cea73cf7bf2a1e4dedadad32fc30e2b6720ed513cb0b3c2ee0679aaea46d5a +size 369687 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot8.PNG b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot8.PNG new file mode 100644 index 0000000000000000000000000000000000000000..3d7c3f49fa5ae9495584308cf7b4eb1376640a9a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Bot8.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b554f447aa539e11f277200d0979cd6409c6f49bbb09968d043efce36b6ac12 +size 271418 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/Capture.PNG b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Capture.PNG new file mode 100644 index 0000000000000000000000000000000000000000..fc843ea6466d799b3b000fdf29411bf71a00e75a --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Capture.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2cea73cf7bf2a1e4dedadad32fc30e2b6720ed513cb0b3c2ee0679aaea46d5a +size 369687 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/Icon.png b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6174b39adc62e80e8d5a1dd409d3af3ba4aa4398 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/Icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac6bf4eb19783054f288a240b7a185984d3967f25d80ab59fb7e75d9dca2f631 +size 101421 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/bot1.PNG b/Ark.Alliance.StartupCms.Ai.UI/public/assets/bot1.PNG new file mode 100644 index 0000000000000000000000000000000000000000..d44cf89a01e264a1cdb87affc4ac800d3606fc40 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/bot1.PNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcc256615a841b8686d03fa6159e33d37b1a40ccd0365288c946dd9740f0b2de +size 258622 diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/assets/grid.svg b/Ark.Alliance.StartupCms.Ai.UI/public/assets/grid.svg new file mode 100644 index 0000000000000000000000000000000000000000..2dfb39a9e14773bd83286f7ff8cdee386cd7b0c5 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/assets/grid.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Ark.Alliance.StartupCms.Ai.UI/public/logo.png b/Ark.Alliance.StartupCms.Ai.UI/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6174b39adc62e80e8d5a1dd409d3af3ba4aa4398 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/public/logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac6bf4eb19783054f288a240b7a185984d3967f25d80ab59fb7e75d9dca2f631 +size 101421 diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/App.tsx b/Ark.Alliance.StartupCms.Ai.UI/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..27ce0d6e31c14b9cdcb2c12f361b0a0eda4d7d39 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/App.tsx @@ -0,0 +1,145 @@ +/** + * @fileoverview Main Application Component + * Root component with routing, theme provider, and toast notifications. + * + * @author Armand Richelet-Kleinberg + */ + +// ═══════════════════════════════════════════════════════════════════════════ +// REACT & ROUTER +// ═══════════════════════════════════════════════════════════════════════════ +import { useState } from 'react'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; + +// ═══════════════════════════════════════════════════════════════════════════ +// CONTEXTS & PROVIDERS +// ═══════════════════════════════════════════════════════════════════════════ +import { ThemeProvider } from './components/generic/ThemeContext'; +import { AuthProvider } from './contexts/AuthContext'; +import { TechnologyProvider } from './contexts/TechnologyContext'; +import { CyberThemeProvider } from './contexts/CyberThemeContext'; +import { ToastProvider } from './contexts/ToastContext'; + +// ═══════════════════════════════════════════════════════════════════════════ +// COMPONENTS +// ═══════════════════════════════════════════════════════════════════════════ +import { ProtectedRoute } from './components/generic/ProtectedRoute'; +import { ToastContainer } from './components/generic/Toast'; +import { SEOLayout } from './components/Layout/SEOLayout'; + +// ═══════════════════════════════════════════════════════════════════════════ +// PUBLIC PAGES +// ═══════════════════════════════════════════════════════════════════════════ +import { HomePageV2 } from './pages/HomeV2'; +import { ResumePageV2 } from './pages/ResumeV2'; +import { ProjectsPageV2 } from './pages/ProjectsV2'; +import { ProjectPresentation } from './pages/ProjectDetails/v2/ProjectPresentation'; +import { ArchitecturePage } from './pages/Architecture'; +import { TeamPage } from './pages/Team'; +import { CollaboratorProfilePage } from './pages/CollaboratorProfile'; +import { LoginPage } from './pages/Login'; +import { LoadingPage } from './pages/Loading'; + +// ═══════════════════════════════════════════════════════════════════════════ +// ADMIN PAGES +// ═══════════════════════════════════════════════════════════════════════════ +import { DashboardPage } from './pages/Admin/Dashboard'; +import { ProjectManager } from './pages/Admin/Projects'; +import { ProjectEditPage } from './pages/Admin/Projects/ProjectEditPage'; +import { ResumeManager } from './pages/Admin/Resume'; +import { WidgetManager } from './pages/Admin/Widgets'; +import { MenuManager } from './pages/Admin/Menu'; +import { StyleManager } from './pages/Admin/Styles'; +import { ThemeManager } from './pages/Admin/Themes'; +import { MediaManager } from './pages/Admin/Media'; +import { CarouselManager } from './pages/Admin/Carousel'; +import { AiSettingsPage } from './pages/Admin/AiSettings'; +import { StaticExportManager } from './pages/Admin/StaticExport'; + +// ═══════════════════════════════════════════════════════════════════════════ +// STYLES +// ═══════════════════════════════════════════════════════════════════════════ +import './styles/architectural-theme.css'; +import './styles/design-system.css'; +import './styles/aloevera-theme.css'; + + +/** + * Main application routes. + */ +const AppRoutes = () => { + return ( + + {/* Public Routes */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Protected Admin Routes */} + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + } /> + + } /> + + ); +}; + +/** + * Main App component with theme provider. + * Shows loading page at startup until all data is loaded. + */ +function App() { + const [isLoading, setIsLoading] = useState(true); + + const handleLoadingComplete = () => { + setIsLoading(false); + }; + + return ( + + + + + + {isLoading ? ( + + ) : ( + + + + + + + )} + + + + + + ); +} + +export default App; diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/Assets/LogoArkAlliance.png b/Ark.Alliance.StartupCms.Ai.UI/src/Assets/LogoArkAlliance.png new file mode 100644 index 0000000000000000000000000000000000000000..4b4ab3a60151b93269082263c697f9315917ac6f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/Assets/LogoArkAlliance.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8f10a10d57e29ca9dcf71ae9b4dc0023d43ae7e96704364cac29c6aec20afd3 +size 220747 diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/client.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/client.ts new file mode 100644 index 0000000000000000000000000000000000000000..33028c3ad9ff7ce1838fc79e3e82ac0241bae406 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/client.ts @@ -0,0 +1,121 @@ +/** + * @fileoverview API Client + * HTTP client wrapper with mock data fallback support for development. + * + * @author Armand Richelet-Kleinberg + * @module api/client + */ + +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; + +// Define configuration to allow switching mock/real +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'; +// Enable usage of mocks if VITE_USE_MOCKS is true OR if request fails and we are in DEV mode +const FORCE_MOCKS = import.meta.env.VITE_USE_MOCKS === 'true'; +const ALLOW_FALLBACK = import.meta.env.DEV || import.meta.env.VITE_ALLOW_MOCK_FALLBACK === 'true'; + +/** + * API Client class with automatic mock data fallback. + * + * @remarks + * Supports development mode fallback to mock data when API calls fail. + * Controlled via environment variables: + * - VITE_USE_MOCKS: Force mock data always + * - VITE_ALLOW_MOCK_FALLBACK: Allow fallback on failure + */ +export class ApiClient { + private client: AxiosInstance; + + constructor() { + this.client = axios.create({ + baseURL: API_BASE_URL, + timeout: 5000, + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + /** + * Execute an API request with optional mock data fallback. + * + * @param request - Function that makes the actual API call + * @param url - URL for logging purposes + * @param mockData - Optional mock data to use on failure or forced mocking + * @returns The response data or mock data on fallback + * @throws Error if no mock data available and request fails + */ + private async withMockFallback( + request: () => Promise>, + url: string, + mockData?: T + ): Promise { + try { + const response = await request(); + return response.data; + } catch (error) { + if ((FORCE_MOCKS || ALLOW_FALLBACK) && mockData) { + console.warn(`API call to ${url} failed. Falling back to mock data.`); + return mockData; + } + throw error; + } + } + + /** + * Perform a GET request. + */ + async get(url: string, config?: AxiosRequestConfig, mockData?: T): Promise { + return this.withMockFallback( + () => this.client.get(url, config), + url, + mockData + ); + } + + /** + * Perform a POST request. + */ + async post(url: string, data?: any, config?: AxiosRequestConfig, mockData?: T): Promise { + return this.withMockFallback( + () => this.client.post(url, data, config), + url, + mockData + ); + } + + /** + * Perform a PUT request. + */ + async put(url: string, data?: any, config?: AxiosRequestConfig, mockData?: T): Promise { + return this.withMockFallback( + () => this.client.put(url, data, config), + url, + mockData + ); + } + + /** + * Perform a DELETE request. + */ + async delete(url: string, config?: AxiosRequestConfig, mockData?: T): Promise { + return this.withMockFallback( + () => this.client.delete(url, config), + url, + mockData + ); + } + + /** + * Perform a PATCH request. + */ + async patch(url: string, data?: any, config?: AxiosRequestConfig, mockData?: T): Promise { + return this.withMockFallback( + () => this.client.patch(url, data, config), + url, + mockData + ); + } +} + +export const apiClient = new ApiClient(); diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/client/apiClient.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/client/apiClient.ts new file mode 100644 index 0000000000000000000000000000000000000000..334aa4939139ee57aea413da99166b3cbd3165fe --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/client/apiClient.ts @@ -0,0 +1,44 @@ +import axios from 'axios'; + +import { authService } from '../../services/auth.service'; +import { API_CONFIG } from '../../config/api.constants'; + +// Use centralized API config +const BASE_URL = API_CONFIG.BASE_URL; + +export const apiClient = axios.create({ + baseURL: BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, + timeout: 10000, +}); + +// Request Interceptor: Inject Token +apiClient.interceptors.request.use( + (config) => { + const token = authService.getToken(); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// Response Interceptor: Handle 401 +apiClient.interceptors.response.use( + (response) => response, + (error) => { + if (error.response && error.response.status === 401) { + console.warn('[API] Unauthorized - redirecting to login'); + authService.logout(); + // Optional: trigger a redirect event or rely on auth state change + window.location.href = '/login'; + } + return Promise.reject(error); + } +); + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/enhanced-client.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/enhanced-client.ts new file mode 100644 index 0000000000000000000000000000000000000000..64ce022dcb6b74e136e01b1354e4416bd11828bc --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/enhanced-client.ts @@ -0,0 +1,179 @@ +/** + * @fileoverview Enhanced API Client with Auth Interceptors + * @module api/enhanced-client + * + * Provides typed HTTP methods with automatic token handling, + * token refresh on 401, and centralized error transformation. + * + * @author Armand Richelet-Kleinberg + */ + +import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'; + +// ═══════════════════════════════════════════════════════════════════════════ +// TYPES +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Standardized API error format + */ +export interface ApiError { + message: string; + statusCode: number; + details?: Record; +} + +/** + * Standard API response wrapper + */ +export interface ApiResponse { + data: T; + success: boolean; + message?: string; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// CONFIGURATION +// ═══════════════════════════════════════════════════════════════════════════ + +const API_BASE_URL = import.meta.env.VITE_API_URL || import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001/api'; + +const TOKEN_KEYS = { + ACCESS: 'ark_access_token', + REFRESH: 'ark_refresh_token', + USER: 'ark_user' +} as const; + +// ═══════════════════════════════════════════════════════════════════════════ +// CLIENT FACTORY +// ═══════════════════════════════════════════════════════════════════════════ + +function createEnhancedApiClient(): AxiosInstance { + const client = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, + timeout: 30000, + }); + + // ═══════════════════════════════════════════════════════════════════════ + // REQUEST INTERCEPTOR: Add auth token + // ═══════════════════════════════════════════════════════════════════════ + client.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + const token = sessionStorage.getItem(TOKEN_KEYS.ACCESS); + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) + ); + + // ═══════════════════════════════════════════════════════════════════════ + // RESPONSE INTERCEPTOR: Handle errors and token refresh + // ═══════════════════════════════════════════════════════════════════════ + client.interceptors.response.use( + (response) => response, + async (error: AxiosError) => { + const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; + + // Handle 401 Unauthorized - attempt token refresh + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + + const refreshToken = localStorage.getItem(TOKEN_KEYS.REFRESH); + if (refreshToken) { + try { + const refreshResponse = await axios.post( + `${API_BASE_URL}/auth/refresh`, + { refreshToken } + ); + + const { accessToken, refreshToken: newRefreshToken } = refreshResponse.data.tokens; + sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken); + localStorage.setItem(TOKEN_KEYS.REFRESH, newRefreshToken); + + // Retry original request with new token + if (originalRequest.headers) { + originalRequest.headers.Authorization = `Bearer ${accessToken}`; + } + return client(originalRequest); + } catch (refreshError) { + // Refresh failed - clear tokens and redirect to login + sessionStorage.removeItem(TOKEN_KEYS.ACCESS); + localStorage.removeItem(TOKEN_KEYS.REFRESH); + localStorage.removeItem(TOKEN_KEYS.USER); + window.location.href = '/login'; + return Promise.reject(refreshError); + } + } + } + + // Transform error to consistent format + const apiError: ApiError = { + message: error.response?.data?.message || error.message || 'An error occurred', + statusCode: error.response?.status || 500, + details: error.response?.data?.details + }; + + return Promise.reject(apiError); + } + ); + + return client; +} + +// Export singleton instance +export const enhancedApiClient = createEnhancedApiClient(); + +// ═══════════════════════════════════════════════════════════════════════════ +// HELPER METHODS +// ═══════════════════════════════════════════════════════════════════════════ + +export const api = { + get: (url: string, params?: object) => + enhancedApiClient.get(url, { params }).then(res => res.data), + + post: (url: string, data?: object) => + enhancedApiClient.post(url, data).then(res => res.data), + + put: (url: string, data?: object) => + enhancedApiClient.put(url, data).then(res => res.data), + + patch: (url: string, data?: object) => + enhancedApiClient.patch(url, data).then(res => res.data), + + delete: (url: string) => + enhancedApiClient.delete(url).then(res => res.data), + + upload: (url: string, formData: FormData) => + enhancedApiClient.post(url, formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }).then(res => res.data) +}; + +// ═══════════════════════════════════════════════════════════════════════════ +// TOKEN UTILITIES +// ═══════════════════════════════════════════════════════════════════════════ + +export const tokenUtils = { + getAccessToken: () => sessionStorage.getItem(TOKEN_KEYS.ACCESS), + getRefreshToken: () => localStorage.getItem(TOKEN_KEYS.REFRESH), + + setTokens: (accessToken: string, refreshToken: string) => { + sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken); + localStorage.setItem(TOKEN_KEYS.REFRESH, refreshToken); + }, + + clearTokens: () => { + sessionStorage.removeItem(TOKEN_KEYS.ACCESS); + localStorage.removeItem(TOKEN_KEYS.REFRESH); + localStorage.removeItem(TOKEN_KEYS.USER); + }, + + isAuthenticated: () => !!sessionStorage.getItem(TOKEN_KEYS.ACCESS) +}; + +export default api; diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/index.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..611c4f8b773f76723a530f173e2bbdf46442bb55 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/index.ts @@ -0,0 +1,62 @@ +/** + * @fileoverview API Service Barrel Export + * @module api + * + * Central export point for all API services and utilities. + * + * @author Armand Richelet-Kleinberg + */ + +// Re-export Role enum from Share layer +export { Role, RolePriority, SupervisorAssignableRoles, getRoleLabel, hasHigherOrEqualPriority } from '@arkalliance/startupcms-ai-share'; + +// ═══════════════════════════════════════════════════════════════════════════ +// CLIENT & UTILITIES +// ═══════════════════════════════════════════════════════════════════════════ + +export { enhancedApiClient, api, tokenUtils } from './enhanced-client'; +export type { ApiError, ApiResponse } from './enhanced-client'; + +// Legacy client for backward compatibility +export { apiClient } from './client'; + +// ═══════════════════════════════════════════════════════════════════════════ +// SERVICES +// ═══════════════════════════════════════════════════════════════════════════ + +export { authService } from './services/auth.service'; +export type { AuthUser, LoginResponse, RegisterRequest } from './services/auth.service'; + +export { organizationService } from './services/organization.service'; +export type { OrganizationDto, OrganizationWithStatsDto, UpdateOrganizationDto } from './services/organization.service'; + +export { collaboratorService } from './services/collaborator.service'; +export type { + OrgChartNodeDto, + OrgChartDto, + CollaboratorSummaryDto, + CollaboratorDto, + CollaboratorWithReportsDto, + CreateCollaboratorDto, + UpdateCollaboratorDto +} from './services/collaborator.service'; + +export { userService } from './services/user.service'; +export type { UserListDto, UserDetailDto } from './services/user.service'; + +export { auditLogService } from './services/audit-log.service'; +export type { + AuditAction, + AuditLogDto, + AuditLogListResponseDto, + AuditLogQueryDto +} from './services/audit-log.service'; + +export { aiResumeService } from './services/ai-resume.service'; +export type { + AIParseDocumentResponseDto, + AIImproveTextRequestDto, + AIImproveTextResponseDto +} from './services/ai-resume.service'; + + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/services/AdminApiService.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/AdminApiService.ts new file mode 100644 index 0000000000000000000000000000000000000000..3365b7677fef34f900c23a6265ed0f52c62a7684 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/AdminApiService.ts @@ -0,0 +1,293 @@ +/** + * @fileoverview Admin API Service + * Centralized service for all admin CRUD operations. + * + * @author Armand Richelet-Kleinberg + */ + +import axios from 'axios'; +import { authService } from '../../services/auth.service'; +import { + AdminProjectDto, + AdminWidgetDto, + AdminMenuItemDto, + AdminStyleConfigDto, + AdminCarouselItemDto, + ReorderWidgetsDto, + ReorderMenuItemsDto, + ReorderCarouselDto, + AdminMediaDto, + CreateMediaFromUrlDto, + PaginatedResponseDto, + CrudResponseDto, +} from '@arkalliance/startupcms-ai-share'; +import { API_CONFIG } from '../../config/api.constants'; + +const API_BASE = API_CONFIG.ADMIN_BASE_URL; + +const getAuthHeaders = () => ({ + headers: { Authorization: `Bearer ${authService.getToken()}` } +}); + +// ============================================ +// Projects +// ============================================ +export const AdminProjectApi = { + getAll: async (page = 1, pageSize = 10, search?: string) => { + const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) }); + if (search) params.append('search', search); + const response = await axios.get(`${API_BASE}/projects?${params}`, getAuthHeaders()); + return response.data; + }, + getById: async (id: string) => { + const response = await axios.get(`${API_BASE}/projects/${id}`, getAuthHeaders()); + return response.data; + }, + create: async (dto: AdminProjectDto) => { + const response = await axios.post(`${API_BASE}/projects`, dto, getAuthHeaders()); + return response.data; + }, + update: async (id: string, dto: AdminProjectDto) => { + const response = await axios.put(`${API_BASE}/projects/${id}`, dto, getAuthHeaders()); + return response.data; + }, + delete: async (id: string) => { + const response = await axios.delete(`${API_BASE}/projects/${id}`, getAuthHeaders()); + return response.data; + } +}; + +// ============================================ +// Widgets +// ============================================ +export const AdminWidgetApi = { + getAll: async (pageId = 'home') => { + const response = await axios.get(`${API_BASE}/widgets?pageId=${pageId}`, getAuthHeaders()); + return response.data; + }, + create: async (dto: AdminWidgetDto) => { + const response = await axios.post(`${API_BASE}/widgets`, dto, getAuthHeaders()); + return response.data; + }, + update: async (id: number, dto: AdminWidgetDto) => { + const response = await axios.put(`${API_BASE}/widgets/${id}`, dto, getAuthHeaders()); + return response.data; + }, + delete: async (id: number) => { + const response = await axios.delete(`${API_BASE}/widgets/${id}`, getAuthHeaders()); + return response.data; + }, + reorder: async (dto: ReorderWidgetsDto) => { + const response = await axios.put(`${API_BASE}/widgets/reorder`, dto, getAuthHeaders()); + return response.data; + } +}; + +// ============================================ +// Menu Items +// ============================================ +export const AdminMenuApi = { + getAll: async (position?: string) => { + const url = position ? `${API_BASE}/menu?position=${position}` : `${API_BASE}/menu`; + const response = await axios.get(url, getAuthHeaders()); + return response.data; + }, + create: async (dto: AdminMenuItemDto) => { + const response = await axios.post(`${API_BASE}/menu`, dto, getAuthHeaders()); + return response.data; + }, + update: async (id: number, dto: AdminMenuItemDto) => { + const response = await axios.put(`${API_BASE}/menu/${id}`, dto, getAuthHeaders()); + return response.data; + }, + delete: async (id: number) => { + const response = await axios.delete(`${API_BASE}/menu/${id}`, getAuthHeaders()); + return response.data; + }, + reorder: async (dto: ReorderMenuItemsDto) => { + const response = await axios.put(`${API_BASE}/menu/reorder`, dto, getAuthHeaders()); + return response.data; + } +}; + +// ============================================ +// Styles +// ============================================ +export const AdminStyleApi = { + getAll: async () => { + const response = await axios.get(`${API_BASE}/styles`, getAuthHeaders()); + return response.data; + }, + getActive: async () => { + const response = await axios.get(`${API_BASE}/styles/active`, getAuthHeaders()); + return response.data; + }, + create: async (dto: AdminStyleConfigDto) => { + const response = await axios.post(`${API_BASE}/styles`, dto, getAuthHeaders()); + return response.data; + }, + update: async (id: number, dto: AdminStyleConfigDto) => { + const response = await axios.put(`${API_BASE}/styles/${id}`, dto, getAuthHeaders()); + return response.data; + }, + delete: async (id: number) => { + const response = await axios.delete(`${API_BASE}/styles/${id}`, getAuthHeaders()); + return response.data; + }, + activate: async (id: number) => { + const response = await axios.put(`${API_BASE}/styles/${id}/activate`, {}, getAuthHeaders()); + return response.data; + } +}; + +// ============================================ +// Media +// ============================================ + +/** + * Media upload progress callback + */ +export type MediaUploadProgressCallback = (progress: number) => void; + +export const AdminMediaApi = { + /** + * Get all media with optional filtering + * @param params - Filter parameters (type, search, tags, page, pageSize) + */ + getAll: async (params?: { + type?: string; + search?: string; + tags?: string[]; + page?: number; + pageSize?: number; + }) => { + const searchParams = new URLSearchParams(); + if (params?.type) searchParams.append('type', params.type); + if (params?.search) searchParams.append('search', params.search); + if (params?.tags) searchParams.append('tags', params.tags.join(',')); + if (params?.page) searchParams.append('page', String(params.page)); + if (params?.pageSize) searchParams.append('pageSize', String(params.pageSize)); + + const url = searchParams.toString() + ? `${API_BASE}/media?${searchParams.toString()}` + : `${API_BASE}/media`; + + const response = await axios.get(url, getAuthHeaders()); + return response.data; + }, + + /** + * Upload media file + * @param file - File object from input + * @param metadata - Media metadata + * @param onProgress - Optional progress callback + */ + upload: async ( + file: File, + metadata: Partial, + onProgress?: MediaUploadProgressCallback + ): Promise> => { + const formData = new FormData(); + formData.append('file', file); + formData.append('name', metadata.name || file.name); + + if (metadata.key) formData.append('key', metadata.key); + if (metadata.altText) formData.append('altText', metadata.altText); + if (metadata.description) formData.append('description', metadata.description); + if (metadata.tags) formData.append('tags', JSON.stringify(metadata.tags)); + if (metadata.metadata) formData.append('metadata', JSON.stringify(metadata.metadata)); + if (metadata.isPublic !== undefined) formData.append('isPublic', String(metadata.isPublic)); + + const response = await axios.post(`${API_BASE}/media/upload`, formData, { + ...getAuthHeaders(), + headers: { + ...getAuthHeaders().headers, + 'Content-Type': 'multipart/form-data', + }, + onUploadProgress: (progressEvent) => { + if (onProgress && progressEvent.total) { + const percentCompleted = Math.round( + (progressEvent.loaded * 100) / progressEvent.total + ); + onProgress(percentCompleted); + } + }, + }); + + return response.data; + }, + + /** + * Create media from external URL + * @param dto - Media from URL DTO + */ + createFromUrl: async (dto: CreateMediaFromUrlDto): Promise> => { + const response = await axios.post(`${API_BASE}/media/url`, dto, getAuthHeaders()); + return response.data; + }, + + /** + * Update media metadata + * @param id - Media ID + * @param dto - Updated metadata + */ + update: async (id: string, dto: Partial): Promise => { + const response = await axios.put(`${API_BASE}/media/${id}`, dto, getAuthHeaders()); + return response.data; + }, + + /** + * Delete media by ID + * @param id - Media ID + */ + delete: async (id: string): Promise => { + const response = await axios.delete(`${API_BASE}/media/${id}`, getAuthHeaders()); + return response.data; + }, + + /** + * Get media by key + * @param key - Media key + */ + getByKey: async (key: string): Promise => { + const response = await axios.get(`${API_BASE}/media/key/${key}`, getAuthHeaders()); + return response.data; + }, + + /** + * Get all media tags + */ + getTags: async (): Promise => { + const response = await axios.get(`${API_BASE}/media/tags`, getAuthHeaders()); + return response.data; + }, +}; + +// ============================================ +// Carousel +// ============================================ +export const AdminCarouselApi = { + getAll: async () => { + const response = await axios.get(`${API_BASE}/carousel`, getAuthHeaders()); + return response.data; + }, + create: async (dto: AdminCarouselItemDto) => { + const response = await axios.post(`${API_BASE}/carousel`, dto, getAuthHeaders()); + return response.data; + }, + update: async (id: number, dto: AdminCarouselItemDto) => { + const response = await axios.put(`${API_BASE}/carousel/${id}`, dto, getAuthHeaders()); + return response.data; + }, + delete: async (id: number) => { + const response = await axios.delete(`${API_BASE}/carousel/${id}`, getAuthHeaders()); + return response.data; + }, + reorder: async (dto: ReorderCarouselDto) => { + const response = await axios.put(`${API_BASE}/carousel/reorder`, dto, getAuthHeaders()); + return response.data; + } +}; + + + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/services/DashboardApiService.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/DashboardApiService.ts new file mode 100644 index 0000000000000000000000000000000000000000..64749521b59151df18af3d0ed65571e34094d979 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/DashboardApiService.ts @@ -0,0 +1,33 @@ +import { DashboardDataDto } from '@arkalliance/startupcms-ai-share'; +import { apiClient } from '../client/apiClient'; + +export class DashboardApiService { + + public async getDashboardData(): Promise { + // Fallback Mock Data if API fails during dev + const mockData: DashboardDataDto = { + stats: { + totalProjects: 12, + activeProjects: 3, + totalSkills: 24, + totalExperienceYears: 5.5 + }, + activity: { + labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], + commits: [50, 80, 120, 90, 150, 180], + complexity: [20, 30, 40, 35, 50, 60] + } + }; + + try { + const response = await apiClient.get('/dashboard'); + return response.data; + } catch (error) { + console.warn('Dashboard API failed, using mock data:', error); + return mockData; + } + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/services/ProfileApiService.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/ProfileApiService.ts new file mode 100644 index 0000000000000000000000000000000000000000..a988848bb39475356eaa17ae8dc016803b6d2034 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/ProfileApiService.ts @@ -0,0 +1,37 @@ +import { apiClient } from '../client/apiClient'; +import { ProfileDto } from '@arkalliance/startupcms-ai-share'; + +// Mock Profile for fallback +const MOCK_PROFILE: ProfileDto = { + firstName: 'Armand', + lastName: 'Richelet Kleinberg', + title: 'Full Stack Engineer', + overview: 'Specialized in high-frequency trading platforms and scalable architectures.', + email: 'contact@ark.com', + linkedinUrl: 'https://linkedin.com', + githubUrl: 'https://github.com/ark' +}; + +export class ProfileApiService { + private static instance: ProfileApiService; + + private constructor() { } + + public static getInstance(): ProfileApiService { + if (!ProfileApiService.instance) { + ProfileApiService.instance = new ProfileApiService(); + } + return ProfileApiService.instance; + } + + public async getProfile(): Promise { + try { + const response = await apiClient.get('/profile'); + return response.data; + } catch (error) { + return MOCK_PROFILE; + } + } +} + + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/services/ProjectApiService.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/ProjectApiService.ts new file mode 100644 index 0000000000000000000000000000000000000000..13dd385b4847bd487d215affa02a2d0ffc6423e3 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/ProjectApiService.ts @@ -0,0 +1,28 @@ +import { apiClient } from '../client/apiClient'; +import { ProjectDto } from '@arkalliance/startupcms-ai-share'; + +export class ProjectApiService { + private static instance: ProjectApiService; + + private constructor() { } + + public static getInstance(): ProjectApiService { + if (!this.instance) { + this.instance = new ProjectApiService(); + } + return this.instance; + } + + public async getAllProjects(): Promise { + const response = await apiClient.get('/projects'); + return response.data; + } + + public async getProjectById(id: string): Promise { + const response = await apiClient.get(`/projects/${id}`); + return response.data; + } +} + + + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/services/ResumeApiService.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/ResumeApiService.ts new file mode 100644 index 0000000000000000000000000000000000000000..c192ff503ead41a925fb0d9c1495523f8befb89b --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/ResumeApiService.ts @@ -0,0 +1,76 @@ +/** + * @fileoverview Resume API Service + * Public API service for fetching resume data. + * + * @author Armand Richelet-Kleinberg + */ + +import { apiClient } from '../client/apiClient'; +import { AdminResumeDto, SkillLevel } from '@arkalliance/startupcms-ai-share'; + +/** Resume DTO type (using AdminResumeDto for compatibility) */ +export type ResumeDto = AdminResumeDto; + +/** + * Mock resume data for fallback + */ +const MOCK_RESUME: ResumeDto = { + profile: { + firstName: '', + lastName: '', + title: '', + overview: '', + email: '', + }, + education: [], + experiences: [], + skills: [ + { name: 'React', level: SkillLevel.EXPERT, category: 'Frontend' }, + { name: 'Node.js', level: SkillLevel.ADVANCED, category: 'Backend' } + ] +}; + +/** + * Resume API Service + * Singleton service for fetching public resume data. + */ +export class ResumeApiService { + private static instance: ResumeApiService; + + private constructor() { } + + public static getInstance(): ResumeApiService { + if (!ResumeApiService.instance) { + ResumeApiService.instance = new ResumeApiService(); + } + return ResumeApiService.instance; + } + + /** + * Fetches the public resume data + * @returns Promise + */ + public async getResume(): Promise { + try { + const response = await apiClient.get('/resume'); + return response.data; + } catch (error) { + console.error('Failed to fetch resume:', error); + return MOCK_RESUME; + } + } + + /** + * @deprecated Use getResume() instead + */ + public async getCV(): Promise { + return this.getResume(); + } +} + +/** + * @deprecated Use ResumeApiService instead + */ +export const CvApiService = ResumeApiService; + + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/services/ai-resume.service.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/ai-resume.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..de8c8d7722282bd61106ecdf33653443ad00a8b7 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/ai-resume.service.ts @@ -0,0 +1,120 @@ +/** + * @fileoverview AI Resume Assistant API Service + * @module api/services/ai-resume + * + * Handles document parsing and AI-powered resume building. + * + * @author Armand Richelet-Kleinberg + */ + +import { api } from '../enhanced-client'; + +// ═══════════════════════════════════════════════════════════════════════════ +// TYPES +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Parsed document response + */ +export interface AIParseDocumentResponseDto { + success: boolean; + extractedData: { + overview?: string; + experience?: Array<{ + company: string; + position: string; + startDate: string; + endDate?: string; + description?: string; + highlights?: string[]; + }>; + education?: Array<{ + institution: string; + degree: string; + field: string; + graduationYear?: number; + }>; + skills?: { + technical?: string[]; + soft?: string[]; + languages?: string[]; + }; + contact?: { + email?: string; + phone?: string; + linkedIn?: string; + github?: string; + }; + }; + rawText?: string; + confidence: number; +} + +/** + * Text improvement request + */ +export interface AIImproveTextRequestDto { + text: string; + section: 'overview' | 'experience' | 'skills'; + tone?: 'professional' | 'casual' | 'technical'; +} + +/** + * Text improvement response + */ +export interface AIImproveTextResponseDto { + improvedText: string; + suggestions?: string[]; + changes: { + type: 'grammar' | 'clarity' | 'impact' | 'conciseness'; + original: string; + improved: string; + }[]; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// SERVICE CLASS +// ═══════════════════════════════════════════════════════════════════════════ + +class AIResumeApiService { + /** + * Parse uploaded document and extract resume data. + */ + async parseDocument(file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + return api.upload('/ai/parse-document', formData); + } + + /** + * Improve text section using AI. + */ + async improveText( + text: string, + section: 'overview' | 'experience' | 'skills', + tone: 'professional' | 'casual' | 'technical' = 'professional' + ): Promise { + return api.post('/ai/improve-text', { + text, + section, + tone + }); + } + + /** + * Generate summary from experience list. + */ + async generateSummary(experiences: Array<{ position: string; company: string; description: string }>): Promise<{ summary: string }> { + return api.post<{ summary: string }>('/ai/generate-summary', { experiences }); + } + + /** + * Suggest skills based on experience. + */ + async suggestSkills(experiences: string[]): Promise<{ suggested: string[]; categories: Record }> { + return api.post('/ai/suggest-skills', { experiences }); + } +} + +export const aiResumeService = new AIResumeApiService(); +export default aiResumeService; diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/services/audit-log.service.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/audit-log.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a35ed6fe77320d1e9bdccf79e243853bf84d702 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/audit-log.service.ts @@ -0,0 +1,125 @@ +/** + * @fileoverview Audit Log API Service + * @module api/services/audit-log + * + * Handles audit log queries. + * + * @author Armand Richelet-Kleinberg + */ + +import { api } from '../enhanced-client'; + +// ═══════════════════════════════════════════════════════════════════════════ +// TYPES +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Audit action types + */ +export type AuditAction = + | 'LOGIN_SUCCESS' + | 'LOGIN_FAILURE' + | 'LOGOUT' + | 'PASSWORD_CHANGE' + | 'PASSWORD_RESET_REQUEST' + | 'PASSWORD_RESET_COMPLETE' + | 'ROLE_ASSIGNED' + | 'ROLE_REVOKED' + | 'ACCOUNT_LOCKED' + | 'ACCOUNT_UNLOCKED' + | 'USER_CREATED' + | 'USER_UPDATED' + | 'COLLABORATOR_CREATED' + | 'COLLABORATOR_UPDATED' + | 'HIERARCHY_CHANGED' + | 'ORGANIZATION_UPDATED'; + +/** + * Individual audit log entry + */ +export interface AuditLogDto { + id: string; + userId?: string; + username?: string; + action: AuditAction; + success: boolean; + ipAddress?: string; + userAgent?: string; + details?: Record; + createdAt: string; +} + +/** + * Paginated audit log response + */ +export interface AuditLogListResponseDto { + logs: AuditLogDto[]; + total: number; + page: number; + pageSize: number; + totalPages: number; +} + +/** + * Query parameters for audit logs + */ +export interface AuditLogQueryDto { + page?: number; + pageSize?: number; + userId?: string; + action?: AuditAction; + success?: boolean; + startDate?: string; + endDate?: string; + search?: string; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// SERVICE CLASS +// ═══════════════════════════════════════════════════════════════════════════ + +class AuditLogApiService { + /** + * Query audit logs with filters (supervisor+). + */ + async query(params: AuditLogQueryDto = {}): Promise { + return api.get('/admin/audit-logs', params); + } + + /** + * Get login history for specific user (supervisor+). + */ + async getUserLoginHistory(userId: string, limit = 10): Promise { + return api.get(`/admin/audit-logs/user/${userId}/login-history`, { limit }); + } + + /** + * Get current user's own login history. + */ + async getMyLoginHistory(limit = 10): Promise { + return api.get('/audit-logs/me/login-history', { limit }); + } + + /** + * Export audit logs to CSV (admin). + */ + async exportToCsv(params: AuditLogQueryDto = {}): Promise { + const response = await api.get('/admin/audit-logs/export', { ...params, format: 'csv' }); + return new Blob([response], { type: 'text/csv' }); + } + + /** + * Get audit log summary statistics (admin). + */ + async getStats(startDate?: string, endDate?: string): Promise<{ + totalEvents: number; + loginSuccessRate: number; + failedLoginAttempts: number; + actionBreakdown: Record; + }> { + return api.get('/admin/audit-logs/stats', { startDate, endDate }); + } +} + +export const auditLogService = new AuditLogApiService(); +export default auditLogService; diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/services/auth.service.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/auth.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..bfe17f3c7adec171783822967a418822a268b857 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/auth.service.ts @@ -0,0 +1,183 @@ +/** + * @fileoverview Authentication API Service + * @module api/services/auth + * + * Handles login, registration, token management, and password operations. + * + * @author Armand Richelet-Kleinberg + */ + +import { api, tokenUtils } from '../enhanced-client'; + +// ═══════════════════════════════════════════════════════════════════════════ +// TYPES +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * User data returned from authentication + */ +export interface AuthUser { + id: string; + username: string; + email: string; + roles: string[]; + collaboratorId?: string; +} + +/** + * Login response with tokens + */ +export interface LoginResponse { + user: AuthUser; + tokens: { + accessToken: string; + refreshToken: string; + }; +} + +/** + * Registration request data + */ +export interface RegisterRequest { + username: string; + email: string; + password: string; + firstName?: string; + lastName?: string; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// TOKEN STORAGE KEYS +// ═══════════════════════════════════════════════════════════════════════════ + +const USER_KEY = 'ark_user'; + +// ═══════════════════════════════════════════════════════════════════════════ +// SERVICE CLASS +// ═══════════════════════════════════════════════════════════════════════════ + +class AuthApiService { + /** + * Authenticate user with username/password. + */ + async login(username: string, password: string): Promise { + const response = await api.post('/auth/login', { username, password }); + this.setUserSession(response); + return response; + } + + /** + * Register new user account (guest role). + */ + async register(data: RegisterRequest): Promise { + await api.post('/auth/register', data); + } + + /** + * Logout and invalidate tokens. + */ + async logout(): Promise { + const refreshToken = tokenUtils.getRefreshToken(); + try { + if (refreshToken) { + await api.post('/auth/logout', { refreshToken }); + } + } finally { + this.clearUserSession(); + } + } + + /** + * Get current authenticated user info from server. + */ + async getCurrentUserFromServer(): Promise { + return api.get('/auth/me'); + } + + /** + * Request password reset email. + */ + async requestPasswordReset(email: string): Promise { + await api.post('/auth/password-reset/request', { email }); + } + + /** + * Complete password reset with token. + */ + async completePasswordReset(token: string, newPassword: string): Promise { + await api.post('/auth/password-reset/complete', { token, newPassword }); + } + + /** + * Change password for authenticated user. + */ + async changePassword(oldPassword: string, newPassword: string): Promise { + await api.post('/auth/change-password', { oldPassword, newPassword }); + } + + /** + * Confirm email with token. + */ + async confirmEmail(token: string): Promise { + await api.get(`/auth/confirm-email/${token}`); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SESSION MANAGEMENT + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Get current user from local storage. + */ + getCurrentUser(): AuthUser | null { + const json = localStorage.getItem(USER_KEY); + return json ? JSON.parse(json) : null; + } + + /** + * Check if user is authenticated. + */ + isAuthenticated(): boolean { + return tokenUtils.isAuthenticated(); + } + + /** + * Check if user has specific role. + */ + hasRole(role: string): boolean { + const user = this.getCurrentUser(); + return user?.roles?.includes(role) ?? false; + } + + /** + * Check if user is admin. + */ + isAdmin(): boolean { + return this.hasRole('admin'); + } + + /** + * Check if user is supervisor or admin. + */ + isSupervisorOrAdmin(): boolean { + return this.hasRole('admin') || this.hasRole('supervisor'); + } + + /** + * Store user session data. + */ + private setUserSession(response: LoginResponse): void { + tokenUtils.setTokens(response.tokens.accessToken, response.tokens.refreshToken); + localStorage.setItem(USER_KEY, JSON.stringify(response.user)); + } + + /** + * Clear user session data. + */ + private clearUserSession(): void { + tokenUtils.clearTokens(); + } +} + +export const authService = new AuthApiService(); +export default authService; diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/services/collaborator.service.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/collaborator.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..61d90f4dbc2e4493dca4fd23c044214d55ea4689 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/collaborator.service.ts @@ -0,0 +1,179 @@ +/** + * @fileoverview Collaborator API Service + * @module api/services/collaborator + * + * Handles team member operations and hierarchy. + * + * @author Armand Richelet-Kleinberg + */ + +import { api } from '../enhanced-client'; + +// ═══════════════════════════════════════════════════════════════════════════ +// TYPES +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Individual org chart node + */ +export interface OrgChartNodeDto { + id: string; + name: string; + position: string; + department?: string; + avatarUrl?: string; + email?: string; + children?: OrgChartNodeDto[]; +} + +/** + * Complete org chart data + */ +export interface OrgChartDto { + organizationName: string; + totalCount: number; + rootNodes: OrgChartNodeDto[]; +} + +/** + * Collaborator summary + */ +export interface CollaboratorSummaryDto { + id: string; + firstName: string; + lastName: string; + fullName: string; + position: string; + department?: string; + avatarUrl?: string; + email?: string; + isActive: boolean; +} + +/** + * Full collaborator details + */ +export interface CollaboratorDto extends CollaboratorSummaryDto { + bio?: string; + linkedinUrl?: string; + githubUrl?: string; + hireDate?: string; + organizationId: string; + reportsToId?: string; + userId?: string; + createdAt: string; + updatedAt: string; +} + +/** + * Collaborator with direct reports + */ +export interface CollaboratorWithReportsDto extends CollaboratorDto { + reportsTo?: CollaboratorSummaryDto; + directReports: CollaboratorSummaryDto[]; +} + +/** + * Create collaborator request + */ +export interface CreateCollaboratorDto { + firstName: string; + lastName: string; + email: string; + position: string; + department?: string; + bio?: string; + linkedinUrl?: string; + githubUrl?: string; + hireDate?: string; + reportsToId?: string; + isActive?: boolean; +} + +/** + * Update collaborator request + */ +export interface UpdateCollaboratorDto extends Partial { + avatarUrl?: string; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// SERVICE CLASS +// ═══════════════════════════════════════════════════════════════════════════ + +class CollaboratorApiService { + /** + * Get organization chart for public team page. + */ + async getOrgChart(): Promise { + return api.get('/team'); + } + + /** + * Get all collaborators (admin). + */ + async getAll(): Promise { + return api.get('/collaborators'); + } + + /** + * Get single collaborator details. + */ + async getById(id: string): Promise { + return api.get(`/team/${id}`); + } + + /** + * Create new collaborator (admin). + */ + async create(data: CreateCollaboratorDto): Promise { + return api.post('/collaborators', data); + } + + /** + * Update collaborator (admin). + */ + async update(id: string, data: UpdateCollaboratorDto): Promise { + return api.patch(`/collaborators/${id}`, data); + } + + /** + * Update collaborator hierarchy (admin). + */ + async updateHierarchy(id: string, reportsToId: string | null): Promise { + await api.patch(`/collaborators/${id}/hierarchy`, { reportsToId }); + } + + /** + * Deactivate collaborator (admin). + */ + async deactivate(id: string): Promise { + await api.patch(`/collaborators/${id}`, { isActive: false }); + } + + /** + * Activate collaborator (admin). + */ + async activate(id: string): Promise { + await api.patch(`/collaborators/${id}`, { isActive: true }); + } + + /** + * Remove/delete collaborator (admin). + */ + async remove(id: string): Promise { + await api.delete(`/collaborators/${id}`); + } + + /** + * Upload collaborator avatar. + */ + async uploadAvatar(id: string, file: File): Promise<{ avatarUrl: string }> { + const formData = new FormData(); + formData.append('avatar', file); + return api.upload<{ avatarUrl: string }>(`/collaborators/${id}/avatar`, formData); + } +} + +export const collaboratorService = new CollaboratorApiService(); +export default collaboratorService; diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/services/organization.service.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/organization.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..61988da74d758b0ac2fdf07770fcfaef8631c9fb --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/organization.service.ts @@ -0,0 +1,68 @@ +/** + * @fileoverview Organization API Service + * @module api/services/organization + * + * Handles organization CRUD operations. + * + * @author Armand Richelet-Kleinberg + */ + +import { api } from '../enhanced-client'; +import { + OrganizationDto, + OrganizationWithStatsDto, + UpdateOrganizationDto +} from '@arkalliance/startupcms-ai-share'; + +// Re-export types for consumers +export type { OrganizationDto, OrganizationWithStatsDto, UpdateOrganizationDto }; + +// ═══════════════════════════════════════════════════════════════════════════ +// SERVICE CLASS +// ═══════════════════════════════════════════════════════════════════════════ + +class OrganizationApiService { + /** + * Get organization details (public). + */ + async get(): Promise { + return api.get('/organization'); + } + + /** + * Get organization with statistics (admin). + */ + async getWithStats(): Promise { + return api.get('/organization/stats'); + } + + /** + * Update organization details (admin). + */ + async update(data: UpdateOrganizationDto): Promise { + return api.patch('/organization', data); + } + + /** + * Upload organization logo (admin). + */ + async uploadLogo(file: File): Promise<{ logoUrl: string }> { + const formData = new FormData(); + formData.append('logo', file); + return api.upload<{ logoUrl: string }>('/organization/logo', formData); + } + + /** + * Upload organization icon (admin). + */ + async uploadIcon(file: File): Promise<{ iconUrl: string }> { + const formData = new FormData(); + formData.append('icon', file); + return api.upload<{ iconUrl: string }>('/organization/icon', formData); + } +} + +export const organizationService = new OrganizationApiService(); +export default organizationService; + + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/services/user.service.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/user.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..6008a3f45f6c980d36c1df73b7190c252cea6a9e --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/services/user.service.ts @@ -0,0 +1,135 @@ +/** + * @fileoverview User Management API Service + * @module api/services/user + * + * Handles admin user and role operations. + * + * @author Armand Richelet-Kleinberg + */ + +import { api } from '../enhanced-client'; +import { Role } from '@arkalliance/startupcms-ai-share'; + +// Re-export Role for convenience +export { Role } from '@arkalliance/startupcms-ai-share'; + +// ═══════════════════════════════════════════════════════════════════════════ +// TYPES +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * User list item + */ +export interface UserListDto { + id: string; + username: string; + email: string; + avatarUrl?: string; + roles: Role[]; + isActive: boolean; + emailConfirmed: boolean; + lockedUntil?: string; + collaboratorName?: string; + lastLoginAt?: string; +} + +/** + * Detailed user info + */ +export interface UserDetailDto extends UserListDto { + createdAt: string; + updatedAt: string; + failedLoginAttempts: number; + collaborator?: { + id: string; + fullName: string; + position: string; + department?: string; + }; + recentActivity: { + action: string; + timestamp: string; + success: boolean; + }[]; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// SERVICE CLASS +// ═══════════════════════════════════════════════════════════════════════════ + +class UserApiService { + /** + * Get all users (supervisor+). + */ + async getAll(): Promise { + return api.get('/admin/users'); + } + + /** + * Get user details (supervisor+). + */ + async getById(id: string): Promise { + return api.get(`/admin/users/${id}`); + } + + /** + * Assign role to user (supervisor+). + */ + async assignRole(userId: string, role: Role): Promise { + await api.post(`/admin/users/${userId}/roles`, { role }); + } + + /** + * Revoke role from user (supervisor+). + */ + async revokeRole(userId: string, role: Role): Promise { + await api.delete(`/admin/users/${userId}/roles/${role}`); + } + + /** + * Unlock user account (admin). + */ + async unlockAccount(userId: string, resetFailedAttempts = true): Promise { + await api.post(`/admin/users/${userId}/unlock`, { resetFailedAttempts }); + } + + /** + * Lock user account (admin). + */ + async lockAccount(userId: string, duration?: number): Promise { + await api.post(`/admin/users/${userId}/lock`, { duration }); + } + + /** + * Deactivate user (admin). + */ + async deactivate(userId: string): Promise { + await api.patch(`/admin/users/${userId}`, { isActive: false }); + } + + /** + * Activate user (admin). + */ + async activate(userId: string): Promise { + await api.patch(`/admin/users/${userId}`, { isActive: true }); + } + + /** + * Link user to collaborator (admin). + */ + async linkToCollaborator(userId: string, collaboratorId: string): Promise { + await api.post(`/admin/users/${userId}/link-collaborator`, { collaboratorId }); + } + + /** + * Update user avatar URL (admin). + */ + async updateAvatar(userId: string, avatarUrl: string): Promise<{ id: string; username: string; avatarUrl: string }> { + return api.post(`/admin/users/${userId}/avatar`, { avatarUrl }); + } +} + +export const userService = new UserApiService(); +export default userService; + + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/api/theme.api.ts b/Ark.Alliance.StartupCms.Ai.UI/src/api/theme.api.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b64ce158decd83138ca534a7c9dbfcba9156056 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/api/theme.api.ts @@ -0,0 +1,81 @@ +/** + * @fileoverview Theme API Client + * Functions for fetching theme data from the backend API. + * + * @module api/theme.api + * @author Armand Richelet-Kleinberg + */ + +import { apiClient } from './client'; + +// ═══════════════════════════════════════════════════════════════════════════ +// TYPES +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Theme list item (without CSS content for performance). + */ +export interface ThemeListItem { + id: number; + name: string; + slug: string; + description?: string; + previewColor?: string; + icon?: string; + isDefault: boolean; + order: number; +} + +/** + * Theme detail with full CSS content. + */ +export interface ThemeDetail extends ThemeListItem { + cssContent: string; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// API FUNCTIONS +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Fetches all active themes (without CSS content). + * + * @returns Promise resolving to array of theme list items + */ +export async function fetchThemes(): Promise { + try { + return await apiClient.get('/themes'); + } catch (error) { + console.warn('Failed to fetch themes from backend:', error); + return []; + } +} + +/** + * Fetches a specific theme by slug with full CSS content. + * + * @param slug - Theme slug identifier + * @returns Promise resolving to theme detail or null if not found + */ +export async function fetchThemeBySlug(slug: string): Promise { + try { + return await apiClient.get(`/themes/${slug}`); + } catch (error) { + console.warn(`Failed to fetch theme '${slug}' from backend:`, error); + return null; + } +} + +/** + * Fetches the default theme with full CSS content. + * + * @returns Promise resolving to default theme or null if not configured + */ +export async function fetchDefaultTheme(): Promise { + try { + return await apiClient.get('/themes/default'); + } catch (error) { + console.warn('Failed to fetch default theme from backend:', error); + return null; + } +} diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/common/constants/design-tokens.ts b/Ark.Alliance.StartupCms.Ai.UI/src/common/constants/design-tokens.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee2ae89c3944f7f085b88419bc1cc5186647e58b --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/common/constants/design-tokens.ts @@ -0,0 +1,255 @@ +/** + * @fileoverview Design System Tokens + * Single source of truth for all design values. + * Import this in components for consistent styling. + * + * @author Armand Richelet-Kleinberg + */ + +// ============================================ +// Colors +// ============================================ + +export const COLORS = { + // Primary - Blue gradient base + primary: { + 50: '#eff6ff', + 100: '#dbeafe', + 200: '#bfdbfe', + 300: '#93c5fd', + 400: '#60a5fa', + 500: '#3b82f6', + 600: '#2563eb', + 700: '#1d4ed8', + 800: '#1e40af', + 900: '#1e3a8a', + }, + // Secondary - Purple accent + secondary: { + 50: '#faf5ff', + 100: '#f3e8ff', + 200: '#e9d5ff', + 300: '#d8b4fe', + 400: '#c084fc', + 500: '#a855f7', + 600: '#9333ea', + 700: '#7c3aed', + 800: '#6b21a8', + 900: '#581c87', + }, + // Neutral - Slate for backgrounds/text + neutral: { + 50: '#f8fafc', + 100: '#f1f5f9', + 200: '#e2e8f0', + 300: '#cbd5e1', + 400: '#94a3b8', + 500: '#64748b', + 600: '#475569', + 700: '#334155', + 800: '#1e293b', + 900: '#0f172a', + 950: '#020617', + }, + // Semantic colors + success: { + light: '#86efac', + DEFAULT: '#22c55e', + dark: '#16a34a', + }, + error: { + light: '#fca5a5', + DEFAULT: '#ef4444', + dark: '#dc2626', + }, + warning: { + light: '#fcd34d', + DEFAULT: '#f59e0b', + dark: '#d97706', + }, + info: { + light: '#7dd3fc', + DEFAULT: '#0ea5e9', + dark: '#0284c7', + }, +} as const; + +// ============================================ +// Spacing (rem-based for accessibility) +// ============================================ + +export const SPACING = { + 0: '0', + px: '1px', + 0.5: '0.125rem', // 2px + 1: '0.25rem', // 4px + 1.5: '0.375rem', // 6px + 2: '0.5rem', // 8px + 2.5: '0.625rem', // 10px + 3: '0.75rem', // 12px + 3.5: '0.875rem', // 14px + 4: '1rem', // 16px + 5: '1.25rem', // 20px + 6: '1.5rem', // 24px + 7: '1.75rem', // 28px + 8: '2rem', // 32px + 9: '2.25rem', // 36px + 10: '2.5rem', // 40px + 11: '2.75rem', // 44px + 12: '3rem', // 48px + 14: '3.5rem', // 56px + 16: '4rem', // 64px + 20: '5rem', // 80px + 24: '6rem', // 96px + 28: '7rem', // 112px + 32: '8rem', // 128px +} as const; + +// ============================================ +// Typography +// ============================================ + +export const TYPOGRAPHY = { + fontFamily: { + sans: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + mono: '"JetBrains Mono", "Fira Code", Consolas, monospace', + display: '"Outfit", "Inter", sans-serif', + }, + fontSize: { + xs: ['0.75rem', { lineHeight: '1rem' }], // 12px + sm: ['0.875rem', { lineHeight: '1.25rem' }], // 14px + base: ['1rem', { lineHeight: '1.5rem' }], // 16px + lg: ['1.125rem', { lineHeight: '1.75rem' }], // 18px + xl: ['1.25rem', { lineHeight: '1.75rem' }], // 20px + '2xl': ['1.5rem', { lineHeight: '2rem' }], // 24px + '3xl': ['1.875rem', { lineHeight: '2.25rem' }], // 30px + '4xl': ['2.25rem', { lineHeight: '2.5rem' }], // 36px + '5xl': ['3rem', { lineHeight: '1.2' }], // 48px + '6xl': ['3.75rem', { lineHeight: '1.1' }], // 60px + '7xl': ['4.5rem', { lineHeight: '1.1' }], // 72px + }, + fontWeight: { + light: 300, + normal: 400, + medium: 500, + semibold: 600, + bold: 700, + extrabold: 800, + }, +} as const; + +// ============================================ +// Border Radius +// ============================================ + +export const RADII = { + none: '0', + sm: '0.25rem', // 4px + DEFAULT: '0.375rem', // 6px + md: '0.5rem', // 8px + lg: '0.75rem', // 12px + xl: '1rem', // 16px + '2xl': '1.5rem', // 24px + full: '9999px', +} as const; + +// ============================================ +// Shadows +// ============================================ + +export const SHADOWS = { + sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)', + DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', + md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', + lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', + xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)', + '2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)', + inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)', + // Glow effects + glow: { + blue: '0 0 20px rgba(59, 130, 246, 0.5)', + purple: '0 0 20px rgba(139, 92, 246, 0.5)', + success: '0 0 20px rgba(34, 197, 94, 0.5)', + }, +} as const; + +// ============================================ +// Breakpoints (Mobile First) +// ============================================ + +export const BREAKPOINTS = { + xs: '320px', + sm: '480px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', +} as const; + +// ============================================ +// Z-Index Scale +// ============================================ + +export const Z_INDEX = { + behind: -1, + base: 0, + dropdown: 10, + sticky: 20, + fixed: 30, + overlay: 40, + modal: 50, + popover: 60, + tooltip: 70, + toast: 80, + skipLink: 100, +} as const; + +// ============================================ +// Transitions +// ============================================ + +export const TRANSITIONS = { + duration: { + fast: '150ms', + DEFAULT: '200ms', + slow: '300ms', + slower: '500ms', + }, + easing: { + linear: 'linear', + in: 'cubic-bezier(0.4, 0, 1, 1)', + out: 'cubic-bezier(0, 0, 0.2, 1)', + inOut: 'cubic-bezier(0.4, 0, 0.2, 1)', + bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', + }, +} as const; + +// ============================================ +// Gradients +// ============================================ + +export const GRADIENTS = { + primary: 'linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%)', + secondary: 'linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%)', + dark: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)', + glow: 'linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%)', + shine: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent)', +} as const; + +// ============================================ +// Export all tokens +// ============================================ + +export const DesignTokens = { + colors: COLORS, + spacing: SPACING, + typography: TYPOGRAPHY, + radii: RADII, + shadows: SHADOWS, + breakpoints: BREAKPOINTS, + zIndex: Z_INDEX, + transitions: TRANSITIONS, + gradients: GRADIENTS, +} as const; + +export default DesignTokens; diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/common/constants/index.ts b/Ark.Alliance.StartupCms.Ai.UI/src/common/constants/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..eb92753269e992d71d0b52334dc499c7e1a447ce --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/common/constants/index.ts @@ -0,0 +1,7 @@ +/** + * @fileoverview Common Constants Index + * Exports all shared constants and tokens. + */ + +export * from './design-tokens'; +export { default as DesignTokens } from './design-tokens'; diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/common/index.ts b/Ark.Alliance.StartupCms.Ai.UI/src/common/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0c144f6082d189009133b1ae1af14e96077d2bfd --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/common/index.ts @@ -0,0 +1,5 @@ +/** + * @fileoverview Common Module Index + */ + +export * from './constants'; diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/AccomplishmentsTable/AccomplishmentsTable.model.ts b/Ark.Alliance.StartupCms.Ai.UI/src/components/AccomplishmentsTable/AccomplishmentsTable.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7d13669473c7524d72fd9c7fb2f6ab03968281f --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/AccomplishmentsTable/AccomplishmentsTable.model.ts @@ -0,0 +1,167 @@ +/** + * @fileoverview AccomplishmentsTable ViewModel + * @module components/AccomplishmentsTable/AccomplishmentsTable.model + * + * MVVM Model layer for the AccomplishmentsTable component. + * Handles data fetching, state management, and business logic. + * + * @author Armand Richelet-Kleinberg + */ + +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { + PublicTaskDto, + AccomplishmentsSummaryDto, + TaskStatus +} from '@arkalliance/startupcms-ai-share'; + +// ═══════════════════════════════════════════════════════════════════════════ +// TYPES +// ═══════════════════════════════════════════════════════════════════════════ + +export interface AccomplishmentsModel { + /** Public tasks to display */ + tasks: PublicTaskDto[]; + /** Summary statistics */ + summary: AccomplishmentsSummaryDto | null; + /** Loading state */ + isLoading: boolean; + /** Error message if any */ + error: string | null; + /** Refetch data */ + refetch: () => Promise; +} + +export interface UseAccomplishmentsOptions { + /** Collaborator ID to fetch tasks for */ + collaboratorId?: string; + /** Pre-loaded tasks (skip fetching) */ + initialTasks?: PublicTaskDto[]; + /** Pre-loaded summary (skip calculation) */ + initialSummary?: AccomplishmentsSummaryDto; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// HELPER FUNCTIONS +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Calculate summary statistics from tasks. + */ +export function calculateSummary(tasks: PublicTaskDto[]): AccomplishmentsSummaryDto { + const ratedTasks = tasks.filter(t => t.ratingCount > 0); + const totalRating = ratedTasks.reduce((sum, t) => sum + t.averageRating, 0); + + const hoursSaved = tasks + .filter(t => t.deadlinePerformanceHours != null && t.deadlinePerformanceHours > 0) + .reduce((sum, t) => sum + (t.deadlinePerformanceHours || 0), 0); + + return { + totalAchieved: tasks.filter(t => t.status === TaskStatus.Achieved).length, + overallAverageRating: ratedTasks.length > 0 ? totalRating / ratedTasks.length : 0, + totalHoursSaved: hoursSaved, + lessonsLearnedCount: tasks.filter(t => t.lessonLearned).length, + highlightedCount: tasks.filter(t => t.isHighlighted).length + }; +} + +/** + * Format deadline performance for display. + * @returns Object with display string and type for styling + */ +export function formatDeadlinePerformance(hours?: number): { + display: string; + type: 'ahead' | 'ontime' | 'overrun' | 'none'; +} { + if (hours === undefined || hours === null) { + return { display: '-', type: 'none' }; + } + + if (hours > 0) { + return { display: `+${hours}h`, type: 'ahead' }; + } + + if (hours === 0) { + return { display: '✓', type: 'ontime' }; + } + + return { display: `${hours}h`, type: 'overrun' }; +} + +/** + * Format rating for display with one decimal place. + */ +export function formatRating(rating: number): string { + return rating.toFixed(1); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// VIEWMODEL HOOK +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * AccomplishmentsTable ViewModel Hook + * + * Manages state and data fetching for the AccomplishmentsTable component. + * Can accept pre-loaded data or fetch from API. + */ +export function useAccomplishmentsModel( + options: UseAccomplishmentsOptions = {} +): AccomplishmentsModel { + const { collaboratorId, initialTasks, initialSummary } = options; + + const [tasks, setTasks] = useState(initialTasks || []); + const [isLoading, setIsLoading] = useState(!initialTasks); + const [error, setError] = useState(null); + + // Calculate summary from tasks (or use provided) + const summary = useMemo(() => { + if (initialSummary) return initialSummary; + if (tasks.length === 0) return null; + return calculateSummary(tasks); + }, [tasks, initialSummary]); + + // Fetch tasks from API + const fetchTasks = useCallback(async () => { + if (!collaboratorId) { + setIsLoading(false); + return; + } + + try { + setIsLoading(true); + setError(null); + + // TODO: Replace with actual API call when endpoint is ready + // const response = await taskService.getPublicTasks(collaboratorId); + // setTasks(response); + + // For now, use empty array (component will show "no accomplishments") + setTasks([]); + } catch (err: any) { + setError(err.message || 'Failed to load accomplishments'); + console.error('AccomplishmentsTable fetch error:', err); + } finally { + setIsLoading(false); + } + }, [collaboratorId]); + + // Initial fetch (if no initial data provided) + useEffect(() => { + if (!initialTasks && collaboratorId) { + fetchTasks(); + } + }, [fetchTasks, initialTasks, collaboratorId]); + + return { + tasks, + summary, + isLoading, + error, + refetch: fetchTasks + }; +} + +export default useAccomplishmentsModel; + + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/AccomplishmentsTable/AccomplishmentsTable.styles.css b/Ark.Alliance.StartupCms.Ai.UI/src/components/AccomplishmentsTable/AccomplishmentsTable.styles.css new file mode 100644 index 0000000000000000000000000000000000000000..a905a537fa6f9002f387a5ae8ea1a21eed28d078 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/AccomplishmentsTable/AccomplishmentsTable.styles.css @@ -0,0 +1,293 @@ +/** + * AccomplishmentsTable Styles + * Recognition-focused styling for public achievements display. + * + * Color Philosophy: + * - Green (#10b981): Achievements, ahead of schedule (celebration) + * - Amber (#f59e0b): Lessons learned (growth, not failure) + * - Blue (#3b82f6): Ratings, neutral info + * - Never use red for mistakes (psychological safety) + */ + +/* ═══════════════════════════════════════════════════════════════════════════ + CONTAINER + ═══════════════════════════════════════════════════════════════════════════ */ + +.accomplishments-container { + width: 100%; +} + +.accomplishments-empty { + text-align: center; + padding: 2rem; + color: rgba(255, 255, 255, 0.6); + font-style: italic; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + SUMMARY STATISTICS + ═══════════════════════════════════════════════════════════════════════════ */ + +.accomplishments-summary { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + margin-bottom: 1.5rem; + padding: 1.25rem; + background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(59, 130, 246, 0.1) 100%); + border-radius: 12px; + border: 1px solid rgba(16, 185, 129, 0.2); +} + +.summary-stat { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(0, 0, 0, 0.2); + border-radius: 8px; +} + +.summary-stat .stat-icon { + opacity: 0.8; +} + +.summary-stat .stat-icon.achieved { + color: #10b981; +} + +.summary-stat .stat-icon.rating { + color: #f59e0b; + fill: #f59e0b; +} + +.summary-stat .stat-icon.time { + color: #3b82f6; +} + +.summary-stat .stat-icon.lesson { + color: #f59e0b; +} + +.summary-stat .stat-value { + font-size: 1.25rem; + font-weight: 700; + color: #ffffff; +} + +.summary-stat .stat-label { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.6); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + TABLE + ═══════════════════════════════════════════════════════════════════════════ */ + +.accomplishments-table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +.accomplishments-table thead th { + text-align: left; + padding: 0.75rem 1rem; + font-weight: 600; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(255, 255, 255, 0.6); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.accomplishments-table tbody tr { + transition: background-color 0.2s ease; +} + +.accomplishments-table tbody tr:hover { + background-color: rgba(255, 255, 255, 0.05); +} + +.accomplishments-table tbody tr.highlighted { + background: linear-gradient(90deg, rgba(16, 185, 129, 0.1) 0%, transparent 100%); +} + +.accomplishments-table tbody td { + padding: 1rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + vertical-align: middle; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + TABLE CELLS + ═══════════════════════════════════════════════════════════════════════════ */ + +.task-title { + display: flex; + align-items: flex-start; + gap: 0.5rem; +} + +.task-title .highlight-icon { + color: #10b981; + flex-shrink: 0; + margin-top: 2px; +} + +.task-title strong { + color: #ffffff; + display: block; +} + +.task-title .lesson-title { + display: block; + font-size: 0.8rem; + color: rgba(245, 158, 11, 0.8); + font-style: italic; + margin-top: 0.25rem; +} + +.task-role, +.task-project { + color: rgba(255, 255, 255, 0.7); +} + +.no-rating, +.deadline-na { + color: rgba(255, 255, 255, 0.3); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + STAR RATING + ═══════════════════════════════════════════════════════════════════════════ */ + +.accomplishments-rating { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.accomplishments-stars { + display: flex; + gap: 2px; +} + +.accomplishments-stars .star-filled { + color: #f59e0b; +} + +.accomplishments-stars .star-empty { + color: rgba(255, 255, 255, 0.2); +} + +.rating-value { + font-weight: 600; + color: #ffffff; +} + +.rating-count { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.5); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + DEADLINE PERFORMANCE BADGES + ═══════════════════════════════════════════════════════════════════════════ */ + +.deadline-badge { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 600; +} + +.deadline-badge.ahead { + background: rgba(16, 185, 129, 0.2); + color: #10b981; + border: 1px solid rgba(16, 185, 129, 0.3); +} + +.deadline-badge.ontime { + background: rgba(59, 130, 246, 0.2); + color: #3b82f6; + border: 1px solid rgba(59, 130, 246, 0.3); +} + +.deadline-badge.overrun { + background: rgba(245, 158, 11, 0.15); + color: #f59e0b; + border: 1px solid rgba(245, 158, 11, 0.3); +} + +.lesson-badge { + display: inline-flex; + align-items: center; + margin-left: 0.25rem; + padding: 2px; + background: rgba(245, 158, 11, 0.3); + border-radius: 50%; +} + +.lesson-badge svg { + color: #f59e0b; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + STATUS BADGES + ═══════════════════════════════════════════════════════════════════════════ */ + +.status-badge { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.35rem 0.75rem; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.status-badge.achieved { + background: rgba(16, 185, 129, 0.2); + color: #10b981; + border: 1px solid rgba(16, 185, 129, 0.3); +} + +.status-badge.lesson { + background: rgba(245, 158, 11, 0.15); + color: #f59e0b; + border: 1px solid rgba(245, 158, 11, 0.3); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + RESPONSIVE + ═══════════════════════════════════════════════════════════════════════════ */ + +@media (max-width: 768px) { + .accomplishments-summary { + flex-direction: column; + gap: 0.75rem; + } + + .accomplishments-table { + font-size: 0.8rem; + } + + .accomplishments-table thead th, + .accomplishments-table tbody td { + padding: 0.5rem; + } + + /* Hide less critical columns on mobile */ + .accomplishments-table .task-role, + .accomplishments-table .task-project { + display: none; + } +} \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/AccomplishmentsTable/AccomplishmentsTable.tsx b/Ark.Alliance.StartupCms.Ai.UI/src/components/AccomplishmentsTable/AccomplishmentsTable.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1cee483faab7050f1496a44a1d026d7b388f4d41 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/AccomplishmentsTable/AccomplishmentsTable.tsx @@ -0,0 +1,264 @@ +/** + * @fileoverview AccomplishmentsTable View Component + * @module components/AccomplishmentsTable/AccomplishmentsTable + * + * MVVM View layer - Pure UI rendering. + * Uses AccomplishmentsTable.model.ts for data and business logic. + * + * Philosophy: + * - Celebrates accomplishments and efficiency (emulation) + * - Constructive framing for lessons learned (growth mindset) + * - Visual badges emphasize recognition, never punishment + * + * @author Armand Richelet-Kleinberg + */ + +import React from 'react'; +import { Star, Clock, Award, Lightbulb, TrendingUp, CheckCircle, RefreshCw, AlertCircle } from 'lucide-react'; +import { PublicTaskDto, TaskStatus, AccomplishmentsSummaryDto } from '@arkalliance/startupcms-ai-share'; +import { + useAccomplishmentsModel, + UseAccomplishmentsOptions, + formatDeadlinePerformance, + formatRating +} from './AccomplishmentsTable.model'; +import './AccomplishmentsTable.styles.css'; + +// ═══════════════════════════════════════════════════════════════════════════ +// TYPES +// ═══════════════════════════════════════════════════════════════════════════ + +interface AccomplishmentsTableProps extends UseAccomplishmentsOptions { + /** Whether to show summary statistics */ + showSummary?: boolean; + /** Additional CSS class */ + className?: string; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// PURE HELPER COMPONENTS (View-only) +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Star rating display. + */ +const StarRating: React.FC<{ rating: number; count?: number }> = ({ rating, count }) => ( +
+
+ {[1, 2, 3, 4, 5].map(star => ( + + ))} +
+ {formatRating(rating)} + {count !== undefined && count > 0 && ( + ({count}) + )} +
+); + +/** + * Deadline performance badge. + */ +const DeadlinePerformance: React.FC<{ hours?: number; lessonLearned?: boolean }> = ({ + hours, + lessonLearned +}) => { + const { display, type } = formatDeadlinePerformance(hours); + + if (type === 'none') return -; + + return ( +
+ {type === 'ahead' && } + {type === 'ontime' && } + {display} + {type === 'overrun' && lessonLearned && ( + + + + )} +
+ ); +}; + +/** + * Status badge. + */ +const StatusBadge: React.FC<{ status: TaskStatus; lessonLearned?: boolean }> = ({ + status, + lessonLearned +}) => { + if (status === TaskStatus.Achieved) { + return ( + + + Réalisé + + ); + } + + if (status === TaskStatus.Mistake && lessonLearned) { + return ( + + + Leçon précieuse + + ); + } + + return null; +}; + +/** + * Summary statistics display. + */ +const SummaryStats: React.FC<{ summary: AccomplishmentsSummaryDto }> = ({ summary }) => ( +
+
+ + {summary.totalAchieved} + Réalisations +
+ {summary.overallAverageRating > 0 && ( +
+ + {formatRating(summary.overallAverageRating)} + Note moyenne +
+ )} + {summary.totalHoursSaved > 0 && ( +
+ + +{summary.totalHoursSaved}h + Heures d'avance +
+ )} + {summary.lessonsLearnedCount > 0 && ( +
+ + {summary.lessonsLearnedCount} + Leçons précieuses +
+ )} +
+); + +/** + * Task row in the table. + */ +const TaskRow: React.FC<{ task: PublicTaskDto }> = ({ task }) => ( + + + {task.isHighlighted && } +
+ {task.title} + {task.lessonLearned && task.lessonTitle && ( + {task.lessonTitle} + )} +
+ + {task.role || '-'} + {task.projectName || '-'} + + {task.ratingCount > 0 ? ( + + ) : ( + - + )} + + + + + + + + +); + +// ═══════════════════════════════════════════════════════════════════════════ +// MAIN VIEW COMPONENT +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * AccomplishmentsTable Component (MVVM View) + * + * Pure view component that uses useAccomplishmentsModel for data. + * Displays public achievements with recognition-focused presentation. + */ +export const AccomplishmentsTable: React.FC = ({ + collaboratorId, + initialTasks, + initialSummary, + showSummary = true, + className = '' +}) => { + // ViewModel hook - handles all data and logic + const vm = useAccomplishmentsModel({ collaboratorId, initialTasks, initialSummary }); + + // Loading state + if (vm.isLoading) { + return ( +
+ +

Loading accomplishments...

+
+ ); + } + + // Error state + if (vm.error) { + return ( +
+ +

{vm.error}

+ +
+ ); + } + + // Empty state + if (vm.tasks.length === 0) { + return ( +
+

No accomplishments to display yet.

+
+ ); + } + + // Main render + return ( +
+ {showSummary && vm.summary && } + + + + + + + + + + + + + + {vm.tasks.map(task => ( + + ))} + +
DescriptionRôleProjetÉvaluationPerformance DélaisStatut
+
+ ); +}; + +export default AccomplishmentsTable; + + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/Carousel/Carousel.tsx b/Ark.Alliance.StartupCms.Ai.UI/src/components/Carousel/Carousel.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9fc7cd98b67b5a84f07dc2982925a242084008b2 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/Carousel/Carousel.tsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react'; +import { ProjectFeatureDto } from '@arkalliance/startupcms-ai-share/dtos/project.dto'; +import { Icon } from '../generic/Icon'; + +interface CarouselProps { + items: ProjectFeatureDto[]; +} + +export const Carousel: React.FC = ({ items }) => { + const [activeIndex, setActiveIndex] = useState(0); + + const next = () => setActiveIndex((prev) => (prev + 1) % items.length); + const prev = () => setActiveIndex((prev) => (prev - 1 + items.length) % items.length); + + if (items.length === 0) return null; + + const activeItem = items[activeIndex]; + + return ( +
+ {/* Main Display */} +
+
+
+ +
+
+
+ +
+

{activeItem.title}

+
+

{activeItem.description}

+
+ + {/* Controls */} + + +
+ + {/* Thumbnails */} +
+ {items.map((item, idx) => ( + + ))} +
+
+ ); +}; + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/CarouselV2/CarouselV2.model.ts b/Ark.Alliance.StartupCms.Ai.UI/src/components/CarouselV2/CarouselV2.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c69a2cc63c24dd655be618b9516dc286d137600 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/CarouselV2/CarouselV2.model.ts @@ -0,0 +1,240 @@ +/** + * @fileoverview CarouselV2 ViewModel + * Manages carousel state, autoplay, navigation, and touch gestures. + * + * @author Armand Richelet-Kleinberg + */ + +import { useState, useCallback, useRef, useEffect } from 'react'; +import { CarouselSlideDto } from '@arkalliance/startupcms-ai-share'; + +/** + * Re-export CarouselSlide from shared library for backward compatibility. + * Use CarouselSlideDto from @arkalliance/startupcms-ai-share in new code. + */ +export type CarouselSlide = CarouselSlideDto; + +/** + * CarouselV2 ViewModel configuration + */ +export interface CarouselV2Config { + /** Slides data */ + slides: CarouselSlide[]; + /** Autoplay interval in ms (0 = disabled) */ + autoplayInterval?: number; + /** Pause autoplay on hover/focus */ + pauseOnInteraction?: boolean; + /** Starting slide index */ + initialIndex?: number; +} + +/** + * CarouselV2 ViewModel state and actions + */ +export interface CarouselV2Model { + /** Current slide index */ + currentIndex: number; + /** Total number of slides */ + totalSlides: number; + /** Is autoplay currently running */ + isPlaying: boolean; + /** Is carousel being interacted with */ + isInteracting: boolean; + /** Direction of last navigation (for animation) */ + direction: 'left' | 'right'; + /** Current slide data */ + currentSlide: CarouselSlide | undefined; + + // Actions + goToSlide: (index: number) => void; + goToNext: () => void; + goToPrev: () => void; + togglePlay: () => void; + pause: () => void; + resume: () => void; + handleKeyDown: (e: React.KeyboardEvent) => void; + handleTouchStart: (e: React.TouchEvent) => void; + handleTouchEnd: (e: React.TouchEvent) => void; + handleMouseEnter: () => void; + handleMouseLeave: () => void; + handleFocus: () => void; + handleBlur: () => void; +} + +/** + * CarouselV2 ViewModel hook + * Encapsulates all carousel logic including autoplay and gestures. + */ +export const useCarouselV2Model = (config: CarouselV2Config): CarouselV2Model => { + const { + slides, + autoplayInterval = 5000, + pauseOnInteraction = true, + initialIndex = 0, + } = config; + + const [currentIndex, setCurrentIndex] = useState(initialIndex); + const [isPlaying, setIsPlaying] = useState(autoplayInterval > 0); + const [isInteracting, setIsInteracting] = useState(false); + const [direction, setDirection] = useState<'left' | 'right'>('right'); + + const touchStartX = useRef(0); + const autoplayRef = useRef | null>(null); + + const totalSlides = slides.length; + + // Clear autoplay timeout + const clearAutoplay = useCallback(() => { + if (autoplayRef.current) { + clearTimeout(autoplayRef.current); + autoplayRef.current = null; + } + }, []); + + // Schedule next autoplay + const scheduleAutoplay = useCallback(() => { + if (isPlaying && autoplayInterval > 0 && !isInteracting) { + clearAutoplay(); + autoplayRef.current = setTimeout(() => { + setDirection('right'); + setCurrentIndex(prev => (prev + 1) % totalSlides); + }, autoplayInterval); + } + }, [isPlaying, autoplayInterval, isInteracting, totalSlides, clearAutoplay]); + + // Handle autoplay + useEffect(() => { + scheduleAutoplay(); + return clearAutoplay; + }, [scheduleAutoplay, clearAutoplay, currentIndex]); + + // Navigation actions + const goToSlide = useCallback((index: number) => { + if (index < 0 || index >= totalSlides) return; + setDirection(index > currentIndex ? 'right' : 'left'); + setCurrentIndex(index); + }, [currentIndex, totalSlides]); + + const goToNext = useCallback(() => { + setDirection('right'); + setCurrentIndex(prev => (prev + 1) % totalSlides); + }, [totalSlides]); + + const goToPrev = useCallback(() => { + setDirection('left'); + setCurrentIndex(prev => (prev - 1 + totalSlides) % totalSlides); + }, [totalSlides]); + + // Playback controls + const togglePlay = useCallback(() => { + setIsPlaying(prev => !prev); + }, []); + + const pause = useCallback(() => { + setIsPlaying(false); + clearAutoplay(); + }, [clearAutoplay]); + + const resume = useCallback(() => { + setIsPlaying(true); + }, []); + + // Keyboard navigation + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + switch (e.key) { + case 'ArrowLeft': + e.preventDefault(); + goToPrev(); + break; + case 'ArrowRight': + e.preventDefault(); + goToNext(); + break; + case ' ': + e.preventDefault(); + togglePlay(); + break; + case 'Escape': + e.preventDefault(); + pause(); + break; + } + }, [goToNext, goToPrev, togglePlay, pause]); + + // Touch gestures + const handleTouchStart = useCallback((e: React.TouchEvent) => { + touchStartX.current = e.touches[0].clientX; + if (pauseOnInteraction) { + setIsInteracting(true); + } + }, [pauseOnInteraction]); + + const handleTouchEnd = useCallback((e: React.TouchEvent) => { + const touchEndX = e.changedTouches[0].clientX; + const diff = touchStartX.current - touchEndX; + const threshold = 50; + + if (Math.abs(diff) > threshold) { + if (diff > 0) { + goToNext(); + } else { + goToPrev(); + } + } + + if (pauseOnInteraction) { + setIsInteracting(false); + } + }, [goToNext, goToPrev, pauseOnInteraction]); + + // Hover/focus interactions + const handleMouseEnter = useCallback(() => { + if (pauseOnInteraction) { + setIsInteracting(true); + } + }, [pauseOnInteraction]); + + const handleMouseLeave = useCallback(() => { + if (pauseOnInteraction) { + setIsInteracting(false); + } + }, [pauseOnInteraction]); + + const handleFocus = useCallback(() => { + if (pauseOnInteraction) { + setIsInteracting(true); + } + }, [pauseOnInteraction]); + + const handleBlur = useCallback(() => { + if (pauseOnInteraction) { + setIsInteracting(false); + } + }, [pauseOnInteraction]); + + return { + currentIndex, + totalSlides, + isPlaying, + isInteracting, + direction, + currentSlide: slides[currentIndex], + goToSlide, + goToNext, + goToPrev, + togglePlay, + pause, + resume, + handleKeyDown, + handleTouchStart, + handleTouchEnd, + handleMouseEnter, + handleMouseLeave, + handleFocus, + handleBlur, + }; +}; + +export default useCarouselV2Model; + + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/CarouselV2/CarouselV2.styles.css b/Ark.Alliance.StartupCms.Ai.UI/src/components/CarouselV2/CarouselV2.styles.css new file mode 100644 index 0000000000000000000000000000000000000000..fcd83ee4ad7098391319c665fc5ae4556e127fc3 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/CarouselV2/CarouselV2.styles.css @@ -0,0 +1,512 @@ +/** + * CarouselV2 Styles + * Premium carousel with smooth animations and responsive design. + * + * @author Armand Richelet-Kleinberg + */ + +/* ============================================ + Carousel Container + ============================================ */ + +.carousel { + position: relative; + width: 100%; + height: 70vh; + min-height: 400px; + max-height: 800px; + overflow: hidden; + border-radius: var(--radius-2xl); + background: var(--bg-secondary); +} + +@media (max-width: 768px) { + .carousel { + height: 60vh; + min-height: 350px; + border-radius: var(--radius-xl); + } +} + +/* ============================================ + Slides Container + ============================================ */ + +.carousel-slides { + position: relative; + width: 100%; + height: 100%; +} + +.carousel-slide { + position: absolute; + inset: 0; + opacity: 0; + visibility: hidden; + transition: opacity var(--duration-slower) var(--ease-out), + transform var(--duration-slower) var(--ease-out); +} + +.carousel-slide.active { + opacity: 1; + visibility: visible; +} + +.carousel-slide.entering-right { + transform: translateX(30px); + opacity: 0; +} + +.carousel-slide.entering-left { + transform: translateX(-30px); + opacity: 0; +} + +/* ============================================ + Slide Content + ============================================ */ + +.carousel-slide-inner { + position: relative; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-end; + padding: var(--space-8); +} + +@media (max-width: 768px) { + .carousel-slide-inner { + padding: var(--space-6); + } +} + +/* Background Image */ +.carousel-bg { + position: absolute; + inset: 0; + z-index: 0; +} + +.carousel-bg-image { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 8s linear; +} + +.carousel-slide.active .carousel-bg-image { + transform: scale(1.05); +} + +/* Gradient Overlay */ +.carousel-bg::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(to top, + rgba(2, 6, 23, 0.95) 0%, + rgba(2, 6, 23, 0.7) 30%, + rgba(2, 6, 23, 0.3) 60%, + rgba(2, 6, 23, 0.1) 100%); +} + +/* Content Wrapper */ +.carousel-content { + position: relative; + z-index: 1; + max-width: 700px; +} + +.carousel-title { + font-family: var(--font-display); + font-size: clamp(2rem, 5vw, 3.5rem); + font-weight: var(--font-bold); + line-height: 1.1; + color: var(--text-primary); + margin-bottom: var(--space-3); + letter-spacing: -0.025em; +} + +.carousel-title span { + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.carousel-subtitle { + font-size: var(--text-lg); + font-weight: var(--font-medium); + color: var(--color-primary-400); + margin-bottom: var(--space-2); + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.carousel-description { + font-size: var(--text-base); + color: var(--text-secondary); + line-height: 1.6; + margin-bottom: var(--space-6); + max-width: 500px; +} + +@media (max-width: 768px) { + .carousel-description { + font-size: var(--text-sm); + margin-bottom: var(--space-4); + } +} + +/* CTA Button */ +.carousel-cta { + display: inline-flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-3) var(--space-6); + font-size: var(--text-sm); + font-weight: var(--font-semibold); + color: white; + background: var(--gradient-primary); + border: none; + border-radius: var(--radius-lg); + text-decoration: none; + cursor: pointer; + transition: all var(--duration) var(--ease-out); +} + +.carousel-cta:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg), var(--shadow-glow-blue); +} + +.carousel-cta svg { + transition: transform var(--duration) var(--ease-out); +} + +.carousel-cta:hover svg { + transform: translateX(4px); +} + +/* ============================================ + Navigation Arrows + ============================================ */ + +.carousel-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 10; +} + +.carousel-nav-prev { + left: var(--space-4); +} + +.carousel-nav-next { + right: var(--space-4); +} + +.carousel-nav-btn { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + padding: 0; + background: rgba(15, 23, 42, 0.7); + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: var(--radius-full); + color: var(--text-primary); + cursor: pointer; + transition: all var(--duration) var(--ease-out); +} + +.carousel-nav-btn:hover { + background: rgba(59, 130, 246, 0.3); + border-color: var(--color-primary-500); + transform: scale(1.05); +} + +.carousel-nav-btn:focus-visible { + outline: 2px solid var(--color-primary-500); + outline-offset: 2px; +} + +@media (max-width: 768px) { + .carousel-nav-btn { + width: 40px; + height: 40px; + } + + .carousel-nav-prev { + left: var(--space-2); + } + + .carousel-nav-next { + right: var(--space-2); + } +} + +/* ============================================ + Dots Indicator + ============================================ */ + +.carousel-dots { + position: absolute; + bottom: var(--space-6); + left: 50%; + transform: translateX(-50%); + z-index: 10; + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-4); + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(8px); + border-radius: var(--radius-full); +} + +.carousel-dot { + width: 8px; + height: 8px; + padding: 0; + background: rgba(255, 255, 255, 0.3); + border: none; + border-radius: var(--radius-full); + cursor: pointer; + transition: all var(--duration) var(--ease-out); +} + +.carousel-dot:hover { + background: rgba(255, 255, 255, 0.5); +} + +.carousel-dot.active { + width: 24px; + background: var(--gradient-primary); +} + +.carousel-dot:focus-visible { + outline: 2px solid var(--color-primary-500); + outline-offset: 2px; +} + +/* ============================================ + Play/Pause Button + ============================================ */ + +.carousel-playback { + position: absolute; + top: var(--space-4); + right: var(--space-4); + z-index: 10; +} + +.carousel-playback-btn { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + padding: 0; + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: var(--radius-full); + color: var(--text-secondary); + cursor: pointer; + transition: all var(--duration) var(--ease-out); +} + +.carousel-playback-btn:hover { + background: rgba(15, 23, 42, 0.8); + color: var(--text-primary); +} + +/* ============================================ + Progress Bar + ============================================ */ + +.carousel-progress { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background: rgba(255, 255, 255, 0.1); + z-index: 10; +} + +.carousel-progress-bar { + height: 100%; + background: var(--gradient-primary); + width: 0; + transition: width 0.1s linear; +} + +.carousel.playing .carousel-progress-bar { + animation: carousel-progress 5s linear forwards; +} + +@keyframes carousel-progress { + from { + width: 0; + } + + to { + width: 100%; + } +} + +/* ============================================ + Skeleton Loading + ============================================ */ + +.carousel-skeleton { + display: flex; + flex-direction: column; + justify-content: flex-end; + height: 100%; + padding: var(--space-8); +} + +.carousel-skeleton-subtitle { + width: 100px; + height: 14px; + margin-bottom: var(--space-3); +} + +.carousel-skeleton-title { + width: 60%; + height: 48px; + margin-bottom: var(--space-3); +} + +.carousel-skeleton-desc { + width: 80%; + height: 20px; + margin-bottom: var(--space-2); +} + +.carousel-skeleton-desc:last-of-type { + width: 60%; + margin-bottom: var(--space-6); +} + +.carousel-skeleton-btn { + width: 140px; + height: 44px; +} + +/* ============================================ + Accessibility: Reduced Motion + ============================================ */ + +@media (prefers-reduced-motion: reduce) { + .carousel-slide { + transition: opacity var(--duration-slow) var(--ease-out); + } + + .carousel-slide.active .carousel-bg-image { + transform: none; + } + + .carousel.playing .carousel-progress-bar { + animation: none; + } +} + +/* ============================================ + ARIA Live Region + ============================================ */ + +.carousel-announcer { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* ============================================ + Cyber Theme Enhancements + ============================================ */ + +[data-cyber-theme] .carousel { + border: 1px solid var(--border-neon, rgba(0, 212, 255, 0.2)); + box-shadow: 0 0 30px rgba(0, 0, 0, 0.3); +} + +[data-cyber-theme] .carousel-title span { + background: var(--gradient-neon, linear-gradient(135deg, #00d4ff 0%, #00ffff 100%)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + filter: drop-shadow(0 0 8px rgba(0, 212, 255, 0.3)); +} + +[data-cyber-theme] .carousel-subtitle { + color: var(--neon-primary, #00d4ff); + text-shadow: 0 0 10px rgba(0, 212, 255, 0.4); +} + +[data-cyber-theme] .carousel-description { + color: var(--text-neon, #e0e7ff); +} + +[data-cyber-theme] .carousel-nav-btn { + background: rgba(15, 20, 30, 0.6); + border-color: var(--border-neon, rgba(0, 212, 255, 0.3)); + color: var(--neon-primary, #00d4ff); +} + +[data-cyber-theme] .carousel-nav-btn:hover { + background: rgba(0, 212, 255, 0.2); + border-color: var(--neon-cyan, #00ffff); + box-shadow: 0 0 16px rgba(0, 212, 255, 0.4); +} + +[data-cyber-theme] .carousel-dots { + background: rgba(15, 20, 30, 0.6); + border: 1px solid var(--border-neon, rgba(0, 212, 255, 0.2)); +} + +[data-cyber-theme] .carousel-dot.active { + background: var(--gradient-neon); + box-shadow: 0 0 10px rgba(0, 212, 255, 0.6); +} + +/* CTA with cyber glow */ +[data-cyber-theme] .carousel-cta { + background: var(--gradient-neon); + color: #0a0e17; + box-shadow: 0 0 16px rgba(0, 212, 255, 0.4); +} + +[data-cyber-theme] .carousel-cta:hover { + box-shadow: 0 0 24px rgba(0, 212, 255, 0.6); +} + +/* Neon mode */ +[data-cyber-theme="neon"] .carousel { + box-shadow: 0 0 40px rgba(0, 255, 255, 0.2); + border-color: var(--neon-primary, #00d4ff); +} + +[data-cyber-theme="neon"] .carousel-title span { + filter: drop-shadow(0 0 15px rgba(0, 255, 255, 0.5)); +} + +/* Minimal mode */ +[data-cyber-theme="minimal"] .carousel-nav-btn, +[data-cyber-theme="minimal"] .carousel-cta { + box-shadow: none; +} \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/CarouselV2/CarouselV2.tsx b/Ark.Alliance.StartupCms.Ai.UI/src/components/CarouselV2/CarouselV2.tsx new file mode 100644 index 0000000000000000000000000000000000000000..410fee9647c27986991154c93309f5609bf159b5 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/CarouselV2/CarouselV2.tsx @@ -0,0 +1,219 @@ +/** + * @fileoverview CarouselV2 View Component + * Premium carousel with autoplay, keyboard nav, and touch support. + * + * @author Armand Richelet-Kleinberg + */ + +import React from 'react'; +import { Link } from 'react-router-dom'; +import { ChevronLeft, ChevronRight, Play, Pause, ArrowRight } from 'lucide-react'; +import { useCarouselV2Model, CarouselSlide } from './CarouselV2.model'; +import './CarouselV2.styles.css'; + +/** + * CarouselV2 Props + */ +export interface CarouselV2Props { + /** Slides to display */ + slides: CarouselSlide[]; + /** Autoplay interval in ms (default: 5000, 0 = disabled) */ + autoplayInterval?: number; + /** Show navigation arrows */ + showNav?: boolean; + /** Show dot indicators */ + showDots?: boolean; + /** Show play/pause button */ + showPlayback?: boolean; + /** Show progress bar */ + showProgress?: boolean; + /** Loading state */ + isLoading?: boolean; + /** Optional class name */ + className?: string; +} + +/** + * CarouselV2 Component + * + * Polished, accessible carousel with: + * - Autoplay with pause on interaction + * - Arrow key navigation + * - Touch swipe gestures + * - Progress bar + * - ARIA live announcements + * - Reduced motion support + */ +export const CarouselV2: React.FC = ({ + slides, + autoplayInterval = 5000, + showNav = true, + showDots = true, + showPlayback = true, + showProgress = true, + isLoading = false, + className = '', +}) => { + const vm = useCarouselV2Model({ + slides, + autoplayInterval, + pauseOnInteraction: true, + }); + + // Render loading skeleton + if (isLoading || slides.length === 0) { + return ( +
+
+
+
+
+
+
+
+
+ ); + } + + return ( +
+ {/* Slides */} +
+ {slides.map((slide, index) => ( +
+
+ {/* Background Image */} + {slide.imageUrl && ( +
+ +
+ )} + + {/* Content */} +
+ {slide.subtitle && ( +

{slide.subtitle}

+ )} +

+ {slide.title.split(' ').map((word, i) => ( + i === 0 ? {word} : word + ' ' + ))} +

+ {slide.description && ( +

{slide.description}

+ )} + {slide.ctaLink && ( + + {slide.ctaLabel || 'Learn More'} + + + )} +
+
+
+ ))} +
+ + {/* Navigation Arrows */} + {showNav && slides.length > 1 && ( + <> +
+ +
+
+ +
+ + )} + + {/* Dot Indicators */} + {showDots && slides.length > 1 && ( +
+ {slides.map((_, index) => ( +
+ )} + + {/* Play/Pause Button */} + {showPlayback && autoplayInterval > 0 && ( +
+ +
+ )} + + {/* Progress Bar */} + {showProgress && vm.isPlaying && ( +
+
+
+ )} + + {/* ARIA Live Region for Announcements */} +
+ Slide {vm.currentIndex + 1} of {vm.totalSlides}: {vm.currentSlide?.title} +
+
+ ); +}; + +export default CarouselV2; diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/CarouselV2/index.ts b/Ark.Alliance.StartupCms.Ai.UI/src/components/CarouselV2/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3bdc4c44e1abda192e38125207106419fcdbe5b0 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/CarouselV2/index.ts @@ -0,0 +1,8 @@ +/** + * @fileoverview CarouselV2 Component Exports + */ + +export { CarouselV2 } from './CarouselV2'; +export { useCarouselV2Model } from './CarouselV2.model'; +export type { CarouselV2Props } from './CarouselV2'; +export type { CarouselV2Model, CarouselSlide, CarouselV2Config } from './CarouselV2.model'; diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/ConnectionIndicator/ConnectionIndicator.styles.css b/Ark.Alliance.StartupCms.Ai.UI/src/components/ConnectionIndicator/ConnectionIndicator.styles.css new file mode 100644 index 0000000000000000000000000000000000000000..963a23226d542e687e39b4d5deeddc98afe85665 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/ConnectionIndicator/ConnectionIndicator.styles.css @@ -0,0 +1,133 @@ +/** + * Connection Indicator Styles + * Visual styling for backend connection status indicator. + * + * @author Armand Richelet-Kleinberg + */ + +.connection-indicator { + display: flex; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.625rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; + cursor: default; + transition: all 0.2s ease; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.connection-icon { + display: flex; + align-items: center; + justify-content: center; +} + +.connection-dot { + width: 6px; + height: 6px; + border-radius: 50%; + animation: pulse 2s infinite; +} + +/* Connected State */ +.connection-connected { + color: var(--color-success, #22c55e); + border-color: rgba(34, 197, 94, 0.3); + background: rgba(34, 197, 94, 0.1); +} + +.connection-connected .connection-dot { + background: var(--color-success, #22c55e); + box-shadow: 0 0 6px var(--color-success, #22c55e); +} + +/* Disconnected State */ +.connection-disconnected { + color: var(--color-error, #ef4444); + border-color: rgba(239, 68, 68, 0.3); + background: rgba(239, 68, 68, 0.1); +} + +.connection-disconnected .connection-dot { + background: var(--color-error, #ef4444); + box-shadow: 0 0 6px var(--color-error, #ef4444); + animation: none; +} + +/* Checking State */ +.connection-checking { + color: var(--color-warning, #f59e0b); + border-color: rgba(245, 158, 11, 0.3); + background: rgba(245, 158, 11, 0.1); +} + +.connection-checking .connection-dot { + background: var(--color-warning, #f59e0b); + animation: blink 0.5s infinite; +} + +.connection-spinner { + animation: spin 1s linear infinite; +} + +/* Mock Mode State */ +.connection-mock { + color: var(--color-info, #0ea5e9); + border-color: rgba(14, 165, 233, 0.3); + background: rgba(14, 165, 233, 0.1); +} + +.connection-mock .connection-dot { + background: var(--color-info, #0ea5e9); + box-shadow: 0 0 6px var(--color-info, #0ea5e9); +} + +/* Animations */ +@keyframes pulse { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.5; + } +} + +@keyframes blink { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.3; + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +/* Hover effect */ +.connection-indicator:hover { + transform: scale(1.05); +} + +/* Responsive - hide on very small screens */ +@media (max-width: 480px) { + .connection-indicator { + padding: 0.25rem 0.5rem; + } +} \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/ConnectionIndicator/ConnectionIndicator.tsx b/Ark.Alliance.StartupCms.Ai.UI/src/components/ConnectionIndicator/ConnectionIndicator.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4be9d5e5546e4f5078c69ce1111921502ba6f615 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/ConnectionIndicator/ConnectionIndicator.tsx @@ -0,0 +1,83 @@ +/** + * @fileoverview Connection Indicator Component + * Displays backend connection status in the header. + * + * @author Armand Richelet-Kleinberg + */ + +import React from 'react'; +import { Wifi, WifiOff, Loader2, Database } from 'lucide-react'; +import { useConnectionStatus, getStatusLabel } from '../../hooks/useConnectionStatus'; +import './ConnectionIndicator.styles.css'; + +export interface ConnectionIndicatorProps { + /** Show detailed info on hover */ + showDetails?: boolean; + /** Additional CSS class */ + className?: string; +} + +/** + * ConnectionIndicator Component + * Shows a visual indicator of backend connectivity. + */ +export const ConnectionIndicator: React.FC = ({ + showDetails = true, + className = '', +}) => { + const connectionInfo = useConnectionStatus(true, 30000); + + const getIcon = () => { + switch (connectionInfo.status) { + case 'connected': + return ; + case 'disconnected': + return ; + case 'checking': + return ; + case 'mock': + return ; + default: + return ; + } + }; + + const getTooltipContent = () => { + const lines = [getStatusLabel(connectionInfo.status)]; + + if (connectionInfo.status === 'connected') { + if (connectionInfo.latency !== null) { + lines.push(`Latency: ${connectionInfo.latency}ms`); + } + if (connectionInfo.protocol !== 'unknown') { + lines.push(`Protocol: ${connectionInfo.protocol.toUpperCase()}`); + } + } + + if (connectionInfo.status === 'mock') { + lines.push('Using mock data'); + } + + if (connectionInfo.lastChecked) { + lines.push(`Last check: ${connectionInfo.lastChecked.toLocaleTimeString()}`); + } + + return lines.join('\n'); + }; + + return ( +
+ + {getIcon()} + + +
+ ); +}; + +export default ConnectionIndicator; diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/ConnectionIndicator/index.ts b/Ark.Alliance.StartupCms.Ai.UI/src/components/ConnectionIndicator/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f37e997a39f4cf5bbbbe512a1cf26ef201c3f1b3 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/ConnectionIndicator/index.ts @@ -0,0 +1,5 @@ +/** + * ConnectionIndicator Component Barrel Export + */ +export { ConnectionIndicator } from './ConnectionIndicator'; +export type { ConnectionIndicatorProps } from './ConnectionIndicator'; diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/ContentRenderer/ContentRenderer.tsx b/Ark.Alliance.StartupCms.Ai.UI/src/components/ContentRenderer/ContentRenderer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9a29544e028236381cd1b50b8d1c32866246d4f7 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/ContentRenderer/ContentRenderer.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useRef, useState } from 'react'; +import mermaid from 'mermaid'; +import { cn } from '../../utils/cn'; + +interface ContentRendererProps { + content: string; + className?: string; +} + +export const ContentRenderer: React.FC = ({ content, className }) => { + const mermaidRef = useRef(null); + const [renderId, setRenderId] = useState(0); + + useEffect(() => { + mermaid.initialize({ + startOnLoad: false, + theme: 'dark', + securityLevel: 'loose', + fontFamily: 'monospace' + }); + }, []); + + useEffect(() => { + if (mermaidRef.current) { + mermaid.run({ + nodes: mermaidRef.current.querySelectorAll('.mermaid') + }); + } + }, [content, renderId]); + + // Simple Markdown Parser (Headers, Lists, Code Blocks) + const parseMarkdown = (text: string) => { + // Pre-process common markdown issues if needed + const lines = text.split('\n'); + const elements: React.ReactElement[] = []; + let inMermaidBlock = false; + let mermaidCode = ''; + let key = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Mermaid Detection + if (line.trim().startsWith('```mermaid')) { + inMermaidBlock = true; + mermaidCode = ''; + continue; + } + if (inMermaidBlock && line.trim().startsWith('```')) { + inMermaidBlock = false; + elements.push( +
+ {mermaidCode} +
+ ); + continue; + } + if (inMermaidBlock) { + mermaidCode += line + '\n'; + continue; + } + + // Headers + if (line.startsWith('### ')) { + elements.push(

{line.replace('### ', '')}

); + } else if (line.startsWith('#### ')) { + elements.push(

{line.replace('#### ', '')}

); + } + // Lists + else if (line.trim().startsWith('* ') || line.trim().startsWith('- ')) { + elements.push(
  • {line.replace(/^[\*\-]\s/, '')}
  • ); + } + else if (line.trim().match(/^\d+\.\s/)) { + elements.push(
  • {line.replace(/^\d+\.\s/, '')}
  • ); + } + // Paragraphs + else if (line.trim() !== '') { + const parts = line.split(/(\*\*.*?\*\*|\*.*?\*|`.*?`)/g); + const paragraph = ( +

    + {parts.map((part, idx) => { + if (part.startsWith('**') && part.endsWith('**')) { + return {part.slice(2, -2)}; + } + if (part.startsWith('*') && part.endsWith('*')) { + return {part.slice(1, -1)}; + } + if (part.startsWith('`') && part.endsWith('`')) { + return {part.slice(1, -1)}; + } + return part; + })} +

    + ); + elements.push(paragraph); + } + } + return elements; + }; + + return ( +
    + {parseMarkdown(content)} +
    + ); +}; + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/HeaderV2/HeaderV2.model.ts b/Ark.Alliance.StartupCms.Ai.UI/src/components/HeaderV2/HeaderV2.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..35492a59f41d3f28c2c43897e43a4cb04995febd --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/HeaderV2/HeaderV2.model.ts @@ -0,0 +1,266 @@ +/** + * @fileoverview HeaderV2 ViewModel + * Manages header state, navigation, and admin visibility logic. + * + * @author Armand Richelet-Kleinberg + */ + +import { useState, useCallback, useEffect } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { authService } from '../../services/auth.service'; +import { profileService } from '../../services/profile.service'; +import { organizationService } from '../../api'; +import { API_CONFIG } from '../../config/defaults'; + +/** + * Navigation item configuration + */ +export interface NavItem { + id: string; + label: string; + path: string; + icon?: React.ReactNode; +} + +/** + * Profile/Organization data for header display + */ +export interface HeaderProfile { + fullName: string; + title: string; + organizationName?: string; + organizationTagline?: string; +} + +/** + * HeaderV2 ViewModel state and actions + */ +export interface HeaderV2Model { + /** Navigation items */ + navItems: NavItem[]; + /** Current active path */ + activePath: string; + /** Is mobile menu open */ + isMobileMenuOpen: boolean; + /** Is user authenticated as admin */ + isAdmin: boolean; + /** Is header scrolled (for shadow effect) */ + isScrolled: boolean; + /** Profile data for header */ + profile: HeaderProfile; + /** Current page title */ + currentPageTitle: string; + /** Is export in progress */ + isExporting: boolean; + /** Export status message */ + exportStatus: string | null; + + // Actions + navigate: (path: string) => void; + toggleMobileMenu: () => void; + closeMobileMenu: () => void; + handleKeyDown: (e: React.KeyboardEvent) => void; + generateStaticSite: () => Promise; +} + +/** + * Primary navigation items + */ +const NAV_ITEMS: NavItem[] = [ + { id: 'home', label: 'Home', path: '/' }, + { id: 'team', label: 'Team', path: '/team' }, + { id: 'portfolio', label: 'Ark Alliance Products', path: '/projects' }, +]; + +/** + * Get page title from path + */ +const getPageTitle = (path: string): string => { + if (path === '/' || path === '') return ''; + if (path.startsWith('/projects/')) return 'Project Details'; + if (path === '/projects') return 'Projects'; + if (path === '/resume' || path === '/cv') return 'Resume'; + if (path === '/architecture') return 'Architecture'; + if (path === '/login') return 'Login'; + if (path.startsWith('/admin')) return 'Admin'; + return ''; +}; + +/** + * HeaderV2 ViewModel hook + * Encapsulates all header logic and state management. + */ +export const useHeaderV2Model = (): HeaderV2Model => { + const navigateTo = useNavigate(); + const location = useLocation(); + + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [isScrolled, setIsScrolled] = useState(false); + const [isAdmin, setIsAdmin] = useState(false); + const [isExporting, setIsExporting] = useState(false); + const [exportStatus, setExportStatus] = useState(null); + const [profile, setProfile] = useState({ + fullName: 'M2H.IO', + title: 'Mindful AI for Humanity' + }); + + // Fetch organization data (priority) then fall back to profile + useEffect(() => { + const fetchData = async () => { + try { + // Try organization first + const orgData = await organizationService.get(); + if (orgData) { + setProfile({ + fullName: orgData.name || 'M2H.IO', + title: orgData.tagline || 'Mindful AI for Humanity', + organizationName: orgData.name, + organizationTagline: orgData.tagline + }); + return; + } + } catch (error) { + console.warn('Failed to fetch organization for header:', error); + } + + // Fall back to profile data + try { + const data = await profileService.getProfile(); + if (data) { + setProfile(prev => ({ + ...prev, + fullName: prev.organizationName || `${data.firstName} ${data.lastName}`, + title: prev.organizationTagline || data.title || '' + })); + } + } catch (error) { + console.warn('Failed to fetch profile for header:', error); + } + }; + fetchData(); + }, []); + + // Check admin status + useEffect(() => { + const checkAuth = () => { + setIsAdmin(authService.isAuthenticated()); + }; + checkAuth(); + + // Listen for auth changes + window.addEventListener('storage', checkAuth); + return () => window.removeEventListener('storage', checkAuth); + }, []); + + // Handle scroll for shadow effect + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 10); + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + // Close mobile menu on route change + useEffect(() => { + setIsMobileMenuOpen(false); + }, [location.pathname]); + + // Close mobile menu on escape + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isMobileMenuOpen) { + setIsMobileMenuOpen(false); + } + }; + + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [isMobileMenuOpen]); + + const navigate = useCallback((path: string) => { + navigateTo(path); + setIsMobileMenuOpen(false); + }, [navigateTo]); + + const toggleMobileMenu = useCallback(() => { + setIsMobileMenuOpen(prev => !prev); + }, []); + + const closeMobileMenu = useCallback(() => { + setIsMobileMenuOpen(false); + }, []); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleMobileMenu(); + } + }, [toggleMobileMenu]); + + /** + * Generate static website export + */ + const generateStaticSite = useCallback(async () => { + if (isExporting) return; + + setIsExporting(true); + setExportStatus('Generating static site...'); + + try { + const token = authService.getToken(); + const response = await fetch(`${API_CONFIG.baseUrl}/admin/export/static`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`Export failed: ${response.statusText}`); + } + + // Download the ZIP file + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `ark-alliance-startupcms-ai-static-${new Date().toISOString().split('T')[0]}.zip`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + setExportStatus('Export complete!'); + setTimeout(() => setExportStatus(null), 3000); + } catch (error) { + console.error('Static site generation failed:', error); + setExportStatus('Export failed. Check console.'); + setTimeout(() => setExportStatus(null), 5000); + } finally { + setIsExporting(false); + } + }, [isExporting]); + + return { + navItems: NAV_ITEMS, + activePath: location.pathname, + isMobileMenuOpen, + isAdmin, + isScrolled, + profile, + currentPageTitle: getPageTitle(location.pathname), + isExporting, + exportStatus, + navigate, + toggleMobileMenu, + closeMobileMenu, + handleKeyDown, + generateStaticSite, + }; +}; + +export default useHeaderV2Model; + diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/HeaderV2/HeaderV2.styles.css b/Ark.Alliance.StartupCms.Ai.UI/src/components/HeaderV2/HeaderV2.styles.css new file mode 100644 index 0000000000000000000000000000000000000000..d5dc90b31d92cbde4f8dcdfde6c49021c3ba20b2 --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/HeaderV2/HeaderV2.styles.css @@ -0,0 +1,722 @@ +/** + * HeaderV2 Styles + * Polished, responsive header with glassmorphism and smooth transitions. + * + * @author Armand Richelet-Kleinberg + */ + +/* ============================================ + Header Container + ============================================ */ + +.header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: var(--z-sticky); + height: var(--header-height); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--space-6); + background: rgba(15, 23, 42, 0.8); + backdrop-filter: blur(12px); + border-bottom: 1px solid transparent; + transition: all var(--duration) var(--ease-out); +} + +/* Centered navigation wrapper - positioned absolutely for true centering */ +.header-nav { + position: absolute; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: var(--space-1); +} + +@media (max-width: 1024px) { + .header-nav { + position: static; + transform: none; + } +} + +.header.scrolled { + background: rgba(15, 23, 42, 0.95); + border-bottom-color: var(--border-color); + box-shadow: var(--shadow-lg); +} + +@media (max-width: 768px) { + .header { + height: var(--header-height-mobile); + padding: 0 var(--space-4); + } +} + +/* ============================================ + Logo Section + ============================================ */ + +.header-logo { + display: flex; + align-items: center; + gap: var(--space-3); + text-decoration: none; + color: var(--text-primary); + transition: transform var(--duration) var(--ease-out); +} + +.header-logo:hover { + transform: scale(1.02); +} + +.header-logo-mark { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-lg); + overflow: hidden; + /* Support for both img and text */ + font-family: var(--font-display); + font-weight: var(--font-bold); + font-size: var(--text-lg); + color: white; +} + +.header-logo-mark img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.header-logo-text { + font-family: var(--font-display); + font-size: var(--text-xl); + font-weight: var(--font-bold); + letter-spacing: -0.025em; +} + +.header-logo-text span { + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Profile Info in Header */ +.header-profile-info { + display: flex; + flex-direction: column; + gap: 0; +} + +.header-profile-name { + font-family: var(--font-display); + font-size: var(--text-lg); + font-weight: var(--font-bold); + color: var(--text-primary); + letter-spacing: -0.02em; + display: flex; + align-items: center; + gap: var(--space-2); +} + +.header-page-separator { + color: var(--text-muted); + font-weight: var(--font-normal); + opacity: 0.5; +} + +.header-page-title { + color: var(--color-primary-400); + font-weight: var(--font-medium); +} + +.header-profile-title { + font-size: var(--text-xs); + color: var(--text-muted); + font-weight: var(--font-normal); + letter-spacing: 0; + /* Remove truncation to show full title */ + white-space: normal; + line-height: 1.3; +} + +@media (max-width: 480px) { + .header-logo-text { + display: none; + } + + .header-profile-info { + display: none; + } +} + +/* ============================================ + Navigation Links + ============================================ */ + +.header-nav-link { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-4); + font-size: var(--text-sm); + font-weight: var(--font-medium); + color: var(--text-secondary); + text-decoration: none; + border-radius: var(--radius-md); + transition: all var(--duration) var(--ease-out); +} + +.header-nav-link:hover { + color: var(--text-primary); + background: rgba(100, 116, 139, 0.1); +} + +.header-nav-link.active { + color: var(--color-primary-400); + background: rgba(59, 130, 246, 0.1); +} + +.header-nav-link.active::before { + content: ''; + position: absolute; + bottom: -1px; + left: 50%; + transform: translateX(-50%); + width: 20px; + height: 2px; + background: var(--gradient-primary); + border-radius: var(--radius-full); +} + +@media (max-width: 768px) { + .header-nav { + display: none; + } +} + +/* ============================================ + Right Section (Admin + Mobile Toggle) + ============================================ */ + +.header-right { + display: flex; + align-items: center; + gap: var(--space-3); +} + +/* Admin Button */ +.header-admin-btn { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-4); + font-size: var(--text-sm); + font-weight: var(--font-medium); + color: var(--color-secondary-400); + background: rgba(139, 92, 246, 0.1); + border: 1px solid rgba(139, 92, 246, 0.2); + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--duration) var(--ease-out); + text-decoration: none; +} + +.header-admin-btn:hover { + background: rgba(139, 92, 246, 0.2); + border-color: rgba(139, 92, 246, 0.4); + box-shadow: var(--shadow-glow-purple); +} + +/* Login Button Variant (when not authenticated) */ +.header-login-btn { + color: var(--color-primary-400); + background: rgba(59, 130, 246, 0.1); + border-color: rgba(59, 130, 246, 0.2); +} + +.header-login-btn:hover { + background: rgba(59, 130, 246, 0.2); + border-color: rgba(59, 130, 246, 0.4); + box-shadow: var(--shadow-glow-blue); +} + +.header-admin-btn svg { + width: 16px; + height: 16px; +} + +@media (max-width: 480px) { + .header-admin-btn span { + display: none; + } + + .header-admin-btn { + padding: var(--space-2); + } +} + +/* Connection Indicator in Header */ +.header-connection-indicator { + margin-right: var(--space-1); +} + +@media (max-width: 640px) { + .header-connection-indicator { + display: none; + } +} + +/* Mobile Menu Toggle */ +.header-mobile-toggle { + display: none; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + padding: 0; + background: transparent; + border: none; + border-radius: var(--radius-md); + color: var(--text-secondary); + cursor: pointer; + transition: all var(--duration) var(--ease-out); +} + +.header-mobile-toggle:hover { + color: var(--text-primary); + background: rgba(100, 116, 139, 0.1); +} + +.header-mobile-toggle[aria-expanded="true"] { + color: var(--color-primary-400); +} + +@media (max-width: 768px) { + .header-mobile-toggle { + display: flex; + } +} + +/* ============================================ + Mobile Menu Overlay + ============================================ */ + +.header-mobile-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + z-index: var(--z-overlay); + opacity: 0; + visibility: hidden; + transition: all var(--duration) var(--ease-out); +} + +.header-mobile-overlay.open { + opacity: 1; + visibility: visible; +} + +/* ============================================ + Mobile Menu Panel + ============================================ */ + +.header-mobile-menu { + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: 280px; + max-width: 100%; + background: var(--bg-secondary); + border-left: 1px solid var(--border-color); + z-index: var(--z-modal); + transform: translateX(100%); + transition: transform var(--duration-slow) var(--ease-out); + display: flex; + flex-direction: column; +} + +.header-mobile-menu.open { + transform: translateX(0); +} + +.header-mobile-menu-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-4); + border-bottom: 1px solid var(--border-color); +} + +.header-mobile-menu-title { + font-size: var(--text-lg); + font-weight: var(--font-semibold); + color: var(--text-primary); +} + +.header-mobile-close { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + padding: 0; + background: transparent; + border: none; + border-radius: var(--radius-md); + color: var(--text-secondary); + cursor: pointer; + transition: all var(--duration) var(--ease-out); +} + +.header-mobile-close:hover { + color: var(--text-primary); + background: rgba(100, 116, 139, 0.1); +} + +/* Mobile Navigation Links */ +.header-mobile-nav { + flex: 1; + padding: var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.header-mobile-link { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-4); + font-size: var(--text-base); + font-weight: var(--font-medium); + color: var(--text-secondary); + text-decoration: none; + border-radius: var(--radius-lg); + transition: all var(--duration) var(--ease-out); +} + +.header-mobile-link:hover { + color: var(--text-primary); + background: rgba(100, 116, 139, 0.1); +} + +.header-mobile-link.active { + color: var(--color-primary-400); + background: rgba(59, 130, 246, 0.1); +} + +.header-mobile-link svg { + width: 20px; + height: 20px; +} + +/* Mobile Admin Section */ +.header-mobile-admin { + padding: var(--space-4); + border-top: 1px solid var(--border-color); +} + +.header-mobile-admin-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: var(--space-3); + font-size: var(--text-sm); + font-weight: var(--font-semibold); + color: white; + background: var(--gradient-secondary); + border: none; + border-radius: var(--radius-lg); + cursor: pointer; + text-decoration: none; + transition: all var(--duration) var(--ease-out); +} + +.header-mobile-admin-btn:hover { + transform: translateY(-1px); + box-shadow: var(--shadow-lg), var(--shadow-glow-purple); +} + +/* Mobile Login Button Variant */ +.header-mobile-login-btn { + background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%) !important; +} + +/* Export Button */ +.header-export-btn { + display: flex; + align-items: center; + gap: var(--space-2); + padding: 0.5rem 1rem; + font-size: var(--text-sm); + font-weight: var(--font-semibold); + color: white; + background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); + border: none; + border-radius: var(--radius-full); + text-decoration: none; + cursor: pointer; + transition: all var(--duration) var(--ease-out); +} + +.header-export-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(34, 197, 94, 0.4); +} + +.header-export-btn:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; +} + +/* Export Spinner Animation */ +.header-export-spinner { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +/* Export Status Toast */ +.header-export-status { + position: absolute; + top: calc(100% + 8px); + right: 0; + padding: 0.5rem 1rem; + background: rgba(15, 23, 42, 0.95); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + font-size: var(--text-xs); + color: var(--text-primary); + white-space: nowrap; + box-shadow: var(--shadow-lg); + animation: fadeInDown 0.2s ease-out; +} + +@keyframes fadeInDown { + from { + opacity: 0; + transform: translateY(-10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Mobile Export Section */ +.header-mobile-export { + padding: var(--space-3) var(--space-4); +} + +.header-mobile-export-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: var(--space-3); + font-size: var(--text-sm); + font-weight: var(--font-semibold); + color: white; + background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); + border: none; + border-radius: var(--radius-lg); + cursor: pointer; + transition: all var(--duration) var(--ease-out); +} + +.header-mobile-export-btn:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(34, 197, 94, 0.4); +} + +.header-mobile-export-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +@media (max-width: 480px) { + .header-export-btn span { + display: none; + } + + .header-export-btn { + padding: var(--space-2); + border-radius: var(--radius-md); + } +} + +.header-mobile-login-btn:hover { + box-shadow: var(--shadow-lg), var(--shadow-glow-blue); +} + +/* ============================================ + Page Offset for Fixed Header + ============================================ */ + +.header-offset { + height: var(--header-height); + background: var(--bg-primary, #0f0f0f); +} + +@media (max-width: 768px) { + .header-offset { + height: var(--header-height-mobile); + } +} + +/* ============================================ + Cyber Theme Enhancements + Applied when data-cyber-theme is set + ============================================ */ + +/* Header glass effect with neon border */ +[data-cyber-theme] .header { + background: rgba(10, 14, 23, 0.85); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-bottom: 1px solid var(--border-neon, rgba(0, 212, 255, 0.2)); +} + +[data-cyber-theme] .header.scrolled { + background: rgba(10, 14, 23, 0.95); + border-bottom-color: var(--border-neon-bright, rgba(0, 212, 255, 0.5)); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), + 0 0 20px rgba(0, 212, 255, 0.1); +} + +/* Logo with neon glow */ +[data-cyber-theme] .header-logo-text span, +[data-cyber-theme] .header-profile-name { + background: var(--gradient-neon, linear-gradient(135deg, #00d4ff 0%, #00ffff 100%)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Navigation links with neon hover */ +[data-cyber-theme] .header-nav-link { + color: var(--text-neon-secondary, #94a3b8); + transition: all 0.3s ease-out; +} + +[data-cyber-theme] .header-nav-link:hover, +[data-cyber-theme] .header-nav-link.active { + color: var(--neon-primary, #00d4ff); + text-shadow: 0 0 12px rgba(0, 212, 255, 0.7); +} + +[data-cyber-theme] .header-nav-link.active::after { + background: var(--gradient-neon, linear-gradient(135deg, #00d4ff 0%, #00ffff 100%)); + box-shadow: 0 0 8px rgba(0, 212, 255, 0.5); +} + +/* Admin button with neon styling */ +[data-cyber-theme] .header-admin-btn { + background: rgba(0, 212, 255, 0.1); + border-color: var(--border-neon, rgba(0, 212, 255, 0.3)); + color: var(--neon-primary, #00d4ff); +} + +[data-cyber-theme] .header-admin-btn:hover { + background: rgba(0, 212, 255, 0.2); + border-color: var(--neon-cyan, #00ffff); + box-shadow: 0 0 16px rgba(0, 212, 255, 0.5); +} + +/* Export button with neon */ +[data-cyber-theme] .header-export-btn { + background: var(--gradient-neon, linear-gradient(135deg, #00d4ff 0%, #00ffff 100%)); + color: #0a0e17; + box-shadow: 0 0 12px rgba(0, 212, 255, 0.4); +} + +[data-cyber-theme] .header-export-btn:hover:not(:disabled) { + box-shadow: 0 0 20px rgba(0, 212, 255, 0.7); +} + +/* Theme switcher spacing in header */ +.header-theme-switcher { + margin-right: var(--space-2); +} + +@media (max-width: 768px) { + .header-theme-switcher { + display: none; + } +} + +/* Mobile menu with cyber styling */ +[data-cyber-theme] .header-mobile-menu { + background: rgba(10, 14, 23, 0.95); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-left: 1px solid var(--border-neon, rgba(0, 212, 255, 0.2)); +} + +[data-cyber-theme] .header-mobile-nav-link { + color: var(--text-neon, #e0e7ff); +} + +[data-cyber-theme] .header-mobile-nav-link:hover, +[data-cyber-theme] .header-mobile-nav-link.active { + background: rgba(0, 212, 255, 0.1); + color: var(--neon-primary, #00d4ff); +} + +/* Neon mode intensified effects */ +[data-cyber-theme="neon"] .header { + border-bottom: 1px solid var(--border-neon-glow, rgba(0, 212, 255, 0.8)); +} + +[data-cyber-theme="neon"] .header-nav-link:hover, +[data-cyber-theme="neon"] .header-nav-link.active { + text-shadow: 0 0 16px rgba(0, 255, 255, 0.9); +} + +[data-cyber-theme="neon"] .header-admin-btn:hover { + box-shadow: 0 0 24px rgba(0, 255, 255, 0.7); +} + +/* Glass mode maximum transparency */ +[data-cyber-theme="glass"] .header { + background: rgba(10, 14, 23, 0.4); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); +} + +[data-cyber-theme="glass"] .header.scrolled { + background: rgba(10, 14, 23, 0.6); +} + +/* Minimal mode reduced effects */ +[data-cyber-theme="minimal"] .header-nav-link:hover, +[data-cyber-theme="minimal"] .header-nav-link.active { + text-shadow: none; +} + +[data-cyber-theme="minimal"] .header-admin-btn:hover { + box-shadow: none; +} \ No newline at end of file diff --git a/Ark.Alliance.StartupCms.Ai.UI/src/components/HeaderV2/HeaderV2.tsx b/Ark.Alliance.StartupCms.Ai.UI/src/components/HeaderV2/HeaderV2.tsx new file mode 100644 index 0000000000000000000000000000000000000000..04e6f0b501bc20160d092bddce5bc979a469fbbe --- /dev/null +++ b/Ark.Alliance.StartupCms.Ai.UI/src/components/HeaderV2/HeaderV2.tsx @@ -0,0 +1,171 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Menu, X, Home, Users, Briefcase, Settings, LogIn } from 'lucide-react'; +import { useHeaderV2Model } from './HeaderV2.model'; +import { ConnectionIndicator } from '../ConnectionIndicator'; +import LogoArkAlliance from '../../Assets/LogoArkAlliance.png'; +import './HeaderV2.styles.css'; + +/** + * HeaderV2 Props + */ +export interface HeaderV2Props { + /** Optional additional CSS class */ + className?: string; +} + +/** + * HeaderV2 Component + * + * Polished, accessible header with: + * - Logo and brand + * - Primary navigation (Home, Team, Ark Alliance Products) + * - Sign Up / Login buttons for guests + * - Admin access button (when authenticated) + * - Responsive mobile menu with slideout drawer + * - Keyboard navigation support + * - ARIA accessibility attributes + */ +export const HeaderV2: React.FC = ({ className = '' }) => { + const vm = useHeaderV2Model(); + + const getNavIcon = (id: string) => { + switch (id) { + case 'home': return ; + case 'team': return ; + case 'portfolio': return ; + default: return null; + } + }; + + return ( + <> +
    + {/* Profile Name & Current Page */} + + Logo +
    +
    + {vm.profile.fullName} + {vm.currentPageTitle && ( + <> + / + {vm.currentPageTitle} + + )} +
    +
    {vm.profile.title}
    +
    + + + {/* Desktop Navigation */} + + + {/* Right Section */} +
    + {/* Backend Connection Status Indicator */} + + + {/* Auth Buttons - Show Login for guests, Admin for authenticated */} + + {vm.isAdmin ? : } + {vm.isAdmin ? 'Admin' : 'Login'} + + + {/* Mobile Menu Toggle */} + +
    +
    + + {/* Mobile Overlay */} +