# AI Control Center Корпоративный Next.js-интерфейс для Aegida Gate. Приложение делегирует вход в `user-service`, хранит историю диалогов в PostgreSQL и оставляет браузер на same-origin `/api/...`: адрес Gate и инфраструктурные настройки доступны только серверным Route Handlers. ## Локальный запуск Нужны два внешних сервиса: - `user-service` на `http://localhost:8080`; - `aegida-gate` на `http://localhost:8082` (порт `8082` выбран, чтобы не конфликтовать с `user-service`). В Gate настройте `AEGIDA_GATE_PORT=8082`, `JWT_ISSUER=corp-ui`, `JWT_AUDIENCE=aegida-gate` и `JWT_HS256_SECRET`, равный `JWT_SECRET` из corp-ui. Затем: ```bash docker compose up -d db minio cp .env.example .env.local npm install npm run db:migrate npm run dev ``` Интерфейс доступен на [http://localhost:3000](http://localhost:3000), PostgreSQL — на `localhost:5433`. MinIO нужен только для чтения legacy-вложений, созданных до перехода на Gate Files API; новые файлы туда не записываются. Для контейнерного запуска при уже работающих host-сервисах используйте `docker compose up --build -d`. Контейнер обращается к `user-service` на `host.docker.internal:8080`, а к Gate — на `host.docker.internal:8082`. ## Переменные окружения ```dotenv DATABASE_URL=postgresql://ai-control-chat-ui:ai-control-chat-ui@localhost:5433/ai_control_chat_ui JWT_SECRET=replace-with-at-least-32-random-characters JWT_ISSUER=corp-ui JWT_AUDIENCE=aegida-gate AEGIDA_TENANT_ID=tenant-1 AEGIDA_GATE_URL=http://localhost:8082 AUTH_SEED_ENABLED=false AUTH_USER_ID=00000000-0000-4000-8000-000000000001 AUTH_EMAIL=demo@ai-control.local AUTH_PASSWORD=Demo1234! NEXT_PUBLIC_APP_URL=http://localhost:3000 USER_SERVICE_URL=http://localhost:8080 S3_ENDPOINT=http://localhost:9000 S3_REGION=us-east-1 S3_BUCKET=ai-control-chat-attachments S3_ACCESS_KEY_ID=ai-control-chat-ui S3_SECRET_ACCESS_KEY=ai-control-chat-ui S3_FORCE_PATH_STYLE=true ``` `AEGIDA_GATE_URL` — строго server-only переменная: не добавляйте к ней префикс `NEXT_PUBLIC_`. В браузер не попадают ни base URL Gate, ни общий service key. Corp-ui пересылает Gate исходный JWT вошедшего пользователя. JWT должен быть согласован с Gate: - `JWT_SECRET` в corp-ui равен `JWT_HS256_SECRET` в Gate; - `JWT_ISSUER` и `JWT_AUDIENCE` имеют одинаковые значения в обоих сервисах; - `AEGIDA_TENANT_ID` выпускается как claim `tenant_id`; - `external_user_id` берётся из профиля `user-service` и передаётся канонической десятичной строкой. `AUTH_*` используются только при явно включённом legacy seed и не заменяют интерактивную аутентификацию через `USER_SERVICE_URL`. Все демонстрационные секреты необходимо заменить за пределами локальной среды. Никогда не коммитьте `.env.local`. ## Интеграция с Aegida Gate ### Модели `GET /api/models` получает OpenAI-совместимый каталог `/v1/models` от имени текущего пользователя. Идентификаторы моделей динамические и валидируются как стабильные slugs; безопасный default/fallback — `auto`. Выбор сохраняется только после повторной серверной проверки каталога. ### Chat и streaming Клиент отправляет в `POST /api/chat` только локальный контракт: ```json { "conversationId": "optional-existing-conversation-id", "model": "auto", "content": "Привет", "attachmentIds": ["optional-local-attachment-id"] } ``` Route Handler собирает историю из PostgreSQL и вызывает `POST /v1/chat/completions` с `stream:true`. OpenAI SSE разбирается инкрементально; браузер по-прежнему получает plain UTF-8 поток и заголовки `X-Conversation-Id`, `X-User-Message-Id`, `X-Assistant-Message-Id`. Upstream payload и диагностические тела не раскрываются клиенту. Для повторения последнего прерванного/ошибочного ответа используется `{ "conversationId": "...", "model": "auto", "retry": true }`. ### Files API Загрузка двухшаговая: 1. corp-ui передаёт исходный multipart `file` и `purpose=user_data` в `POST /v1/files` с JWT пользователя; 2. в PostgreSQL сохраняются локальные метаданные и opaque `gate_file_id`, а Chat получает native part `{ "type":"file", "file": { "file_id":"file-..." } }`. Ни S3 key, ни presigned URL не отправляются на inference. Миграция сохраняет старые `object_key`: такие legacy-строки продолжают скачиваться через приватный MinIO до окончания retention. После удаления/переноса всех legacy-строк можно отдельной миграцией убрать S3 runtime wiring и bucket bootstrap. ### Эффективный баланс `GET /api/me/quota?model=...` проксирует `/v1/aegida/quota`. Это информационный authoritative snapshot, а не резервирование токенов: UI не уменьшает значение оптимистически и обновляет его после model switch и каждого turn. Отправка блокируется только при явном `exhausted:true`; сбой quota dependency показывает `Баланс недоступен`, но не блокирует Chat. ## Проверка ```bash npm test npm run lint npm run build ./scripts/smoke-db.sh git diff --check ``` Smoke-скрипт печатает `SKIP` и завершается успешно, если отсутствуют Docker, `user-service` или готовый Gate. Для полного smoke в `user-service` должна существовать учётная запись из `SMOKE_EMAIL`/`SMOKE_PASSWORD` (по умолчанию demo-значения из Compose), а Gate должен доверять тому же JWT contract. ## Kubernetes (minikube) Манифесты разворачивают corp-ui, его PostgreSQL и временный legacy MinIO в namespace `aegida-services`. Они ожидают уже существующие Service: - `user-service.aegida-services.svc.cluster.local:8080`; - `aegida-gate.aegida-services.svc.cluster.local:8080`. ```bash minikube image build -t ai-control-chat-ui:minikube . kubectl apply -f k8s/namespace.yaml kubectl apply -f k8s/config.yaml kubectl apply -f k8s/postgres.yaml kubectl apply -f k8s/minio.yaml kubectl apply -f k8s/app.yaml kubectl -n aegida-services port-forward service/ai-control-chat-ui 3000:3000 ``` Ingress намеренно отсутствует. Development Secret в `k8s/config.yaml` необходимо заменить; его `JWT_SECRET` должен совпадать с `JWT_HS256_SECRET` деплоймента Gate. Дополнительные проверки описаны в [k8s/README.md](k8s/README.md).