diff --git a/.env.example b/.env.example index deef819770..e30797c77f 100644 --- a/.env.example +++ b/.env.example @@ -168,7 +168,8 @@ NEXT_PUBLIC_BUILDER_URL=http://localhost:3123 SCHEDULER_BUCKET_RANGE=0-255 # Increase Node.js heap for large workloads. Set per-process, not globally. -# NODE_OPTIONS="--max_old_space_size=4096" +# Increase Node.js heap for large workloads. Set per-process, not globally. +# NODE_OPTIONS="--max-old-space-size=8192" # ───────────────────────────────────────────── # Logging diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f23acd77be..a296df94d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,8 @@ jobs: name: Types runs-on: ubuntu-latest timeout-minutes: 20 + env: + NODE_OPTIONS: --max-old-space-size=4096 steps: - name: Checkout repository uses: actions/checkout@v7 @@ -55,12 +57,14 @@ jobs: # Sets TURBO_API/TURBO_TOKEN/TURBO_TEAM for the steps below. - name: Set up Turbo cache uses: rharkor/caching-for-turbo@v2.5.1 + with: + server-port: 0 # 56 independent `tsc --noEmit` runs (no `composite`/`references` in this # repo, so turbo.json declares no `dependsOn` — see the comment there). # Each is single-threaded, so concurrency can fill all 4 vCPUs. - name: Type-check - run: pnpm turbo run check-types --concurrency=4 + run: pnpm turbo run check-types --concurrency=1 lint: name: Lint @@ -84,6 +88,8 @@ jobs: - name: Set up Turbo cache uses: rharkor/caching-for-turbo@v2.5.1 + with: + server-port: 0 # Root `pnpm lint` runs cheapest-first: check:agent-instructions, then # `turbo run lint` (apps/builder's i18n key-parity check plus 57 @@ -115,6 +121,8 @@ jobs: - name: Set up Turbo cache uses: rharkor/caching-for-turbo@v2.5.1 + with: + server-port: 0 # Turbo does not parallelize *within* a suite, so wall clock is bounded # by the slowest single suite — builder (248 files), which is ~40% of diff --git a/.github/workflows/deploy-devel.yml b/.github/workflows/deploy-devel.yml new file mode 100644 index 0000000000..63c0cd6116 --- /dev/null +++ b/.github/workflows/deploy-devel.yml @@ -0,0 +1,152 @@ +# Deploy devel — ChatbotX (patrón sysbrazo) +# +# Corre en push a main (después de merge de PR). +# Enforce PR-only merges via branch protection on main (no direct pushes). +# +# workflow_dispatch (manual): +# maintenance_on — maintenance sin deploy +# maintenance_off — restaura tráfico si deploy/smoke falló +# +# Los scripts viven en el repo (scripts/deployment/*.sh) y corren desde el +# runner, igual que sysbrazo (deploy-v3.sh). Las credenciales de AWS las +# tiene la instancia EC2 (IAM role). El runner se autentica con SSH_PRIVATE_KEY. + +name: Deploy Devel + +permissions: + contents: read + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + job: + description: Manual maintenance action + type: choice + required: true + options: + - maintenance_on + - maintenance_off + +concurrency: + group: chatbotx-devel + cancel-in-progress: true + +env: + ENV: devel + WORKSPACE: /var/www/chatbotx-dev + SERVER: ip-10-6-3-11.us-west-2.compute.internal + SSH_USER: sysbrazo + SECRET_NAME: dev/chatbotx/all-secret + COMPOSE_FILES: "-f docker-compose.yml -f docker-compose.apps.yml -f docker-compose.dev.yml" + HUSKY: "0" + +jobs: + maintenance_on: + name: Maintenance on + if: | + always() && + ( + (github.event_name == 'push') || + (github.event_name == 'workflow_dispatch' && github.event.inputs.job == 'maintenance_on') + ) + runs-on: [self-hosted, chatbotx] + timeout-minutes: 15 + environment: + name: devel + url: https://dev-chatbotx.fibrazo.com.co/ + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Init SSH + shell: bash + run: | + mkdir -p ~/.ssh + chmod 700 ~/.ssh + echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config + + - name: Load SSH key + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Enable maintenance + run: | + chmod +x ./scripts/deployment/maintenance-on.sh + ./scripts/deployment/maintenance-on.sh + + deploy: + name: Deploy + smoke + needs: [maintenance_on] + if: | + always() && + github.event_name == 'push' && + needs.maintenance_on.result == 'success' + runs-on: [self-hosted, chatbotx] + timeout-minutes: 60 + environment: + name: devel + url: https://dev-chatbotx.fibrazo.com.co/ + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Init SSH + shell: bash + run: | + mkdir -p ~/.ssh + chmod 700 ~/.ssh + echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config + + - name: Load SSH key + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Deploy + run: | + chmod +x ./scripts/deployment/deploy.sh + ./scripts/deployment/deploy.sh + + - name: Smoke tests + run: | + chmod +x ./scripts/deployment/run-smoke-tests.sh + ./scripts/deployment/run-smoke-tests.sh + + maintenance_off: + name: Maintenance off + needs: [deploy] + if: | + always() && + ( + (github.event_name == 'push' && needs.deploy.result == 'success') || + (github.event_name == 'workflow_dispatch' && github.event.inputs.job == 'maintenance_off') + ) + runs-on: [self-hosted, chatbotx] + timeout-minutes: 15 + environment: + name: devel + url: https://dev-chatbotx.fibrazo.com.co/ + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Init SSH + shell: bash + run: | + mkdir -p ~/.ssh + chmod 700 ~/.ssh + echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config + + - name: Load SSH key + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Disable maintenance + run: | + chmod +x ./scripts/deployment/maintenance-off.sh + ./scripts/deployment/maintenance-off.sh diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 0000000000..d226acd66e --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,158 @@ +# Deploy production — ChatbotX (patrón sysbrazo) +# +# Corre en push de tags (igual que sysbrazo: crear tags desde main, ej.: +# git tag 4.1.19 && git push origin 4.1.19 +# +# workflow_dispatch (manual): +# maintenance_on — maintenance sin deploy +# maintenance_off — restaura tráfico si deploy/smoke falló +# +# Los scripts viven en el repo (scripts/deployment/*.sh) y corren desde el +# runner, igual que sysbrazo (deploy-v3.sh). Las credenciales de AWS las +# tiene la instancia EC2 (IAM role). El runner se autentica con SSH_PRIVATE_KEY. +# Requiere GitHub environment `production` con SSH_PRIVATE_KEY secret. + +name: Deploy Production + +permissions: + contents: read + +on: + push: + tags: + - "*" + workflow_dispatch: + inputs: + job: + description: Manual maintenance action + type: choice + required: true + options: + - maintenance_on + - maintenance_off + +concurrency: + group: chatbotx-production + cancel-in-progress: true + +env: + ENV: production + WORKSPACE: /var/www/chatbotx-prod + SERVER: + SSH_USER: sysbrazo + SECRET_NAME: prod/chatbotx/all-secret + COMPOSE_FILES: "-f docker-compose.yml -f docker-compose.apps.yml -f docker-compose.prod.yml" + HUSKY: "0" + +jobs: + maintenance_on: + name: Maintenance on + if: | + always() && + ( + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) || + (github.event_name == 'workflow_dispatch' && github.event.inputs.job == 'maintenance_on') + ) + runs-on: [self-hosted, chatbotx] + timeout-minutes: 15 + environment: + name: production + url: https://chatbotx.fibrazo.com.co/ + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && 'main' || github.ref }} + + - name: Init SSH + shell: bash + run: | + mkdir -p ~/.ssh + chmod 700 ~/.ssh + echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config + + - name: Load SSH key + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Enable maintenance + run: | + chmod +x ./scripts/deployment/maintenance-on.sh + ./scripts/deployment/maintenance-on.sh + + deploy: + name: Deploy + smoke + needs: [maintenance_on] + if: | + always() && + github.event_name == 'push' && + startsWith(github.ref, 'refs/tags/') && + needs.maintenance_on.result == 'success' + runs-on: [self-hosted, chatbotx] + timeout-minutes: 60 + environment: + name: production + url: https://chatbotx.fibrazo.com.co/ + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Init SSH + shell: bash + run: | + mkdir -p ~/.ssh + chmod 700 ~/.ssh + echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config + + - name: Load SSH key + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Deploy + run: | + chmod +x ./scripts/deployment/deploy.sh + ./scripts/deployment/deploy.sh + + - name: Smoke tests + run: | + chmod +x ./scripts/deployment/run-smoke-tests.sh + ./scripts/deployment/run-smoke-tests.sh + + maintenance_off: + name: Maintenance off + needs: [deploy] + if: | + always() && + ( + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && needs.deploy.result == 'success') || + (github.event_name == 'workflow_dispatch' && github.event.inputs.job == 'maintenance_off') + ) + runs-on: [self-hosted, chatbotx] + timeout-minutes: 15 + environment: + name: production + url: https://chatbotx.fibrazo.com.co/ + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && 'main' || github.ref }} + + - name: Init SSH + shell: bash + run: | + mkdir -p ~/.ssh + chmod 700 ~/.ssh + echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config + + - name: Load SSH key + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Disable maintenance + run: | + chmod +x ./scripts/deployment/maintenance-off.sh + ./scripts/deployment/maintenance-off.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4ae28c917e..f2aa4b93f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,9 @@ name: Build and Push Docker Images on: - push: - branches: [main] - tags: ["v*"] + # Disabled: this workflow is not used for ChatbotX deploys (we build on the + # server via docker compose, not via ghcr.io images). Kept for reference; + # can be re-enabled by restoring the push triggers. workflow_dispatch: inputs: environment: @@ -58,6 +58,8 @@ jobs: - name: Set up Turbo cache uses: rharkor/caching-for-turbo@v2.5.1 + with: + server-port: 0 # `pnpm lint` covers everything, cheapest-first: check:agent-instructions, # `turbo run lint` for apps/builder's i18n check, then repo-wide Biome diff --git a/.gitignore b/.gitignore index dc7d0699a0..c49a0208e6 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ CLAUDE.md .pnpm-store/ .plans/ + +# Local editor config with machine-specific paths (codegraph --path) +.cursor/mcp.json diff --git a/.opencode/agents/raul.md b/.opencode/agents/raul.md new file mode 100644 index 0000000000..418960747a --- /dev/null +++ b/.opencode/agents/raul.md @@ -0,0 +1,236 @@ +--- +description: Maximum fidelity Cursor-like Ask agent with planning, relevance scoring, execution tracing, and adaptive multi-step reasoning. +mode: all + +permission: + edit: allow + write: allow + bash: allow +--- + +You are Raul, a senior software engineer + codebase reasoning engine. + +You simulate how an expert engineer debugs and understands systems in real time. + +Your goal is not to read code — it is to efficiently reconstruct behavior from minimal evidence. + +--- + +## Ask & explain before modifying + +Before making any modification or addition (code, config, docs, SDD artifacts, etc.), you **must**: + +1. Briefly explain what change is proposed and why — concise technical justification. +2. List the files affected. +3. Ask the user for confirmation. + +**Do not execute until the user explicitly approves.** + +Exception: this rule does not apply to pure read-only exploration or answering questions. + +--- + +# Core principle + +> Build understanding like a debugger: hypothesize, trace, confirm, stop. + +Avoid unnecessary exploration. Every file read must have a purpose. + +--- + +# 🧠 Multi-step reasoning engine + +For every request, you internally follow: + +## Step 1 — Intent classification + +Determine: + +- Is this a local question? (function/class) +- Is this a flow question? (feature behavior) +- Is this a bug? (debugging) + +## Step 2 — Entry point discovery + +Find the most likely starting point: + +- routes +- controllers +- UI entry +- main/server files +- event handlers + +## Step 3 — Relevance scoring (critical) + +Assign implicit relevance scores to files: + +- 0.9–1.0 → direct entrypoint +- 0.7–0.9 → direct dependency +- 0.4–0.7 → indirect relation +- <0.4 → ignore unless necessary + +Only follow high-score paths. + +## Step 4 — Execution tracing + +Follow only one primary execution path unless ambiguity requires branching. + +## Step 5 — Stop condition + +Stop when: + +- behavior is fully explained +- further exploration adds no value + +--- + +# ⚡ Adaptive modes + +## FAST MODE + +- single file or function +- no cross-module reasoning +- immediate answer + +## FLOW MODE + +- multi-file logic +- feature tracing +- entrypoint → execution path → output + +## DEBUG MODE + +- logs, errors, runtime issues +- hypothesis → test → confirmation loop + +--- + +# 🧭 Execution tracing model (Cursor-like behavior) + +When tracing behavior: + +Always reconstruct: + +Input → Handler → Processing → Output + +Include: + +- function calls +- state changes +- side effects (DB, API, cache) + +Stop when output is explained. + +--- + +# 🧩 Framework-aware tracing + +Auto-detect and adapt: + +## Laravel + +routes → controller → service → model → DB + +## React + +UI → component → hooks → API → backend + +## Node + +routes → handlers → services → DB/external APIs + +## Generic + +entry file → dependency graph → execution flow + +--- + +# 🧠 File memory (session persistence simulation) + +Maintain implicit context of: + +- already opened files +- already explained flows +- previously identified entrypoints + +Never re-read unless necessary. + +--- + +# 📊 File prioritization system + +Prefer files in this order: + +1. Entry points (routes, main, index) +2. Controllers / handlers +3. Core services +4. Models / repositories +5. Utilities / helpers +6. Config (only if relevant) + +--- + +# 🧾 Response structure + +## 1. Direct answer (mandatory) + +Short, precise explanation. + +## 2. Execution reasoning (if needed) + +- flow summary +- key components involved + +## 3. Evidence (only when useful) + +- file paths +- functions/classes +- minimal code snippets + +--- + +# 🔁 Intelligent follow-ups + +If ambiguity exists: + +- ask ONE precise question OR +- proceed with most likely interpretation + +Do NOT block reasoning unnecessarily. + +--- + +# 🚫 Hard rules + +NEVER: + +- modify code +- propose refactors unless explicitly requested +- generate patches or implementations +- hallucinate files, APIs, or architecture +- explore unrelated parts of the repo +- perform full repository scans without justification + +--- + +# 🧠 Cognitive optimization rules + +Always prefer: + +- minimal file reads +- single-path reasoning +- runtime behavior over static structure +- evidence over assumptions +- stopping early over over-analysis + +--- + +# 🎯 Ultimate goal + +Act like a senior engineer debugging a live system: + +- thinks in execution flows +- reasons incrementally +- verifies with minimal reads +- avoids noise +- always grounded in real code diff --git a/.vscode/settings.json b/.vscode/settings.json index b2cf2def41..8feee5613f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,7 +18,7 @@ "editor.defaultFormatter": "vscode.json-language-features" }, "[jsonc]": { - "editor.defaultFormatter": "biomejs.biome" + "editor.defaultFormatter": "vscode.json-language-features" }, "[css]": { "editor.defaultFormatter": "biomejs.biome" diff --git a/AGENTS.md b/AGENTS.md index 6e694c3a03..2fa23a90ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -137,6 +137,159 @@ For automatic context injection on every prompt, add the hook to your **own** `. - **Test placement:** use `/__tests__/` for app/package/integration-level tests, especially tests covering actions, routes, API behavior, cache behavior, worker behavior, or multiple feature boundaries (e.g. `apps/builder/__tests__`, `apps/worker/__tests__`, `packages/sdk/__tests__`, `integrations/messenger/__tests__`). Use colocated `src/**/__tests__` only for narrow unit/component tests clearly owned by that module. - **Quality bar:** Run `pnpm lint` (and typecheck scripts for touched packages) before considering work done. Keep changes scoped to the requested behavior. +## Spec-Driven Development (SDD) Flow + +This project adapts the sysbrazo SDD methodology for the ChatbotX-fibrazo Turborepo/Node ecosystem. All SDD artifacts live under the `spec/` directory at the repo root. + +### Directory Structure + +``` +spec/ +├── README.md # SDD rules and workflow overview +├── feature/ # Feature changes (ticket-based) +│ └── {ticket_id}/ # e.g., spec/feature/14100/ +│ ├── spec.md # Requirements, acceptance criteria +│ ├── plan.md # Approach, files affected, risk +│ ├── task.md # Checklist with Owners +│ └── context.md # Summary, tests, deviations +├── hotfix/ # Critical production bugs (no-ticket) +├── bug/ # Isolated bugs without ticket +├── chore/ # Technical debt/cleanup +└── spike/ # Research/investigation +``` + +### Artifact Format (Markdown) + +#### `spec.md` - Specification +- Ticket info (URL, tracker, priority, goal) +- Problem description and acceptance criteria +- **Skills & rules to load** (which `.agents/skills/` and `.agents/rules/` apply) +- **Do** / **Don't** constraints (project-specific invariants) +- Settings & flags to configure +- Docs to update (target: `docs/context/{domain}/{topic}.md`) + +#### `plan.md` - Plan (requires explicit user approval) +- Approach section +- Files affected list with change details +- Risk assessment (low/medium/high) +- **Approval gate rules** - each step needs explicit approval +- Open questions and approval status + +#### `task.md` - Tasks (checklist format) +- Ordered checklist items +- Each item has **Owner** (`parent`, `database-schema`, `feature-integration`, `debug-specialist`, `refactoring-specialist`) +- References to `plan.md` and `spec.md` sections + +#### `context.md` - Context (written when iteration closes) +- Summary of what was done vs. plan +- Files changed list +- Tests run (`pnpm lint`, typecheck, tests) +- Deviations from plan +- Subagents executed with summaries +- Next steps/blockers +- How to test (automated + manual QA) + +### SDD Workflow & Approval Gates + +| Phase | Action | Approval Required | +|-------|--------|-------------------| +| **Spec → Plan** | Write `spec.md` → Present | Explicit "ok"/"aprobado"/"approved" | +| **Plan → Tasks** | Write `plan.md` → Present | Explicit approval before coding | +| **Tasks → Implement** | Proceed after plan approval | Binding - no turning back | +| **Implement → Validate** | Run `pnpm lint` + typecheck + tests | Quality gate | +| **Validate → Context** | Write `context.md` and close | Mandatory | + +**Key constraints:** +- Feedback ≠ approval ("I like it" doesn't count) +- Partial approval ≠ full approval +- Must update `docs/context/{domain}/{topic}.md` when closing +- SDD files in English; conversation matches user's language +- Artifacts committed with code changes + +### Getting Started with SDD + +1. **Create a ticket/issue** in your tracking system +2. **Run**: `pnpm sd:init ` (or manually create `spec/feature/{ticket_id}/`) +3. **Write `spec.md`** following the template +4. **Present to user** for spec approval +5. **Write `plan.md`** and get plan approval +6. **Proceed with implementation** using subagents with explicit Owners +7. **Validate** with `pnpm lint` and tests +8. **Write `context.md`** and close the iteration + +### Example: Creating a New Feature SDD + +```bash +mkdir -p spec/feature/14100 + +# Create spec.md from template +cat > spec/feature/14100/spec.md << 'EOF' +# Specification: FEATURE_NAME + +## Ticket +- **URL**: https://redmine.example.com/issues/14100 +- **Tracker**: Feature +- **Priority**: High +- **Goal**: Brief description + +## Problem +Describe the problem or requested feature. + +## Acceptance Criteria +- [ ] Criteria 1 +- [ ] Criteria 2 +- [ ] Criteria 3 + +## Skills & Rules +Load these skills: +- agents/skills/feature-scaffold +- agents/skills/orpc-api (if adding API) + +## Do's and Don'ts +- Follow invariant #3 (ChannelType cascade) +- Use useTranslations() for all strings +- Don't import db directly in app layer + +## Docs to Update +- docs/context/feature/feature-name.md +EOF + +# Present spec to user for approval +# After approval, create plan.md +# After plan approval, proceed with tasks +``` + +### SDD Artifacts Already Created + +The following files/directories were created under `spec/`: +- `spec/README.md` - SDD rules and workflow overview +- `spec/feature/.template` - spec.md template +- `spec/plan/.template` - plan.md template +- `spec/task/.template` - task.md checklist template +- `spec/context/.template` - context.md iteration close template +- `spec/feature/`, `hotfix/`, `bug/`, `chore/`, `spike/` - Empty subdirectories ready for use + +### Integration with Existing Project Patterns + +The SDD flow integrates with ChatbotX-fibrazo's existing patterns: +- **Invariants**: Reference project invariants from `.agents/rules/` in your `spec.md` Do's/Don'ts +- **Data access**: Use service/repository pattern (no direct `db` import in app layer) +- **i18n**: All user-facing strings must use `useTranslations()` +- **Channel types**: Adding new `ChannelType` values requires fixing all `Record` hits +- **Flow nodes**: New node types must register in ALL node maps +- **Folder scoping**: `changeFolder` needs extra checks for shared tables across FolderTypes + +### SDD Scripts (recommended addition) + +Consider adding these `pnpm` scripts to `package.json` for SDD workflow automation: + +```json +"sd:init": "echo 'Create spec/feature/{ticket_id}/ directory manually'", +"sd:spec": "echo 'Open spec/{feature,bug,hotfix,chore,spike}/{ticket_id}/spec.md for editing'", +"sd:plan": "echo 'Open spec/{feature,bug,hotfix,chore,spike}/{ticket_id}/plan.md for editing'", +"sd:context": "echo 'Write context.md when iteration closes and update docs/context/{domain}/{topic}.md'" +``` + ## Key invariants for AI agents These are the most common mistakes — read before writing any code: diff --git a/FORK-CHANGES.md b/FORK-CHANGES.md new file mode 100644 index 0000000000..70c24e4707 --- /dev/null +++ b/FORK-CHANGES.md @@ -0,0 +1,163 @@ +# Fork ChatbotX — Cambios necesarios + +> Rama base: `main` de [github.com/chatbotxio/chatbotx](https://github.com/chatbotxio/chatbotx) + +--- + +## 🔧 Parches actuales (ya resueltos, ahora nativos) + +Estos cambios ya los tenemos funcionando con parches en runtime. Ahora se hacen directo en el source. + +### 1. Realtime auth — usar `REALTIME_AUTH_URL` + +**Archivo**: `apps/realtime/src/lib/auth.ts` +**Cambio**: Línea ~35, reemplazar detección por `origin` header con `process.env.REALTIME_AUTH_URL` +**Patch actual**: `patch-auth.sh` + +### 2. Realtime server — redirect de `/ws/parties/*` → `/parties/*` + +**Archivo**: `apps/realtime/src/lib/server.ts` +**Cambio**: Agregar fallback redirect (nginx ya hace el strip, esto es backup) +**Patch actual**: `patch-server.sh` + +### 3. Desbloquear edición enterprise (sin límites community) + +**Archivo**: raíz del proyecto (donde se define `isCommunity()`) +**Cambio**: Forzar `isCommunity() = false` para que la edición enterprise funcione sin restricciones +**Patch actual**: `Dockerfile.fibrazo` + +### 4. Host binding + +**Archivo**: `Dockerfile` del builder +**Cambio**: Agregar `HOST=0.0.0.0` para que Next.js escuche en todas las interfaces +**Actual**: En `docker-compose.yml` como variable de entorno + +### 5. Healthcheck del builder + +**Archivo**: `docker-compose.yml` (o `Dockerfile` para healthcheck nativo) +**Cambio**: Usar `node -e "require('os').hostname()"` en vez de `curl` +**Actual**: En `docker-compose.yml` + +--- + +## 🐛 Bugs bloqueantes (requieren desarrollo) + +### 6. Broadcast de mensajes en tiempo real para Telegram + +**Problema**: Cuando llega un mensaje por Telegram, el worker no emite `broadcastToWorkspaceParty`. El Shared Inbox no se actualiza hasta refresh manual. + +**Archivos**: +- `integrations/telegram/src/` — handler de incoming messages +- `apps/worker/src/` — donde se procesa el mensaje y debería hacer broadcast + +**Qué hacer**: Agregar llamada a `broadcastToWorkspaceParty(workspaceId, event)` en el flujo de recepción de mensajes de Telegram. + +**Complejidad**: Media (~1-2 días) + +### 7. Auto-skip con timeout para todos los canales + +**Problema**: El `autoSkip` configurado en `Get User Data` no funciona en Telegram. El timeout no se dispara porque depende del canal. + +**Archivos**: +- `packages/variables/` o `apps/worker/src/` — donde se maneja el `waitForContactInput` +- `integrations/telegram/` — handler de Telegram + +**Qué hacer**: Implementar el timeout server-side con BullMQ delayed jobs, independiente del canal. Después de X segundos de inactividad, el flow continúa automáticamente. + +**Complejidad**: Alta (~2-3 días) + +### 8. `CancelContactInput` — implementar + +**Problema**: Listado en el código como step type pero implementación vacía (`void 0`). No aparece en la UI del flow builder. + +**Archivos**: `apps/builder/`, `apps/worker/`, packages de flow execution + +**Complejidad**: Alta (~3-5 días) + +### 9. `WaitUserReply` — implementar + +**Problema**: Igual que arriba. Declarado pero sin implementar. + +**Complejidad**: Alta (~3-5 días) + +--- + +## ✨ Features nuevas (deseables) + +### 10. Variables globales de workspace + +**Problema**: No hay constantes tipo `{{env.URL_API}}` ni variables configurables por workspace. Las URLs se hardcodean en cada nodo. + +**Archivos**: +- `packages/database/` — nueva tabla `workspace_variable` (workspaceId, key, value) +- `packages/variables/src/` — nuevo resolver para `{{workspace.XXX}}` o `{{env.XXX}}` +- `apps/builder/` — UI en workspace settings para definir variables + +**Complejidad**: Media (~2-3 días) + +### 11. Migrar flows entre workspaces + +**Problema**: No hay UI ni API para copiar un flow de Dev a Prod. + +**Archivos**: +- `apps/builder/` — botón "Copy to workspace" en flow list +- API endpoint de export/import con regeneración de IDs + +**Complejidad**: Media-Alta (~3-5 días) + +### 12. Keyword para resetear conversación (ya funciona parcialmente) + +**Problema**: La keyword `reiniciar` existe pero no siempre resetea bien el estado. + +**Archivos**: `apps/worker/src/`, handlers de flow execution + +**Qué hacer**: Verificar que el reset de estado del flow funcione y que el contacto pueda empezar de cero. + +**Complejidad**: Baja (~1 día) + +--- + +## 📋 Prioridades + +| # | Cambio | Urgencia | Motivo | +|---|---|---|---| +| 1-5 | Parches a nativos | 🔴 YA | Dejar de depender de parches | +| 12 | Reset de conversación | 🔴 YA | Workaround para loop infinito | +| 10 | Variables globales | 🟠 Alta | Evitar hardcodear URLs en todos los flows | +| 6 | Broadcast Telegram | 🟠 Alta | Shared Inbox usable sin refresh | +| 7 | Auto-skip timeout | 🟡 Media | Flows con Get User Data no se traban | +| 11 | Migrar flows | 🟡 Media | Pasar de Dev a Prod sin script SQL | +| 8-9 | CancelContactInput / WaitUserReply | 🟢 Baja | Nice to have | + +--- + +## 🏗️ Estructura del proyecto + +``` +chatbotx-source/ +├── apps/ +│ ├── builder/ # Next.js — UI + API +│ ├── worker/ # BullMQ workers — ejecuta flows +│ ├── realtime/ # PartyKit — WebSocket +│ └── cli/ # CLI tool +├── packages/ +│ ├── database/ # Drizzle ORM schema +│ ├── variables/ # Motor de {{variables}} +│ ├── business/ # Lógica de negocio +│ └── ... +├── integrations/ +│ ├── telegram/ # Handler de Telegram +│ ├── whatsapp/ # Handler de WhatsApp +│ └── ... +└── docker-compose.yml +``` + +## 🛠️ Build y test + +```bash +cd chatbotx-source +pnpm install +pnpm build # build completo con Turborepo +pnpm --filter builder dev # solo el builder en dev +pnpm --filter worker dev # solo el worker en dev +``` diff --git a/apps/builder/__tests__/channel-route-guards.test.ts b/apps/builder/__tests__/channel-route-guards.test.ts index 97426b87e4..4b2954d699 100644 --- a/apps/builder/__tests__/channel-route-guards.test.ts +++ b/apps/builder/__tests__/channel-route-guards.test.ts @@ -8,6 +8,8 @@ const { mockRequireWorkspacePermission, mockGetCurrentUserAndTargetWorkspace, mockGetCurrentUserId, + mockGetCurrentUser, + mockIsPlatformAdmin, mockNotFound, mockRedirect, mockInboxCardList, @@ -16,6 +18,8 @@ const { mockRequireWorkspacePermission: vi.fn(async () => undefined), mockGetCurrentUserAndTargetWorkspace: vi.fn(), mockGetCurrentUserId: vi.fn(), + mockGetCurrentUser: vi.fn(async () => ({ id: "user-1" })), + mockIsPlatformAdmin: vi.fn(async () => true), mockNotFound: vi.fn(() => { throw new Error("not found") }), @@ -31,6 +35,7 @@ vi.mock("@/lib/auth/require-workspace-permission", () => ({ vi.mock("@/lib/auth/utils", () => ({ getCurrentUserAndTargetWorkspace: mockGetCurrentUserAndTargetWorkspace, getCurrentUserId: mockGetCurrentUserId, + getCurrentUser: mockGetCurrentUser, })) vi.mock("next/navigation", () => ({ @@ -51,6 +56,7 @@ vi.mock("@/features/analytics/components/analytics-nav", () => ({ })) vi.mock("@chatbotx.io/business", () => ({ + isPlatformAdmin: mockIsPlatformAdmin, isWorkspaceScheduledForDeletion: ( workspace: | { scheduledDeletionAt?: Date | string | null } @@ -254,6 +260,8 @@ describe("channel route guards", () => { beforeEach(() => { vi.clearAllMocks() mockGetCurrentUserId.mockResolvedValue("user-1") + mockGetCurrentUser.mockResolvedValue({ id: "user-1" }) + mockIsPlatformAdmin.mockResolvedValue(true) }) test("guards workspace-scoped channel creation on /channels/create", async () => { @@ -285,6 +293,20 @@ describe("channel route guards", () => { expect(mockRequireWorkspacePermission).not.toHaveBeenCalled() }) + test("redirects non-platform-admins away from the new-workspace flow (fork gate)", async () => { + mockIsPlatformAdmin.mockResolvedValue(false) + + await CreateChannelPage({ + searchParams: Promise.resolve({ + channel: "whatsapp", + workspaceId: null, + }), + }) + + expect(mockRedirect).toHaveBeenCalledWith("/") + expect(mockRequireWorkspacePermission).not.toHaveBeenCalled() + }) + test("hides the dashboard add-channel card for non-superAdmins", async () => { mockGetCurrentUserAndTargetWorkspace.mockResolvedValue({ targetWorkspace: { ownerId: "owner-1" }, diff --git a/apps/builder/__tests__/channels-create-messenger-handoff.test.ts b/apps/builder/__tests__/channels-create-messenger-handoff.test.ts index ecc5d5fb5d..72c075ef02 100644 --- a/apps/builder/__tests__/channels-create-messenger-handoff.test.ts +++ b/apps/builder/__tests__/channels-create-messenger-handoff.test.ts @@ -2,15 +2,19 @@ import { beforeEach, describe, expect, test, vi } from "vitest" -const { mockGetCurrentUserId, mockRedirect, mockResolveForOwner } = vi.hoisted( - () => ({ - mockGetCurrentUserId: vi.fn(async () => "user-1"), - mockRedirect: vi.fn((path: string) => { - throw new Error(`redirect:${path}`) - }), - mockResolveForOwner: vi.fn(), +const { + mockGetCurrentUser, + mockGetCurrentUserId, + mockRedirect, + mockResolveForOwner, +} = vi.hoisted(() => ({ + mockGetCurrentUserId: vi.fn(async () => "user-1"), + mockGetCurrentUser: vi.fn(async () => ({ id: "user-1" })), + mockRedirect: vi.fn((path: string) => { + throw new Error(`redirect:${path}`) }), -) + mockResolveForOwner: vi.fn(), +})) vi.mock("next/navigation", () => ({ notFound: vi.fn(() => { @@ -20,6 +24,7 @@ vi.mock("next/navigation", () => ({ })) vi.mock("@chatbotx.io/business", () => ({ + isPlatformAdmin: vi.fn(async () => true), platformCredentialService: { resolveForOwner: mockResolveForOwner }, tenantService: { resolveVisibleChannels: vi.fn(async () => [ @@ -54,7 +59,10 @@ vi.mock("@/lib/auth/require-workspace-permission", () => ({ requireWorkspacePermission: vi.fn(async () => undefined), })) -vi.mock("@/lib/auth/utils", () => ({ getCurrentUserId: mockGetCurrentUserId })) +vi.mock("@/lib/auth/utils", () => ({ + getCurrentUserId: mockGetCurrentUserId, + getCurrentUser: mockGetCurrentUser, +})) vi.mock("@/features/inboxes/components/inbox-select-card", () => ({ default: () => null, diff --git a/apps/builder/__tests__/channels-create-platform-owner.test.ts b/apps/builder/__tests__/channels-create-platform-owner.test.ts index c70eef029b..971e937dea 100644 --- a/apps/builder/__tests__/channels-create-platform-owner.test.ts +++ b/apps/builder/__tests__/channels-create-platform-owner.test.ts @@ -28,6 +28,7 @@ vi.mock("@/lib/auth/require-workspace-permission", () => ({ vi.mock("@/lib/auth/utils", () => ({ getCurrentUserId: vi.fn(async () => "user-1"), + getCurrentUser: vi.fn(async () => ({ id: "user-1" })), })) vi.mock("@/lib/platform-credential-owner", () => ({ @@ -44,6 +45,7 @@ vi.mock("next/navigation", () => ({ })) vi.mock("@chatbotx.io/business", () => ({ + isPlatformAdmin: vi.fn(async () => true), platformCredentialService: { resolveForOwner: mockResolveForOwner, }, diff --git a/apps/builder/__tests__/channels-create-visibility-guard.test.ts b/apps/builder/__tests__/channels-create-visibility-guard.test.ts index a53a57a5d8..6d2c8706c4 100644 --- a/apps/builder/__tests__/channels-create-visibility-guard.test.ts +++ b/apps/builder/__tests__/channels-create-visibility-guard.test.ts @@ -22,6 +22,7 @@ vi.mock("@/lib/auth/require-workspace-permission", () => ({ vi.mock("@/lib/auth/utils", () => ({ getCurrentUserId: vi.fn(async () => "user-1"), + getCurrentUser: vi.fn(async () => ({ id: "user-1" })), })) vi.mock("next/navigation", () => ({ @@ -34,6 +35,7 @@ vi.mock("next/navigation", () => ({ })) vi.mock("@chatbotx.io/business", () => ({ + isPlatformAdmin: vi.fn(async () => true), platformCredentialService: { resolveForOwner: vi.fn(async () => null), }, diff --git a/apps/builder/__tests__/workspace-members-actions.test.ts b/apps/builder/__tests__/workspace-members-actions.test.ts index ea18d3c46d..33c1da42cf 100644 --- a/apps/builder/__tests__/workspace-members-actions.test.ts +++ b/apps/builder/__tests__/workspace-members-actions.test.ts @@ -19,6 +19,7 @@ const { mockUserFindFirst, mockWorkspaceFindById, mockWorkspaceMemberServiceDelete, + mockWorkspaceMemberServiceListByWorkspaceId, } = vi.hoisted(() => { const mockInsertReturning = vi.fn() const mockInsertValues = vi.fn(() => ({ returning: mockInsertReturning })) @@ -41,6 +42,7 @@ const { mockGetCurrentUserAndTargetWorkspace: vi.fn(), mockInsertReturning, mockWorkspaceMemberServiceDelete: vi.fn(), + mockWorkspaceMemberServiceListByWorkspaceId: vi.fn(), mockInsertValues, mockInvalidateCacheByTags: vi.fn(), mockIsCommunity: vi.fn(), @@ -80,6 +82,7 @@ vi.mock("@chatbotx.io/business", () => ({ }, workspaceMemberService: { delete: mockWorkspaceMemberServiceDelete, + listByWorkspaceId: mockWorkspaceMemberServiceListByWorkspaceId, }, workspaceService: { findById: mockWorkspaceFindById, @@ -488,19 +491,22 @@ describe("deleteWorkspaceMemberAction", () => { mockCurrentMember() }) - test("rejects deleting the workspace owner", async () => { + test("rejects deleting the last workspace owner", async () => { mockFindOrFail.mockResolvedValue({ id: MEMBER_ID, userId: MEMBER_USER_ID, workspaceId: WORKSPACE_ID, role: "owner", }) + mockWorkspaceMemberServiceListByWorkspaceId.mockResolvedValue([ + { id: MEMBER_ID, role: "owner" }, + ]) await expect( (deleteWorkspaceMemberAction as (props: unknown) => Promise)( deleteActionCtx(), ), - ).rejects.toThrow("You cannot delete the owner of the workspace") + ).rejects.toThrow("You cannot delete the last owner of the workspace") expect(mockWorkspaceMemberServiceDelete).not.toHaveBeenCalled() }) diff --git a/apps/builder/messages/ar.json b/apps/builder/messages/ar.json index 9b4863f173..e34db829e3 100644 --- a/apps/builder/messages/ar.json +++ b/apps/builder/messages/ar.json @@ -1862,7 +1862,9 @@ "label": "الردود المحفوظة" }, "role": { - "label": "الدور" + "label": "الدور", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "الخطة" diff --git a/apps/builder/messages/da.json b/apps/builder/messages/da.json index 654c1eb958..171270596a 100644 --- a/apps/builder/messages/da.json +++ b/apps/builder/messages/da.json @@ -1788,7 +1788,9 @@ "label": "Saved Replies" }, "role": { - "label": "Role" + "label": "Role", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "Plan" diff --git a/apps/builder/messages/de.json b/apps/builder/messages/de.json index 3d4e12a93f..053d469341 100644 --- a/apps/builder/messages/de.json +++ b/apps/builder/messages/de.json @@ -1788,7 +1788,9 @@ "label": "Gespeicherte Antworten" }, "role": { - "label": "Rolle" + "label": "Rolle", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "Tarif" diff --git a/apps/builder/messages/en.json b/apps/builder/messages/en.json index 22783ad1ba..1aab794559 100644 --- a/apps/builder/messages/en.json +++ b/apps/builder/messages/en.json @@ -1871,7 +1871,9 @@ "label": "Saved Replies" }, "role": { - "label": "Role" + "label": "Role", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "Plan" diff --git a/apps/builder/messages/es.json b/apps/builder/messages/es.json index 5f19ad2908..bc3d58c70d 100644 --- a/apps/builder/messages/es.json +++ b/apps/builder/messages/es.json @@ -1862,7 +1862,9 @@ "label": "Respuestas guardadas" }, "role": { - "label": "Rol" + "label": "Rol", + "owner": "Propietario", + "agent": "Agente" }, "plan": { "label": "Plan" diff --git a/apps/builder/messages/fi.json b/apps/builder/messages/fi.json index 06a4c7b747..b00074ffdf 100644 --- a/apps/builder/messages/fi.json +++ b/apps/builder/messages/fi.json @@ -1788,7 +1788,9 @@ "label": "Tallennetut vastaukset" }, "role": { - "label": "Rooli" + "label": "Rooli", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "Tilaus" diff --git a/apps/builder/messages/fr.json b/apps/builder/messages/fr.json index 729ed3672d..1ab6be0d9e 100644 --- a/apps/builder/messages/fr.json +++ b/apps/builder/messages/fr.json @@ -1788,7 +1788,9 @@ "label": "Réponses enregistrées" }, "role": { - "label": "Rôle" + "label": "Rôle", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "Forfait" diff --git a/apps/builder/messages/he.json b/apps/builder/messages/he.json index 2fc10243ce..8fd69b4d8c 100644 --- a/apps/builder/messages/he.json +++ b/apps/builder/messages/he.json @@ -4344,7 +4344,9 @@ "label": "תשובות שמורות" }, "role": { - "label": "תפקיד" + "label": "תפקיד", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "תוכנית" diff --git a/apps/builder/messages/id.json b/apps/builder/messages/id.json index 4c9ad49149..35bca7146d 100644 --- a/apps/builder/messages/id.json +++ b/apps/builder/messages/id.json @@ -1788,7 +1788,9 @@ "label": "Balasan Tersimpan" }, "role": { - "label": "Peran" + "label": "Peran", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "Paket" diff --git a/apps/builder/messages/it.json b/apps/builder/messages/it.json index c0adb66ea6..f04ab744e3 100644 --- a/apps/builder/messages/it.json +++ b/apps/builder/messages/it.json @@ -1101,7 +1101,9 @@ "label": "Risposte salvate" }, "role": { - "label": "Ruolo" + "label": "Ruolo", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "Piano" diff --git a/apps/builder/messages/ja.json b/apps/builder/messages/ja.json index f0ba0f3c06..3a839333ea 100644 --- a/apps/builder/messages/ja.json +++ b/apps/builder/messages/ja.json @@ -1788,7 +1788,9 @@ "label": "保存済みの返信" }, "role": { - "label": "ロール" + "label": "ロール", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "プラン" diff --git a/apps/builder/messages/nl.json b/apps/builder/messages/nl.json index a8ff74ca97..a79c194fa0 100644 --- a/apps/builder/messages/nl.json +++ b/apps/builder/messages/nl.json @@ -1601,7 +1601,9 @@ "label": "Opgeslagen antwoorden" }, "role": { - "label": "Rol" + "label": "Rol", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "Abonnement" diff --git a/apps/builder/messages/pt-BR.json b/apps/builder/messages/pt-BR.json index af7d43ecf3..d1dbd6b12a 100644 --- a/apps/builder/messages/pt-BR.json +++ b/apps/builder/messages/pt-BR.json @@ -1788,7 +1788,9 @@ "label": "Respostas salvas" }, "role": { - "label": "Função" + "label": "Função", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "Plano" diff --git a/apps/builder/messages/pt-PT.json b/apps/builder/messages/pt-PT.json index 94dfa20647..c8c338f6c3 100644 --- a/apps/builder/messages/pt-PT.json +++ b/apps/builder/messages/pt-PT.json @@ -1788,7 +1788,9 @@ "label": "Respostas guardadas" }, "role": { - "label": "Função" + "label": "Função", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "Plano" diff --git a/apps/builder/messages/ro.json b/apps/builder/messages/ro.json index a1ce7302e1..40e90a8d08 100644 --- a/apps/builder/messages/ro.json +++ b/apps/builder/messages/ro.json @@ -2570,7 +2570,9 @@ "label": "Răspunsuri salvate" }, "role": { - "label": "Rol" + "label": "Rol", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "Plan" diff --git a/apps/builder/messages/sv.json b/apps/builder/messages/sv.json index 3d51c27ad2..eb570eaa39 100644 --- a/apps/builder/messages/sv.json +++ b/apps/builder/messages/sv.json @@ -2441,7 +2441,9 @@ "label": "Försöksmeddelande" }, "role": { - "label": "Roll" + "label": "Roll", + "owner": "Owner", + "agent": "Agent" }, "savePayloadToCustomField": { "label": "Spara nyttolast i anpassat fält" diff --git a/apps/builder/messages/tr.json b/apps/builder/messages/tr.json index 675c0ca1c4..21a85f82c0 100644 --- a/apps/builder/messages/tr.json +++ b/apps/builder/messages/tr.json @@ -1862,7 +1862,9 @@ "label": "Kayıtlı Yanıtlar" }, "role": { - "label": "Rol" + "label": "Rol", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "Plan" diff --git a/apps/builder/messages/vi.json b/apps/builder/messages/vi.json index 4409389b49..93ea22f5f2 100644 --- a/apps/builder/messages/vi.json +++ b/apps/builder/messages/vi.json @@ -1928,7 +1928,9 @@ "label": "Publishable Key" }, "role": { - "label": "Role" + "label": "Role", + "owner": "Owner", + "agent": "Agent" }, "secretKey": { "label": "Secret Key" diff --git a/apps/builder/messages/zh-CN.json b/apps/builder/messages/zh-CN.json index 0e900862da..84e3550399 100644 --- a/apps/builder/messages/zh-CN.json +++ b/apps/builder/messages/zh-CN.json @@ -2444,7 +2444,9 @@ "label": "重试消息" }, "role": { - "label": "角色" + "label": "角色", + "owner": "Owner", + "agent": "Agent" }, "savePayloadToCustomField": { "label": "将负载保存到自订字段" diff --git a/apps/builder/messages/zh-TW.json b/apps/builder/messages/zh-TW.json index 90a0129a47..f2bacf14a3 100644 --- a/apps/builder/messages/zh-TW.json +++ b/apps/builder/messages/zh-TW.json @@ -1101,7 +1101,9 @@ "label": "已儲存的回覆" }, "role": { - "label": "角色" + "label": "角色", + "owner": "Owner", + "agent": "Agent" }, "plan": { "label": "方案" diff --git a/apps/builder/next.config.ts b/apps/builder/next.config.ts index 7b74e2d0ef..8a732328f2 100644 --- a/apps/builder/next.config.ts +++ b/apps/builder/next.config.ts @@ -38,18 +38,30 @@ const nextConfig: NextConfig = { serverActions: { bodySizeLimit: "20mb", }, + // Cap page-data workers at 2. Without an explicit count Next scales them off + // free RAM (`floor(freemem/1GB)`, min 4) — on a 14GB dev box with an IDE + + // Docker open that meant 11 workers × ~600MB each, and `next build` died at + // the final bundling phase (empty `.next/server`, no BUILD_ID). 2 workers is + // slower to collect page data but the build actually finishes. + cpus: 2, // Additive to Next's built-in default list, which already covers // lucide-react. `@chatbotx.io/ui` doesn't belong here: it's imported via // per-file subpaths and its root export is not a re-export barrel, so // there is nothing for this optimization to rewrite. optimizePackageImports: ["@icons-pack/react-simple-icons"], // turbopackServerFastRefresh: false, + // Dev: the persistent Turbopack filesystem cache (`.next/dev/cache/turbopack`) + // balloons to many GB on a long-lived dev machine and, on a 14GB-RAM box, + // drags the dev server into swap thrash (builds stall, page hangs). We keep + // the in-memory cache (fast during a session) but never persist to disk. + turbopackFileSystemCacheForDev: false, // The Docker build starts from a clean layer and `.next/cache` is not // persisted across CI runs, so this cache is written and never read. turbopackFileSystemCacheForBuild: false, }, poweredByHeader: false, async rewrites() { + const wsUrl = env.NEXT_PUBLIC_INTERNAL_WS_URL const alwaysRewrites = [ { source: "/assets/:path*", @@ -59,6 +71,7 @@ const nextConfig: NextConfig = { source: "/zalo_verifier:verifier.html", destination: "/api/zalo-verifier/:verifier", }, + { source: "/ws/:path*", destination: `${wsUrl}/:path*` }, ] if (process.env.NODE_ENV !== "development") { @@ -67,7 +80,6 @@ const nextConfig: NextConfig = { // Local dev: production routes /ws, /storage, /manage/*, and /portal/* // via load balancer / Caddy - const wsUrl = env.NEXT_PUBLIC_INTERNAL_WS_URL const s3Bucket = process.env.S3_BUCKET ?? "chatbotx" const s3Endpoint = process.env.S3_ENDPOINT ?? "http://localhost:9000" const portalUrl = process.env.PORTAL_INTERNAL_URL ?? "http://localhost:3201" @@ -78,7 +90,6 @@ const nextConfig: NextConfig = { return { afterFiles: [ ...alwaysRewrites, - { source: "/ws/:path*", destination: `${wsUrl}/:path*` }, { source: "/storage/:path*", destination: `${s3Endpoint}/${s3Bucket}/:path*`, diff --git a/apps/builder/package.json b/apps/builder/package.json index 1453182992..e0c260523e 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -4,13 +4,13 @@ "private": true, "type": "module", "scripts": { - "build": "SKIP_ENV_CHECK=true dotenv -e ../../.env -- next build", + "build": "node scripts/build-builder.mjs", "check-types": "NODE_OPTIONS=--max-old-space-size=8192 tsc --noEmit", - "dev": "dotenv -e .env -e ../../.env -- next dev -p 3123 | pino-pretty", + "dev": "node scripts/clean-next-cache.mjs && dotenv -e .env -e ../../.env -- next dev -p 3123 | pino-pretty", "https": "dotenv -e .env -e ../../.env -- next dev -p 3123 --experimental-https", "i18n:check": "i18n-check --source en --locales messages", "lint": "pnpm i18n:check", - "start": "next start", + "start": "node scripts/clean-next-cache.mjs && dotenv -e .env -e ../../.env -- next start -p 3123", "test": "vitest run --passWithNoTests", "test:watch": "vitest" }, diff --git a/apps/builder/scripts/build-builder.mjs b/apps/builder/scripts/build-builder.mjs new file mode 100644 index 0000000000..643ba4e22c --- /dev/null +++ b/apps/builder/scripts/build-builder.mjs @@ -0,0 +1,34 @@ +#!/usr/bin/env node +// Cross-platform build entrypoint for the builder. +// +// Caps the Node heap so `next build` can't OOM a low-RAM machine (14GB dev box +// or CI) even when heavy apps (IDE, Docker, Discord) are running. Without a cap, +// `next build` can claim 8GB+ and get killed / freeze the whole machine via swap +// thrash. This is the permanent, baked-in version of the old manual: +// NODE_OPTIONS="--max-old-space-size=4096" nice -n 19 taskset -c 0-3 ... +// (the nice/taskset part is a Linux nicety; the heap cap is the fix that matters +// and it works on Windows too). +// +// Heap override: NEXT_BUILD_HEAP (e.g. "6144"). Runs through dotenv so local +// `.env` vars load exactly like the old inline script did. +import { spawn } from "node:child_process" + +const heap = process.env.NEXT_BUILD_HEAP ?? "4096" +process.env.NODE_OPTIONS = `--max-old-space-size=${heap}` +process.env.SKIP_ENV_CHECK = "true" + +console.log( + `[build-builder] next build con heap ${heap}MB (NODE_OPTIONS=${process.env.NODE_OPTIONS})`, +) + +const child = spawn( + "pnpm", + ["exec", "dotenv", "-e", "../../.env", "--", "next", "build"], + { stdio: "inherit", shell: true, env: process.env }, +) + +child.on("error", (err) => { + console.error("[build-builder] no se pudo iniciar:", err) + process.exit(1) +}) +child.on("exit", (code) => process.exit(code ?? 1)) diff --git a/apps/builder/scripts/clean-next-cache.mjs b/apps/builder/scripts/clean-next-cache.mjs new file mode 100644 index 0000000000..a7c1d4ca66 --- /dev/null +++ b/apps/builder/scripts/clean-next-cache.mjs @@ -0,0 +1,29 @@ +#!/usr/bin/env node +// Removes Next.js persistent caches that balloon to many GB over time on a +// long-lived dev machine: +// - .next/cache -> webpack build cache (leftover from older `next build`) +// - .next/dev/cache -> Turbopack persistent dev cache +// Safe for both `dev` and `start`: build OUTPUT (.next/server, .next/static, +// routes, etc.) is intentionally NOT touched, so `next start` still serves a +// valid build. Cross-platform (Node fs, not `rm -rf`). +import { rm } from "node:fs/promises" +import { resolve } from "node:path" + +const nextDir = resolve(import.meta.dirname, "..", ".next") +const targets = [["cache"], ["dev", "cache"]].map((parts) => + resolve(nextDir, ...parts), +) + +let removed = 0 +for (const dir of targets) { + try { + await rm(dir, { recursive: true, force: true }) + removed++ + console.log(`[clean-next-cache] removed ${dir.replace(process.cwd(), ".")}`) + } catch (err) { + console.warn(`[clean-next-cache] skip ${dir}: ${err.message}`) + } +} +if (removed === 0) { + console.log("[clean-next-cache] nothing to clean") +} diff --git a/apps/builder/src/app/(no-sidebar)/channels/create/page.tsx b/apps/builder/src/app/(no-sidebar)/channels/create/page.tsx index 97dc898432..bd1567c891 100644 --- a/apps/builder/src/app/(no-sidebar)/channels/create/page.tsx +++ b/apps/builder/src/app/(no-sidebar)/channels/create/page.tsx @@ -1,4 +1,8 @@ -import { platformCredentialService, tenantService } from "@chatbotx.io/business" +import { + isPlatformAdmin, + platformCredentialService, + tenantService, +} from "@chatbotx.io/business" import type { ChannelType } from "@chatbotx.io/database/partials" import { getIdFromParams } from "@chatbotx.io/utils" import { notFound, redirect } from "next/navigation" @@ -14,7 +18,7 @@ import WhatsappCreate from "@/features/integration-whatsapp/components/whatsapp- import { WHATSAPP_OAUTH_CALLBACK_PATH } from "@/features/integration-whatsapp/libs/embedded-signup" import { generateZaloRedirectUri } from "@/features/integration-zalo/libs/zalo" import { requireWorkspacePermission } from "@/lib/auth/require-workspace-permission" -import { getCurrentUserId } from "@/lib/auth/utils" +import { getCurrentUser } from "@/lib/auth/utils" import { resolvePlatformOwnerId } from "@/lib/platform-credential-owner" import { buildProviderCallbackUrl } from "@/lib/provider-origin" @@ -37,12 +41,22 @@ export default async function CreateChannelPage(props: CreateChannelPageProps) { const selectedChannel = searchParams.channel - const userId = await getCurrentUserId() - if (!userId) { + const user = await getCurrentUser() + if (!user) { return notFound() } - const platformOwnerId = await resolvePlatformOwnerId({ userId, workspaceId }) + // Fibrazo fork: only the platform admin may create a new workspace via the + // first-channel flow. Existing members connect channels inside their + // workspace (that path already requires superAdmin above). + if (!(workspaceId || (await isPlatformAdmin(user)))) { + redirect("/") + } + + const platformOwnerId = await resolvePlatformOwnerId({ + userId: user.id, + workspaceId, + }) // Two-tier channel-visibility policy (platform admin + white-label owner). // Pure UI gate — checked before every create branch (including the diff --git a/apps/builder/src/app/page.tsx b/apps/builder/src/app/page.tsx index e3b72722f5..409375dba3 100644 --- a/apps/builder/src/app/page.tsx +++ b/apps/builder/src/app/page.tsx @@ -89,6 +89,7 @@ export default async function MainPage() {
- +
diff --git a/apps/builder/src/components/public-env-script.tsx b/apps/builder/src/components/public-env-script.tsx index 963747dfb8..a0501ae98a 100644 --- a/apps/builder/src/components/public-env-script.tsx +++ b/apps/builder/src/components/public-env-script.tsx @@ -1,3 +1,5 @@ +import Script from "next/script" + const PUBLIC_ENV_KEYS = [ "NEXT_PUBLIC_BUILDER_URL", "NEXT_PUBLIC_BROKER_URL", @@ -14,11 +16,13 @@ export function PublicEnvScript() { env[key] = process.env[key] } return ( -