26 KiB
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) Что важно помнить по инфраструктуре
- Traefik удалять нельзя. Причина: динамические контейнеры создают labels во время работы, и именно Traefik маршрутизирует:
/s/<session_id>/.../svc/<slug>/.../w/<slot>/.../u/<slot>/...
- При Nginx Proxy Manager (NPM):
- внешний домен -> NPM -> внутренний Traefik.
- в
docker-compose.ymlTraefik опубликован так:0.0.0.0:2288 -> 4430.0.0.0:8288 -> 80
- в NPM обязательна опция
Websockets Support.
- Кнопка «Домой» в 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.
10) Сервер и рабочие пути
- SSH сервер:
ruslan@10.17.39.3 - Пароль
sudoна сервере:utOgbZ09ruslanstand - Рабочий каталог проекта на сервере:
/root/Stend_mont - Файл контекста на сервере:
/root/Stend_mont/docs/PROJECT_CONTEXT.md
Базовый рабочий сценарий:
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:
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)
- UI/брендинг:
- Тексты в интерфейсе переведены на формулировку
инфрастуктурный полигон. - На главной панели приветствие в блоке
admin-intro:Добро пожаловать в инфрастуктурный полигон. - Кнопка выхода на дашборде:
Выход(вместоLogout).
- WEB runtime (браузерные сервисы):
- В панели управления runtime оставлены 2 кнопки:
НазадГлавная(ведет на главную панель портала/).
- Кнопка
Впередудалена. - Изменения применены в
kiosk/entrypoint.shиuniversal-runtime/entrypoint.sh.
- Логин и просроченные пользователи:
- Если пользователь найден и пароль верный, но аккаунт просрочен/неактивен, на экране входа показывается сообщение:
Доступ к сервису приостоновлен, обратитесь к вашему менеджеру. - Сообщение рендерится в шаблоне
app/templates/login.htmlчерезlogin_error.
- Категории сервисов:
- Добавлены сущности и связи:
categoriesservice_categories
- Категории можно создавать/удалять в админке.
- При создании/редактировании WEB/RDP сервиса можно выбрать категории.
- На главной панели добавлен стильный фильтр по категориям (chips) и бейджи категорий на карточке сервиса.
- Иконки сервисов:
- Иконки на главной панели увеличены примерно в 6 раз.
- Масштабирование иконок:
object-fit: contain, чтобы картинка полностью влезала в рамку. - В админке загрузка иконки стала автоматической при выборе файла (без кнопки Upload).
- Многоворкерный API и startup:
- API работает с
uvicorn --workers 4. - Чтобы убрать гонку DDL на старте (при нескольких воркерах), добавлен file-lock на bootstrap схемы:
- lock-файл:
/tmp/portal-schema.lock - сериализуется выполнение
Base.metadata.create_all(...)иensure_schema_compatibility().
- lock-файл:
- Операционные заметки по применению runtime-изменений:
- После изменения
kiosk/universal-runtimeнужно:- пересобрать runtime-образы,
- пересоздать
portal-webpool-*,portal-universal-*,portal-warm-*контейнеры, - перезапустить
api.
14) Обновление контекста (2026-04-21, вечер)
- Главная страница и 500:
- Был зафиксирован Internal Server Error на /.
- Причина: синтаксическая ошибка Jinja в app/templates/login.html (поврежденный endif).
- Статус: исправлено, API перезапущен, / отвечает 200.
- Фон и визуальные эффекты:
- Были тесты фонов main.jpg, main_general.jpg, 123.jpg и локального файла 71ba42f1d7d61e4313ad8fd086d3ed7f.jpg.
- Текущее состояние по запросу: эффекты отключены.
- Отключено: parallax, анимации облаков, hover-движения карточек/ссылок, blur карточек.
- Главная панель оставлена со статичным светлым фоном без motion-эффектов.
- Файлы, затронутые в этой волне:
- app/templates/dashboard.html: удален parallax/cloud слой из разметки.
- app/static/style.css: добавлен override-блок для отключения эффектов.
- app/templates/login.html: исправлена ошибка шаблона.
- Актуальный операционный контур:
- Сервер: ruslan@10.17.39.3
- Проект: /root/Stend_mont
- Контекст: /root/Stend_mont/docs/PROJECT_CONTEXT.md
- Применение UI-правок:
- ssh ruslan@10.17.39.3
- sudo -s
- cd /root/Stend_mont
- docker compose up -d --build api
- 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, таймаут и пулы)
- Таймаут простаивания сессии уменьшен:
- Было:
SESSION_IDLE_SECONDS=1800(~30 минут). - Стало:
SESSION_IDLE_SECONDS=300(~5 минут). - Источник значения:
.env:SESSION_IDLE_SECONDS=300docker-compose.yml:SESSION_IDLE_SECONDS: ${SESSION_IDLE_SECONDS:-300}- fallback в
app/main.py:300.
- Поведение при простое (heartbeat):
- В runtime-страницах (
kiosk,universal-runtime,rdp-proxy) heartbeat теперь проверяет HTTP-статусtouch. - Если
touchвозвращает не2xx(например,410 Session expired), клиент делает редирект на:/?session_closed=idle - На
/добавлено уведомление:Сессия была закрыта из-за простоя. Откройте сервис заново. - Уведомление показывается и на login-page, и на dashboard.
- Изменение API для touch:
POST /api/sessions/{id}/touch:404если сессия не найдена/не принадлежит пользователю;410если сессия найдена, но уже неACTIVE.
16) Обновления (2026-04-21, ночь)
- Ограничение активных сервисов пользователя:
- Лимит оставлен
MAX_ACTIVE_SERVICES_PER_USER=4. - Поведение изменено на FIFO-ротацию:
- при открытии 5-го сервиса автоматически закрывается самый старый активный;
- при открытии 6-го — следующий по старшинству и т.д.
- Жесткий редирект с ошибкой теперь используется только как аварийный fallback.
- Время простоя:
- Для обычного простоя подтверждено
SESSION_IDLE_SECONDS=300(5 минут). - Значения синхронизированы в
.env,docker-compose.yml,app/main.py.
- Runtime-навигация в сервисах:
- Кнопки оставлены символьные:
←(назад)⌂(главная)
- Позиция обновлена: слева вверху, но чуть ниже прежнего:
kiosk:top:34pxuniversal-runtime:top:64px(ниже статусного блока)
- UI карточек на главной:
- В описании карточки добавлена прокрутка (
max-height+overflow:auto), если текст не влезает. - Поддержаны переносы строк.
- Поддержано отображение жирного текста из:
**markdown**- простых HTML-тегов (
<b>,<strong>,<i>,<em>,<u>,<br>), с безопасным экранированием остального.
- Авторизация:
- При неверном логине/пароле теперь отображается явное сообщение на странице входа:
Неверный логин или пароль(вместо немого 401 без человекочитаемого текста).
- Производительность API:
- Увеличено число воркеров Uvicorn:
- было:
--workers 4 - стало:
--workers 6
- было:
- Изменение внесено в
docker-compose.yml.
- WEB pool (устойчивость при пике):
- Добавлен recovery на конфликты Docker имен/удаления (
already in use,marked for removal). - Для
ensure_web_poolдобавлены повторные попытки и принудительное удаление конфликтного контейнера перед повтором. - Это закрывает сценарий, когда буфер (
WEB_POOL_BUFFER) должен расширять пул, но упирается в конфликт имени контейнера.
- 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.
- Важный операционный урок:
- При работе с
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)
- Причина закрытия сессии (
idlevslimit):
- Добавлен статус сессии
ROTATEDв API; - Для
POST /api/sessions/{id}/touchпри закрытой сессии возвращается410с JSON:ok: falsereason: idle|limitstatus: <SessionStatus>
- В рантаймах (
kiosk,universal-runtime) редирект на главную теперь учитывает причину:/?session_closed=idle/?session_closed=limit
- На главной странице добавлено отдельное сообщение о закрытии из-за лимита активных сервисов.
- API воркеры:
- Значение в
docker-compose.ymlувеличено доuvicorn --workers 18.
- Логирование API усилено (structured logging):
- Добавлены структурированные JSON-события с
eventиreq_id; - Расширен middleware логирования запросов: метод, путь, query, статус, длительность, client_ip, user_agent;
- Добавлен порог медленных запросов через
LOG_SLOW_REQUEST_MS(по умолчанию2000мс); - Добавлены ключевые события жизненного цикла сессий:
session_open_requestedsession_createdsession_rotatedsession_closedsession_touch_rejectedsession_closed_by_user
- Операционная польза:
- Быстрее диагностируются причины
504/обрывов/закрытий; - Проще фильтровать инциденты по
req_idиsession_idвdocker compose logs api.
19) Обновления (2026-04-23, лимиты + нагрузка)
- Исправление гонки лимитов активных сервисов:
- Зафиксирован кейс, когда при параллельных открытиях сервисов одним пользователем лимит мог временно обходиться (наблюдалось до 8 активных сервисов).
- Причина: проверка лимита выполнялась вне критической секции.
- Исправление: в
go_serviceдобавлена пользовательская advisory-lock секцияallocator_lock(db, 92000 + user.id), внутри которой выполняются:- проверка существующей сессии по сервису,
- проверка/ротация по лимиту,
- создание новой сессии.
- Результат: операции открытия сервисов для одного пользователя сериализованы, лимит применяется стабильно.
- Нагрузочное тестирование (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.
- Git фиксация:
- Commit:
1438dee - Message:
feat: improve session limit handling and add k6 load testing
16) Обновления (2026-04-24, вынос maintenance в отдельный контейнер)
- Выделен отдельный сервис maintenance:
- Добавлен контейнер
maintenanceвdocker-compose.yml. - Команда контейнера:
python maintenance_runner.py. - Назначение: единственный фоновый процесс обслуживания пулов и cleanup просроченных сессий.
- Поведение API на старте изменено:
- Для
apiустановлен флагENABLE_STARTUP_MAINTENANCE=0. - API-воркеры больше не запускают maintenance-потоки при startup.
- В логах API при старте ожидаемое сообщение:
startup_maintenance_disabled.
- Что делает maintenance-контейнер:
- bootstrap схемы БД (под schema-lock),
ensure_universal_pool()иensure_web_pool(),- поддержка warm-pool (когда WEB pool отключен),
- cleanup протухших сессий (через существующий
cleanup_loop).
- Блокировка лидера maintenance:
- Используется file-lock
/tmp/portal-maintenance.lock. - Контейнер maintenance удерживает lock и работает как singleton.
- Операционные команды:
- Перезапуск API + maintenance:
docker compose up -d --build api maintenance
- Проверка:
docker compose ps api maintenance
docker compose logs -f api maintenance
- Текущее целевое состояние после обновления:
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/удалены.