Files
Stend_mont/docs/PROJECT_CONTEXT.md
T

28 KiB
Raw Blame History

Project Context: Portal Stand Access

Этот файл нужен как быстрый технический контекст для нового разработчика/оператора.

1) Что это за проект

Веб-портал для выдачи пользователям доступа к стендам/сервисам через браузер.

Ключевая идея:

  • пользователь выбирает сервис;
  • портал открывает сервис в уже прогретом браузерном контейнере (WEB) или в RDP-слоте;
  • каждая пользовательская сессия имеет session_id (UUID) и свой URL /s/<session_id>/....

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/<uuid>/...).

4) Критичные маршруты

  • / — выбор сервисов
  • /go/<slug> — запуск пользовательской сессии
  • /s/<session_id>/ — страница ожидания старта
  • /s/<session_id>/view — сессионный view для WEB-пула
  • /svc/<slug>/ — роут к warm runtime конкретного сервиса
  • /w/<slot>/ — роут к WEB pool слоту
  • /u/<slot>/ — роут к universal pool слоту
  • /admin — админка

5) Что важно помнить по инфраструктуре

  1. Traefik удалять нельзя. Причина: динамические контейнеры создают labels во время работы, и именно Traefik маршрутизирует:
  • /s/<session_id>/...
  • /svc/<slug>/...
  • /w/<slot>/...
  • /u/<slot>/...
  1. При 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.
  1. Кнопка «Домой» в 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-образов:

docker compose --profile build-only build kiosk-image rdp-proxy-image universal-runtime-image

Поднять всё:

docker compose up -d --build

Перезапуск только API:

docker compose up -d api

Проверка состояния:

docker compose ps
docker compose logs -f api traefik

9) Что еще можно улучшить

  • вынести миграции в Alembic;
  • добавить отдельный health dashboard с websocket/rdp метриками;
  • централизованный сбор логов и алерты;
  • e2e smoke-тесты на сценарии /go -> /s/<uuid>/view.

11) Git доступ и публикация

Репозиторий:

  • https://git.ruslan.xyz/ruslan/Stend_mont

Учетные данные HTTPS (текущие):

  • login: ruslan@ipcom.su
  • password/token: utOgbZ09ruslan

Пример push:

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).
  1. WEB runtime (браузерные сервисы):
  • В панели управления runtime оставлены 2 кнопки:
    • Назад
    • Главная (ведет на главную панель портала /).
  • Кнопка Вперед удалена.
  • Изменения применены в kiosk/entrypoint.sh и universal-runtime/entrypoint.sh.
  1. Логин и просроченные пользователи:
  • Если пользователь найден и пароль верный, но аккаунт просрочен/неактивен, на экране входа показывается сообщение: Доступ к сервису приостоновлен, обратитесь к вашему менеджеру.
  • Сообщение рендерится в шаблоне app/templates/login.html через login_error.
  1. Категории сервисов:
  • Добавлены сущности и связи:
    • categories
    • service_categories
  • Категории можно создавать/удалять в админке.
  • При создании/редактировании WEB/RDP сервиса можно выбрать категории.
  • На главной панели добавлен стильный фильтр по категориям (chips) и бейджи категорий на карточке сервиса.
  1. Иконки сервисов:
  • Иконки на главной панели увеличены примерно в 6 раз.
  • Масштабирование иконок: object-fit: contain, чтобы картинка полностью влезала в рамку.
  • В админке загрузка иконки стала автоматической при выборе файла (без кнопки Upload).
  1. Многоворкерный API и startup:
  • API работает с uvicorn --workers 4.
  • Чтобы убрать гонку DDL на старте (при нескольких воркерах), добавлен file-lock на bootstrap схемы:
    • lock-файл: /tmp/portal-schema.lock
    • сериализуется выполнение Base.metadata.create_all(...) и ensure_schema_compatibility().
  1. Операционные заметки по применению 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.
  1. Фон и визуальные эффекты:
  • Были тесты фонов main.jpg, main_general.jpg, 123.jpg и локального файла 71ba42f1d7d61e4313ad8fd086d3ed7f.jpg.
  • Текущее состояние по запросу: эффекты отключены.
  • Отключено: parallax, анимации облаков, hover-движения карточек/ссылок, blur карточек.
  • Главная панель оставлена со статичным светлым фоном без motion-эффектов.
  1. Файлы, затронутые в этой волне:
  • app/templates/dashboard.html: удален parallax/cloud слой из разметки.
  • app/static/style.css: добавлен override-блок для отключения эффектов.
  • app/templates/login.html: исправлена ошибка шаблона.
  1. Git публикация:

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.
  1. Поведение при простое (heartbeat):
  • В runtime-страницах (kiosk, universal-runtime, rdp-proxy) heartbeat теперь проверяет HTTP-статус touch.
  • Если touch возвращает не 2xx (например, 410 Session expired), клиент делает редирект на: /?session_closed=idle
  • На / добавлено уведомление: Сессия была закрыта из-за простоя. Откройте сервис заново.
  • Уведомление показывается и на login-page, и на dashboard.
  1. Изменение 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.
  1. Время простоя:
  • Для обычного простоя подтверждено SESSION_IDLE_SECONDS=300 (5 минут).
  • Значения синхронизированы в .env, docker-compose.yml, app/main.py.
  1. Runtime-навигация в сервисах:
  • Кнопки оставлены символьные:
    • (назад)
    • (главная)
  • Позиция обновлена: слева вверху, но чуть ниже прежнего:
    • kiosk: top:34px
    • universal-runtime: top:64px (ниже статусного блока)
  1. UI карточек на главной:
  • В описании карточки добавлена прокрутка (max-height + overflow:auto), если текст не влезает.
  • Поддержаны переносы строк.
  • Поддержано отображение жирного текста из:
    • **markdown**
    • простых HTML-тегов (<b>, <strong>, <i>, <em>, <u>, <br>), с безопасным экранированием остального.
  1. Авторизация:
  • При неверном логине/пароле теперь отображается явное сообщение на странице входа: Неверный логин или пароль (вместо немого 401 без человекочитаемого текста).
  1. Производительность API:
  • Увеличено число воркеров Uvicorn:
    • было: --workers 4
    • стало: --workers 6
  • Изменение внесено в docker-compose.yml.
  1. WEB pool (устойчивость при пике):
  • Добавлен recovery на конфликты Docker имен/удаления (already in use, marked for removal).
  • Для ensure_web_pool добавлены повторные попытки и принудительное удаление конфликтного контейнера перед повтором.
  • Это закрывает сценарий, когда буфер (WEB_POOL_BUFFER) должен расширять пул, но упирается в конфликт имени контейнера.
  1. 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.
  1. Важный операционный урок:
  • При работе с 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: <SessionStatus>
  • В рантаймах (kiosk, universal-runtime) редирект на главную теперь учитывает причину:
    • /?session_closed=idle
    • /?session_closed=limit
  • На главной странице добавлено отдельное сообщение о закрытии из-за лимита активных сервисов.
  1. API воркеры:
  • Значение в docker-compose.yml увеличено до uvicorn --workers 18.
  1. Логирование 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
  1. Операционная польза:
  • Быстрее диагностируются причины 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), внутри которой выполняются:
    • проверка существующей сессии по сервису,
    • проверка/ротация по лимиту,
    • создание новой сессии.
  • Результат: операции открытия сервисов для одного пользователя сериализованы, лимит применяется стабильно.
  1. Нагрузочное тестирование (k6):
  • Добавлен скрипт scripts/load/portal_k6.js:
    • логин,
    • открытие сервиса /go/<slug>,
    • 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.
  1. 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 просроченных сессий.
  1. Поведение API на старте изменено:
  • Для api установлен флаг ENABLE_STARTUP_MAINTENANCE=0.
  • API-воркеры больше не запускают maintenance-потоки при startup.
  • В логах API при старте ожидаемое сообщение: startup_maintenance_disabled.
  1. Что делает maintenance-контейнер:
  • bootstrap схемы БД (под schema-lock),
  • ensure_universal_pool() и ensure_web_pool(),
  • поддержка warm-pool (когда WEB pool отключен),
  • cleanup протухших сессий (через существующий cleanup_loop).
  1. Блокировка лидера maintenance:
  • Используется file-lock /tmp/portal-maintenance.lock.
  • Контейнер maintenance удерживает lock и работает как singleton.
  1. Операционные команды:
  • Перезапуск API + maintenance:
docker compose up -d --build api maintenance
  • Проверка:
docker compose ps api maintenance
docker compose logs -f api maintenance
  1. Текущее целевое состояние после обновления:
  • 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/<slug>.

Вывод:

  • при 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/удалены.

19) Обновления (2026-04-25, разрешение WEB-сессий и runtime tuning)

  1. Диагностика "узкого экрана" в /s/<session_id>/view:
  • проверен полный путь передачи разрешения:
    • frontend: app/templates/dashboard.html -> query sw/sh для /go/<slug>;
    • backend: app/main.py -> sanitize_client_resolution(...) + dispatch в web/universal pool;
    • runtime: universal-runtime/manager.py -> apply_resolution(...) через xrandr.
  • в реальном кейсе подтверждено, что проблема воспроизводилась при открытии старого URL сессии /s/<id>/view вместо нового запуска через /go/<slug>.
  1. Изменения по разрешению:
  • app/templates/dashboard.html:
    • обновлен расчет sw/sh (приоритет screen.width/screen.height, fallback на viewport).
  • universal-runtime/manager.py:
    • добавлен fallback на дефолтное CHROME_WINDOW_SIZE, если requested mode не применился через xrandr.
  • app/main.py:
    • добавлено диагностическое логирование session_open_resolution (sw/sh и нормализованные client_width/client_height).
  1. Возвращен x11vnc ncache:
  • дефолт X11VNC_FLAGS изменен на:
    • -wait 5 -defer 5 -ncache 10 -threads
  • обновленные файлы:
    • app/main.py
    • kiosk/entrypoint.sh
    • universal-runtime/entrypoint.sh
  1. Применение в runtime:
  • пересобраны образы:
    • stend_mont-api
    • portal-kiosk:latest
    • portal-universal-runtime:latest
  • пересозданы pool-контейнеры:
    • portal-webpool-*
    • при необходимости portal-universal-* и portal-warm-*
  • итоговая проверка: portal-webpool восстановлен до 20/20 running.
  1. Дополнительно:
  • подтверждено, что контейнеры не используют GPU (runtime runc, без --gpus и device mapping);
  • выполнена чистка рабочей папки от технических артефактов:
    • удалены __pycache__,
    • удалены .bak*/pre-* backup-файлы.
  1. Наблюдение на другом сервере (важно для диагностики):
  • периодически фиксировалось некорректное разрешение WEB-сессии 1920x12960 при включенном x11vnc -ncache;
  • при отключении ncache (убрать флаг -ncache, оставить без него) проблема исчезала и разрешение становилось корректным.