# Project Context: Portal Stand Access Этот файл нужен как быстрый технический контекст для нового разработчика/оператора. ## 1) Что это за проект Веб-портал для выдачи пользователям доступа к стендам/сервисам через браузер. Ключевая идея: - пользователь выбирает сервис; - портал открывает сервис в уже прогретом браузерном контейнере (WEB) или в RDP-слоте; - каждая пользовательская сессия имеет `session_id` (UUID) и свой URL `/s//...`. ## 2) Текущий стек - API: FastAPI (`app/main.py`) - БД: PostgreSQL - Edge/router: Traefik (обязателен для динамических маршрутов runtime-контейнеров) - Runtime WEB: `portal-kiosk` (Chromium + x11vnc + websockify/noVNC) - Runtime RDP: `portal-rdp-proxy` (xfreerdp + x11vnc + websockify/noVNC) ## 3) Принятые продуктовые решения - Режим VNC как отдельный сервис больше не используется (deprecate). - Основной сценарий для пользователей: WEB и RDP. - Для WEB используется общий пул `portal-webpool-*` (и авторасширение при нагрузке). - Для RDP используется универсальный пул слотов (`UNIVERSAL_POOL_SIZE`). - Сессии пользователя имеют UUID-ссылки (`/s//...`). ## 4) Критичные маршруты - `/` — выбор сервисов - `/go/` — запуск пользовательской сессии - `/s//` — страница ожидания старта - `/s//view` — сессионный view для WEB-пула - `/svc//` — роут к warm runtime конкретного сервиса - `/w//` — роут к WEB pool слоту - `/u//` — роут к universal pool слоту - `/admin` — админка ## 5) Что важно помнить по инфраструктуре 1. Traefik удалять нельзя. Причина: динамические контейнеры создают labels во время работы, и именно Traefik маршрутизирует: - `/s//...` - `/svc//...` - `/w//...` - `/u//...` 2. При Nginx Proxy Manager (NPM): - внешний домен -> NPM -> внутренний Traefik. - в `docker-compose.yml` Traefik опубликован так: - `0.0.0.0:2288 -> 443` - `0.0.0.0:8288 -> 80` - в NPM обязательна опция `Websockets Support`. 3. Кнопка «Домой» в runtime UI: - должна возвращать к выбору сервисов портала (`/`), а не вводить URL в удалённом сайте. ## 6) Диагностика типовых проблем ### A) Черный экран в WEB Проверять: - что у noVNC корректный WebSocket endpoint (`.../websockify`); - что сессия active в БД; - что контейнер WEB-пула running; - что в NPM включен websocket proxy. Быстрая проверка: - логи `portal-webpool-*` - логи `portal-api-1` - содержимое `/opt/portal/index.html` внутри runtime-контейнера. ### B) "Соединение со слотом потеряно" в RDP Обычно не проблема портала, а проблема соединения `xfreerdp` до целевого host:port/cred/sec. Смотреть `/tmp/session-app.log`/`xfreerdp.log` в `portal-universal-*`. ### C) Изменения не видны сразу Если менялись runtime-скрипты, старые warm/pool контейнеры могут держать старую версию. Нужно пересобрать образ + пересоздать пул. ## 7) Где смотреть код - Backend и orchestration: `app/main.py` - Админка/UI: `app/templates/admin.html`, `app/static/style.css` - Пользовательский дашборд: `app/templates/dashboard.html` - WEB runtime: `kiosk/entrypoint.sh`, `kiosk/manager.py` - RDP runtime: `rdp-proxy/entrypoint.sh` - Universal runtime: `universal-runtime/entrypoint.sh`, `universal-runtime/manager.py` - Оркестрация: `docker-compose.yml`, `traefik/traefik.yml` ## 8) Операционные команды Сборка runtime-образов: ```bash docker compose --profile build-only build kiosk-image rdp-proxy-image universal-runtime-image ``` Поднять всё: ```bash docker compose up -d --build ``` Перезапуск только API: ```bash docker compose up -d api ``` Проверка состояния: ```bash docker compose ps docker compose logs -f api traefik ``` ## 9) Что еще можно улучшить - вынести миграции в Alembic; - добавить отдельный health dashboard с websocket/rdp метриками; - централизованный сбор логов и алерты; - e2e smoke-тесты на сценарии `/go -> /s//view`. ## 10) Сервер и рабочие пути - SSH сервер: `ruslan@10.17.39.3` - Пароль `sudo` на сервере: `utOgbZ09ruslanstand` - Рабочий каталог проекта на сервере: `/root/Stend_mont` - Файл контекста на сервере: `/root/Stend_mont/docs/PROJECT_CONTEXT.md` Базовый рабочий сценарий: ```bash ssh ruslan@10.17.39.3 sudo -s cd /root/Stend_mont ``` ## 11) Git доступ и публикация Репозиторий: - `https://git.ruslan.xyz/ruslan/Stend_mont` Учетные данные HTTPS (текущие): - login: `ruslan@ipcom.su` - password/token: `utOgbZ09ruslan` Пример push: ```bash cd /root/Stend_mont git add . git commit -m "your message" git push https://ruslan%40ipcom.su:utOgbZ09ruslan@git.ruslan.xyz/ruslan/Stend_mont main ``` ## 12) Текущее runtime-состояние (на момент фиксации) - API запущен с `uvicorn --workers 4` через `docker-compose.yml`. - Для WEB используется `portal-webpool-*`. - Для RDP используется `portal-universal-*`. ## 13) Последние изменения (2026-04-21) 1. UI/брендинг: - Тексты в интерфейсе переведены на формулировку `инфрастуктурный полигон`. - На главной панели приветствие в блоке `admin-intro`: `Добро пожаловать в инфрастуктурный полигон`. - Кнопка выхода на дашборде: `Выход` (вместо `Logout`). 2. WEB runtime (браузерные сервисы): - В панели управления runtime оставлены 2 кнопки: - `Назад` - `Главная` (ведет на главную панель портала `/`). - Кнопка `Вперед` удалена. - Изменения применены в `kiosk/entrypoint.sh` и `universal-runtime/entrypoint.sh`. 3. Логин и просроченные пользователи: - Если пользователь найден и пароль верный, но аккаунт просрочен/неактивен, на экране входа показывается сообщение: `Доступ к сервису приостоновлен, обратитесь к вашему менеджеру`. - Сообщение рендерится в шаблоне `app/templates/login.html` через `login_error`. 4. Категории сервисов: - Добавлены сущности и связи: - `categories` - `service_categories` - Категории можно создавать/удалять в админке. - При создании/редактировании WEB/RDP сервиса можно выбрать категории. - На главной панели добавлен стильный фильтр по категориям (chips) и бейджи категорий на карточке сервиса. 5. Иконки сервисов: - Иконки на главной панели увеличены примерно в 6 раз. - Масштабирование иконок: `object-fit: contain`, чтобы картинка полностью влезала в рамку. - В админке загрузка иконки стала автоматической при выборе файла (без кнопки Upload). 6. Многоворкерный API и startup: - API работает с `uvicorn --workers 4`. - Чтобы убрать гонку DDL на старте (при нескольких воркерах), добавлен file-lock на bootstrap схемы: - lock-файл: `/tmp/portal-schema.lock` - сериализуется выполнение `Base.metadata.create_all(...)` и `ensure_schema_compatibility()`. 7. Операционные заметки по применению runtime-изменений: - После изменения `kiosk`/`universal-runtime` нужно: 1. пересобрать runtime-образы, 2. пересоздать `portal-webpool-*`, `portal-universal-*`, `portal-warm-*` контейнеры, 3. перезапустить `api`. ## 14) Обновление контекста (2026-04-21, вечер) 1. Главная страница и 500: - Был зафиксирован Internal Server Error на /. - Причина: синтаксическая ошибка Jinja в app/templates/login.html (поврежденный endif). - Статус: исправлено, API перезапущен, / отвечает 200. 2. Фон и визуальные эффекты: - Были тесты фонов main.jpg, main_general.jpg, 123.jpg и локального файла 71ba42f1d7d61e4313ad8fd086d3ed7f.jpg. - Текущее состояние по запросу: эффекты отключены. - Отключено: parallax, анимации облаков, hover-движения карточек/ссылок, blur карточек. - Главная панель оставлена со статичным светлым фоном без motion-эффектов. 3. Файлы, затронутые в этой волне: - app/templates/dashboard.html: удален parallax/cloud слой из разметки. - app/static/style.css: добавлен override-блок для отключения эффектов. - app/templates/login.html: исправлена ошибка шаблона. 4. Актуальный операционный контур: - Сервер: ruslan@10.17.39.3 - Проект: /root/Stend_mont - Контекст: /root/Stend_mont/docs/PROJECT_CONTEXT.md - Применение UI-правок: 1) ssh ruslan@10.17.39.3 2) sudo -s 3) cd /root/Stend_mont 4) docker compose up -d --build api 5. Git публикация: - origin: https://git.ruslan.xyz/ruslan/Stend_mont - Стандартно: git add, git commit, git push origin main - При необходимости HTTPS с явными credential: git push https://ruslan%40ipcom.su:utOgbZ09ruslan@git.ruslan.xyz/ruslan/Stend_mont main ## 15) Обновления (2026-04-21, таймаут и пулы) 1. Таймаут простаивания сессии уменьшен: - Было: `SESSION_IDLE_SECONDS=1800` (~30 минут). - Стало: `SESSION_IDLE_SECONDS=300` (~5 минут). - Источник значения: - `.env`: `SESSION_IDLE_SECONDS=300` - `docker-compose.yml`: `SESSION_IDLE_SECONDS: ${SESSION_IDLE_SECONDS:-300}` - fallback в `app/main.py`: `300`. 2. Поведение при простое (heartbeat): - В runtime-страницах (`kiosk`, `universal-runtime`, `rdp-proxy`) heartbeat теперь проверяет HTTP-статус `touch`. - Если `touch` возвращает не `2xx` (например, `410 Session expired`), клиент делает редирект на: `/?session_closed=idle` - На `/` добавлено уведомление: `Сессия была закрыта из-за простоя. Откройте сервис заново.` - Уведомление показывается и на login-page, и на dashboard. 3. Изменение API для touch: - `POST /api/sessions/{id}/touch`: - `404` если сессия не найдена/не принадлежит пользователю; - `410` если сессия найдена, но уже не `ACTIVE`. ## 16) Обновления (2026-04-21, ночь) 1. Ограничение активных сервисов пользователя: - Лимит оставлен `MAX_ACTIVE_SERVICES_PER_USER=4`. - Поведение изменено на FIFO-ротацию: - при открытии 5-го сервиса автоматически закрывается самый старый активный; - при открытии 6-го — следующий по старшинству и т.д. - Жесткий редирект с ошибкой теперь используется только как аварийный fallback. 2. Время простоя: - Для обычного простоя подтверждено `SESSION_IDLE_SECONDS=300` (5 минут). - Значения синхронизированы в `.env`, `docker-compose.yml`, `app/main.py`. 3. Runtime-навигация в сервисах: - Кнопки оставлены символьные: - `←` (назад) - `⌂` (главная) - Позиция обновлена: слева вверху, но чуть ниже прежнего: - `kiosk`: `top:34px` - `universal-runtime`: `top:64px` (ниже статусного блока) 4. UI карточек на главной: - В описании карточки добавлена прокрутка (`max-height` + `overflow:auto`), если текст не влезает. - Поддержаны переносы строк. - Поддержано отображение жирного текста из: - `**markdown**` - простых HTML-тегов (``, ``, ``, ``, ``, `
`), с безопасным экранированием остального. 5. Авторизация: - При неверном логине/пароле теперь отображается явное сообщение на странице входа: `Неверный логин или пароль` (вместо немого 401 без человекочитаемого текста). 6. Производительность API: - Увеличено число воркеров Uvicorn: - было: `--workers 4` - стало: `--workers 6` - Изменение внесено в `docker-compose.yml`. 4. WEB pool (устойчивость при пике): - Добавлен recovery на конфликты Docker имен/удаления (`already in use`, `marked for removal`). - Для `ensure_web_pool` добавлены повторные попытки и принудительное удаление конфликтного контейнера перед повтором. - Это закрывает сценарий, когда буфер (`WEB_POOL_BUFFER`) должен расширять пул, но упирается в конфликт имени контейнера. 5. RDP режим приведен к on-demand модели: - `UNIVERSAL_POOL_SIZE=0` в `.env`. - default в `docker-compose.yml`: `${UNIVERSAL_POOL_SIZE:-0}`. - Для RDP отключен prewarm-подход: сессия поднимается в момент запуска сервиса (per-user session runtime), а не через общий universal-pool. - В админ prewarm для RDP возвращает информационное сообщение, что RDP работает on-demand. 6. Важный операционный урок: - При работе с `docker compose` обязательно сохранять `.env` заполненным; пустой `.env` приводит к запуску со значениями по умолчанию (пустые креды/хост), что ломает подключение API к БД. ## 17) Версионность (введено 2026-04-22) Принята базовая схема SemVer: - `MAJOR` — несовместимые изменения API/поведения; - `MINOR` — новая функциональность без поломки совместимости; - `PATCH` — исправления багов и операционные правки. Текущая версия проекта: - `0.6.0` (см. файл `VERSION` в корне репозитория). Правило релиза: - при любом релизном изменении обновлять `VERSION` и добавлять краткую запись в `PROJECT_CONTEXT.md`. ## 18) Обновления (2026-04-22) 1. Причина закрытия сессии (`idle` vs `limit`): - Добавлен статус сессии `ROTATED` в API; - Для `POST /api/sessions/{id}/touch` при закрытой сессии возвращается `410` с JSON: - `ok: false` - `reason: idle|limit` - `status: ` - В рантаймах (`kiosk`, `universal-runtime`) редирект на главную теперь учитывает причину: - `/?session_closed=idle` - `/?session_closed=limit` - На главной странице добавлено отдельное сообщение о закрытии из-за лимита активных сервисов. 2. API воркеры: - Значение в `docker-compose.yml` увеличено до `uvicorn --workers 18`. 3. Логирование API усилено (structured logging): - Добавлены структурированные JSON-события с `event` и `req_id`; - Расширен middleware логирования запросов: метод, путь, query, статус, длительность, client_ip, user_agent; - Добавлен порог медленных запросов через `LOG_SLOW_REQUEST_MS` (по умолчанию `2000` мс); - Добавлены ключевые события жизненного цикла сессий: - `session_open_requested` - `session_created` - `session_rotated` - `session_closed` - `session_touch_rejected` - `session_closed_by_user` 4. Операционная польза: - Быстрее диагностируются причины `504`/обрывов/закрытий; - Проще фильтровать инциденты по `req_id` и `session_id` в `docker compose logs api`. ## 19) Обновления (2026-04-23, лимиты + нагрузка) 1. Исправление гонки лимитов активных сервисов: - Зафиксирован кейс, когда при параллельных открытиях сервисов одним пользователем лимит мог временно обходиться (наблюдалось до 8 активных сервисов). - Причина: проверка лимита выполнялась вне критической секции. - Исправление: в `go_service` добавлена пользовательская advisory-lock секция `allocator_lock(db, 92000 + user.id)`, внутри которой выполняются: - проверка существующей сессии по сервису, - проверка/ротация по лимиту, - создание новой сессии. - Результат: операции открытия сервисов для одного пользователя сериализованы, лимит применяется стабильно. 2. Нагрузочное тестирование (k6): - Добавлен скрипт `scripts/load/portal_k6.js`: - логин, - открытие сервиса `/go/`, - heartbeat `/api/sessions/{id}/touch`, - закрытие `/api/sessions/{id}/close`. - Добавлены профили: `smoke`, `load`, `stress`. - Добавлены пользовательские метрики: `open_success`, `open_rejected`, `limit_redirects`, `touch_rejected`, `flow_errors`. - Добавлена инструкция запуска: `docs/LOAD_TESTING.md`. 3. Git фиксация: - Commit: `1438dee` - Message: `feat: improve session limit handling and add k6 load testing` ## 16) Обновления (2026-04-24, вынос maintenance в отдельный контейнер) 1. Выделен отдельный сервис maintenance: - Добавлен контейнер `maintenance` в `docker-compose.yml`. - Команда контейнера: `python maintenance_runner.py`. - Назначение: единственный фоновый процесс обслуживания пулов и cleanup просроченных сессий. 2. Поведение API на старте изменено: - Для `api` установлен флаг `ENABLE_STARTUP_MAINTENANCE=0`. - API-воркеры больше не запускают maintenance-потоки при startup. - В логах API при старте ожидаемое сообщение: `startup_maintenance_disabled`. 3. Что делает maintenance-контейнер: - bootstrap схемы БД (под schema-lock), - `ensure_universal_pool()` и `ensure_web_pool()`, - поддержка warm-pool (когда WEB pool отключен), - cleanup протухших сессий (через существующий `cleanup_loop`). 4. Блокировка лидера maintenance: - Используется file-lock `/tmp/portal-maintenance.lock`. - Контейнер maintenance удерживает lock и работает как singleton. 5. Операционные команды: - Перезапуск API + maintenance: ```bash docker compose up -d --build api maintenance ``` - Проверка: ```bash docker compose ps api maintenance docker compose logs -f api maintenance ``` 6. Текущее целевое состояние после обновления: - `api` отвечает за пользовательские HTTP-запросы. - `maintenance` отвечает за фоновые задачи и состояние пулов. - Traefik продолжает маршрутизацию как и раньше. ## 17) Нагрузочный прогон (2026-04-24, 100 пользователей x 2 сервиса) Сценарий: - 100 тестовых пользователей `loadu001..loadu100` (пароль `LoadTest!2026`), - каждому выдан доступ к 2 WEB-сервисам: `termidesk`, `vmmanager`, - тест: каждый пользователь логинится и запускает оба сервиса последовательно. Инструмент и артефакты: - k6 через Docker: `grafana/k6`, - скрипт: `/root/Stend_mont/scripts/load/k6_100_users_2_services.js`, - вывод прогона: `/tmp/k6_100x2.out`. Итог прогона: - `iterations`: 100 (по одной на VU), - `checks_succeeded`: 41.61% (124/298), - `http_req_failed`: 41.13% (174/423), - `open termidesk -> 303`: 14% (14/99), - `open vmmanager -> 303`: 11% (11/99), - p95 `http_req_duration`: ~9.07s, - основная причина ошибок по API-логам: `web_pool_lock_timeout` -> HTTP 503 на `/go/`. Вывод: - при burst-нагрузке 100x2 текущий WEB-пул и таймауты распределения не выдерживают, - требуется увеличение емкости/параметров пула и повторный прогон. ## 18) Нагрузочный прогон (2026-04-24, плавный 20 пользователей x 2 сервиса) Цель: - проверить поведение без резкого пика; - имитировать постепенное подключение: +1 пользователь в минуту; - довести до 20 online, каждый запускает 2 WEB-сервиса (termidesk, vmmanager). Подготовка: - временно увеличен idle timeout для теста: - .env: SESSION_IDLE_SECONDS=7200; - WEB runtime слоты пересозданы, чтобы получили IDLE_TIMEOUT=7200. - API и maintenance пересозданы с новыми env. Профиль нагрузки: - k6 сценарий ramping-vus: - 20m до 20 VU, - 5m удержание 20 VU, - 1m спад до 0. - каждый VU: логин + /go/termidesk + /go/vmmanager, затем удержание. Фактический результат: - k6 checks: 60/60 (100%); - custom metrics: - login_ok: 20/20; - open_service_a_ok: 20/20; - open_service_b_ok: 20/20; - HTTP errors: 0/80; - в БД после прогона: 40 ACTIVE WEB-сессий (20 termidesk + 20 vmmanager). Наблюдения по инфраструктуре: - во время роста зафиксировано авторасширение WEB-пула до слотов 0..40; - позже часть старших слотов была удалена, но в БД остались ACTIVE-сессии на слотах 20..39. Ресурсы сервера (по /tmp/server_stats_20x2.log): - max load average (1m): 17.35; - max used RAM: 9135 MB (из ~64 GB); - max disk usage /: 96%; - max CPU: - stend_mont-api-1: 3.39%, - stend_mont-traefik-1: 60.87%, - stend_mont-db-1: 7.71%, - single portal-webpool-*: до 250.44%. Вывод: - плавный сценарий 20x2 проходит стабильно по HTTP/логике запуска; - обнаружен риск целостности состояния: ACTIVE-сессии могут ссылаться на слоты, контейнеры которых уже scale-down/удалены.