diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml index 6f7c503..ee301ea 100644 --- a/.github/workflows/build-images.yml +++ b/.github/workflows/build-images.yml @@ -94,6 +94,7 @@ jobs: LOG_TAGS="${REGISTRY}/flowly/log-consumer:${TAG}" AI_API_TAGS="${REGISTRY}/flowly/ai-api:${TAG}" AI_WORKER_TAGS="${REGISTRY}/flowly/ai-worker:${TAG}" + AI_INDEXER_TAGS="${REGISTRY}/flowly/ai-indexer:${TAG}" # Add latest tag if requested if [ "${{ inputs.push_latest }}" = "true" ]; then @@ -103,6 +104,7 @@ jobs: LOG_TAGS="${LOG_TAGS}"$'\n'"${REGISTRY}/flowly/log-consumer:${ENV}-latest" AI_API_TAGS="${AI_API_TAGS}"$'\n'"${REGISTRY}/flowly/ai-api:${ENV}-latest" AI_WORKER_TAGS="${AI_WORKER_TAGS}"$'\n'"${REGISTRY}/flowly/ai-worker:${ENV}-latest" + AI_INDEXER_TAGS="${AI_INDEXER_TAGS}"$'\n'"${REGISTRY}/flowly/ai-indexer:${ENV}-latest" fi { @@ -124,6 +126,9 @@ jobs: echo "ai_worker_tags<> $GITHUB_OUTPUT - name: Build and push NestJS API image @@ -195,6 +200,17 @@ jobs: cache-from: type=gha,scope=ai-worker cache-to: type=gha,mode=max,scope=ai-worker + - name: Build and push AI Indexer image + uses: docker/build-push-action@v5 + with: + context: apps/ai-agent + file: apps/ai-agent/ai-indexer/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.prepare-tags.outputs.ai_indexer_tags }} + cache-from: type=gha,scope=ai-indexer + cache-to: type=gha,mode=max,scope=ai-indexer + - name: Scan images for vulnerabilities uses: aquasecurity/trivy-action@master with: @@ -220,3 +236,4 @@ jobs: echo "| Log Consumer | \`${{ steps.login-ecr.outputs.registry }}/flowly/log-consumer:${{ steps.set-tag.outputs.image_tag }}\` |" >> $GITHUB_STEP_SUMMARY echo "| AI API | \`${{ steps.login-ecr.outputs.registry }}/flowly/ai-api:${{ steps.set-tag.outputs.image_tag }}\` |" >> $GITHUB_STEP_SUMMARY echo "| AI Worker | \`${{ steps.login-ecr.outputs.registry }}/flowly/ai-worker:${{ steps.set-tag.outputs.image_tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| AI Indexer | \`${{ steps.login-ecr.outputs.registry }}/flowly/ai-indexer:${{ steps.set-tag.outputs.image_tag }}\` |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7d4271..d0ee85a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -412,7 +412,7 @@ jobs: env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: - args: --sarif-file-output=snyk-api-springboot.sarif --file=apps/api-springboot/build.gradle.kts + args: --sarif-file-output=snyk-api-springboot.sarif --file=apps/api-springboot/build.gradle command: test - name: Upload Snyk results (api-springboot) diff --git a/ansible/on-premise/environments/production/deploy.sh b/ansible/on-premise/environments/production/deploy.sh index a8028f2..30c07ea 100755 --- a/ansible/on-premise/environments/production/deploy.sh +++ b/ansible/on-premise/environments/production/deploy.sh @@ -78,6 +78,7 @@ LOG SERVICES: web Next.js frontend ai-api AI API ai-worker AI Worker + ai-indexer AI Indexer ollama Ollama postgres, postgresql, db PostgreSQL database redis Redis cache @@ -207,7 +208,7 @@ deploy_application() { if [[ -z "$ARG1" ]]; then # No args: default to latest, all : - elif [[ "$ARG1" == "all" ]] || [[ "$ARG1" =~ , ]] || [[ "$ARG1" =~ ^(nest|spring|web|log-consumer|ai-all|ai-api|ai-worker|ollama)$ ]]; then + elif [[ "$ARG1" == "all" ]] || [[ "$ARG1" =~ , ]] || [[ "$ARG1" =~ ^(nest|spring|web|log-consumer|ai-all|ai-api|ai-worker|ai-indexer|ollama)$ ]]; then # First arg is target: assume latest tag TARGETS_RAW="$ARG1" if [[ -n "$ARG2" ]]; then @@ -225,10 +226,10 @@ deploy_application() { # Expand 'ai-all' to individual components if [[ "$TARGETS_RAW" == "ai-all" ]]; then - TARGETS_RAW="ai-api,ai-worker,ollama" + TARGETS_RAW="ai-api,ai-worker,ai-indexer,ollama" elif [[ "$TARGETS_RAW" == "ai" ]]; then # Backward compatibility or shorthand - TARGETS_RAW="ai-api,ai-worker,ollama" + TARGETS_RAW="ai-api,ai-worker,ai-indexer,ollama" fi # Validate targets @@ -236,9 +237,9 @@ deploy_application() { IFS=',' read -r -a _targets <<< "$TARGETS_RAW" for x in "${_targets[@]}"; do case "$x" in - nest|spring|web|log-consumer|ai-api|ai-worker|ollama) ;; + nest|spring|web|log-consumer|ai-api|ai-worker|ai-indexer|ollama) ;; *) - echo "ERROR: Unknown target '$x'. Allowed: all, nest, spring, web, log-consumer, ai-all, ai-api, ai-worker, ollama (comma-separated)." + echo "ERROR: Unknown target '$x'. Allowed: all, nest, spring, web, log-consumer, ai-all, ai-api, ai-worker, ai-indexer, ollama (comma-separated)." exit 1 ;; esac @@ -517,7 +518,7 @@ show_logs() { echo "ERROR: Service name required" echo "Usage: $0 logs " echo - echo "Services: api, api-springboot, web, postgres, redis, prometheus, grafana, ai-api, ai-worker, ollama" + echo "Services: api, api-springboot, web, postgres, redis, prometheus, grafana, ai-api, ai-worker, ai-indexer, ollama" echo echo "Aliases:" echo " nest, api → NestJS API" @@ -539,6 +540,7 @@ show_logs() { grafana) service="grafana" ;; ai|ai-api) service="ai-api" ;; ai-worker) service="ai-worker" ;; + ai-indexer) service="ai-indexer" ;; ollama) service="ollama" ;; *) # Allow exact matches for other services if they exist diff --git a/ansible/on-premise/roles/app-deploy/handlers/main.yml b/ansible/on-premise/roles/app-deploy/handlers/main.yml index 8747a3f..e1d0a44 100644 --- a/ansible/on-premise/roles/app-deploy/handlers/main.yml +++ b/ansible/on-premise/roles/app-deploy/handlers/main.yml @@ -4,3 +4,4 @@ ansible.builtin.shell: | cd "{{ app_base_dir }}" docker compose up -d --force-recreate --remove-orphans + changed_when: true diff --git a/ansible/on-premise/roles/app-deploy/tasks/database.yml b/ansible/on-premise/roles/app-deploy/tasks/database.yml index 8a49de9..78e440a 100644 --- a/ansible/on-premise/roles/app-deploy/tasks/database.yml +++ b/ansible/on-premise/roles/app-deploy/tasks/database.yml @@ -41,7 +41,7 @@ ansible.builtin.shell: | set -euo pipefail cd "{{ app_base_dir }}" - + if [ -d "infra/postgres/migrations" ]; then for sql_file in $(find infra/postgres/migrations -name "*.sql" | sort); do echo "Applying migration: $sql_file" diff --git a/ansible/on-premise/roles/app-deploy/tasks/deploy.yml b/ansible/on-premise/roles/app-deploy/tasks/deploy.yml index e9ec509..10ac330 100644 --- a/ansible/on-premise/roles/app-deploy/tasks/deploy.yml +++ b/ansible/on-premise/roles/app-deploy/tasks/deploy.yml @@ -46,11 +46,18 @@ force_source: true when: "'ai-worker' in deploy_targets_list" +- name: Pull AI Indexer image + community.docker.docker_image: + name: "{{ ecr_registry }}/{{ ecr_repo_ai_indexer | default('flowly-ai-indexer') }}:{{ image_tag }}" + source: pull + force_source: true + when: "'ai-indexer' in deploy_targets_list" + - name: Deploy application containers ansible.builtin.shell: | set -euo pipefail cd "{{ app_base_dir }}" - + # Clean up existing containers for target services to avoid conflicts # This acts as a forced recreation/rolling update step docker compose -f "{{ docker_compose_file }}" rm -fsv {{ deploy_services_list | join(' ') }} || true diff --git a/ansible/on-premise/roles/app-deploy/tasks/normalize-targets.yml b/ansible/on-premise/roles/app-deploy/tasks/normalize-targets.yml index b77f30c..a20daf5 100644 --- a/ansible/on-premise/roles/app-deploy/tasks/normalize-targets.yml +++ b/ansible/on-premise/roles/app-deploy/tasks/normalize-targets.yml @@ -6,7 +6,7 @@ {{ (deploy_targets is not defined or (deploy_targets | lower) == 'all') | ternary( - ['nest','spring','web','log-consumer','ai-api','ai-worker','ollama'], + ['nest','spring','web','log-consumer','ai-api','ai-worker','ai-indexer','ollama'], (deploy_targets | lower | regex_replace('\\s+', '')).split(',') ) }} @@ -15,7 +15,7 @@ {{ (deploy_targets is not defined or (deploy_targets | lower) == 'all') | ternary( - ['api-nestjs','api-springboot','web','log-consumer','ai-api','ai-worker','ollama','ollama-init'], + ['api-nestjs','api-springboot','web','log-consumer','ai-api','ai-worker','ai-indexer','ollama','ollama-init'], ( (( (deploy_targets | lower | regex_replace('\\s+', '')).split(',') @@ -25,9 +25,10 @@ | map('regex_replace', '^log-consumer$', 'log-consumer') | map('regex_replace', '^ai-api$', 'ai-api') | map('regex_replace', '^ai-worker$', 'ai-worker') + | map('regex_replace', '^ai-indexer$', 'ai-indexer') | map('regex_replace', '^ollama$', 'ollama') | list - | select('match', '^(api-nestjs|api-springboot|web|log-consumer|ai-api|ai-worker|ollama)$') + | select('match', '^(api-nestjs|api-springboot|web|log-consumer|ai-api|ai-worker|ai-indexer|ollama)$') | list ) + ( ('ollama' in (deploy_targets | lower | regex_replace('\\s+', '')).split(',')) | ternary(['ollama-init'], []) diff --git a/apps/api-springboot/src/test/kotlin/com/flowly/api/auth/controller/AuthControllerTest.kt b/apps/api-springboot/src/test/kotlin/com/flowly/api/auth/controller/AuthControllerTest.kt index adc0ea6..cbefecf 100644 --- a/apps/api-springboot/src/test/kotlin/com/flowly/api/auth/controller/AuthControllerTest.kt +++ b/apps/api-springboot/src/test/kotlin/com/flowly/api/auth/controller/AuthControllerTest.kt @@ -79,6 +79,7 @@ class AuthControllerTest { id = 1L, email = request.email, nickname = request.nickname, + workspaceId = "test-workspace-id", createdAt = "2025-12-09T00:00:00Z", ), tokens = @@ -145,6 +146,7 @@ class AuthControllerTest { id = 1L, email = request.email, nickname = "Test User", + workspaceId = "test-workspace-id", createdAt = "2025-12-09T00:00:00Z", ), tokens = @@ -207,6 +209,7 @@ class AuthControllerTest { id = 1L, email = "test@example.com", nickname = "Test User", + workspaceId = "test-workspace-id", createdAt = "2025-12-09T00:00:00Z", ), tokens = @@ -318,6 +321,7 @@ class AuthControllerTest { id = 1L, email = "test@example.com", nickname = "Test User", + workspaceId = "test-workspace-id", createdAt = "2025-12-09T00:00:00Z", ) diff --git a/apps/api/src/chat/chat.service.spec.ts b/apps/api/src/chat/chat.service.spec.ts index a2dac8f..6734500 100644 --- a/apps/api/src/chat/chat.service.spec.ts +++ b/apps/api/src/chat/chat.service.spec.ts @@ -63,6 +63,12 @@ describe('ChatService', () => { provide: CACHE_MANAGER, useValue: mockCacheManager, }, + { + provide: 'REDIS_PUBLISHER_CLIENT', + useValue: { + publish: jest.fn(), + }, + }, ], }).compile(); diff --git a/apps/api/src/todo/todo.service.spec.ts b/apps/api/src/todo/todo.service.spec.ts index bf4379f..4fbe6b8 100644 --- a/apps/api/src/todo/todo.service.spec.ts +++ b/apps/api/src/todo/todo.service.spec.ts @@ -8,7 +8,7 @@ import { Todo } from './todo.entity'; import { User } from '../user/user.entity'; import { createMockRepository } from '../test-utils/mock-repository'; import { createMockCacheManager } from '../test/helpers/cache-mock.helper'; -import {Friendship} from "../friend/friendship.entity"; +import { Friendship } from "../friend/friendship.entity"; describe('TodoService', () => { let service: TodoService; @@ -77,6 +77,9 @@ describe('TodoService', () => { ownerId: mockUser.id, visibility: 'PRIVATE', done: false, + status: 'TODO', + startDate: undefined, + endDate: undefined, }); expect(repository.save).toHaveBeenCalledWith(expectedTodo); expect(result).toEqual(expectedTodo); @@ -96,6 +99,9 @@ describe('TodoService', () => { ownerId: mockUser.id, visibility: 'FRIENDS', done: false, + status: 'TODO', + startDate: undefined, + endDate: undefined, }); expect(result).toEqual(expectedTodo); }); @@ -222,11 +228,11 @@ describe('TodoService', () => { expect(mockQueryBuilder.leftJoinAndSelect).toHaveBeenCalledWith('todo.owner', 'owner'); expect(mockQueryBuilder.where).toHaveBeenCalledWith('todo.visibility = :visibility', { visibility: 'FRIENDS' }); expect(mockQueryBuilder.innerJoin).toHaveBeenCalledWith( - Friendship, - 'friendship', - '(friendship.requesterId = :userId AND friendship.addresseeId = todo.ownerId AND friendship.status = :status) OR ' + - '(friendship.addresseeId = :userId AND friendship.requesterId = todo.ownerId AND friendship.status = :status)', - { userId, status: 'ACCEPTED' } + Friendship, + 'friendship', + '(friendship.requesterId = :userId AND friendship.addresseeId = todo.ownerId AND friendship.status = :status) OR ' + + '(friendship.addresseeId = :userId AND friendship.requesterId = todo.ownerId AND friendship.status = :status)', + { userId, status: 'ACCEPTED' } ); expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith('todo.createdAt', 'DESC'); expect(result).toEqual(friendsTodos); diff --git a/apps/web/src/app/calendar/page.tsx b/apps/web/src/app/calendar/page.tsx index 1617026..56fb5d1 100644 --- a/apps/web/src/app/calendar/page.tsx +++ b/apps/web/src/app/calendar/page.tsx @@ -193,6 +193,7 @@ const CalendarPage = () => { + {/* @ts-ignore */}