Admin Pilots tab: - list and create/edit form merged into one - "+ Добавить пилота" at the top of the list clears the form into create mode, clicking a pilot switches it into edit mode (title, Delete button and icon box toggle accordingly) instead of keeping two separate stacked forms - each list row gets an inline on/off toggle (⏻/▶) to flip active without opening the form - categories and the login/password hint field removed from the pilot form per earlier feedback - not applicable to pilots Access model: assigning a pilot's RDP slot to a user is now the single action that grants them the pilot - it creates/updates their UserServiceAccess row (with the optional expiry typed right next to the assign dropdown) in the same call, and unassigning revokes it. Previously slot assignment and the ACL grant were two separate steps an admin could forget to pair up, leaving a user "granted" with no working slot or a slot with no visible access. Consequently: pilots are no longer listed in the Users tab's ACL grid at all - the Pilots tab is now the only place pilot access is managed. set_acl() was fixed to never touch pilot grants regardless of what's posted (it used to free a user's pilot slot whenever it saw a pilot id missing from service_ids - which happens on every save now that the grid never includes pilots, so saving any other product's ACL would have silently stripped every pilot grant). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Portal Stand Access (MVP)
MVP-портал доступа к стендам в Proxmox через единый вход https://stend.4mont.ru.
1. Архитектура
traefik: входная точка TLS, маршрутизация на API и динамические сессионные контейнеры.api(FastAPI): auth, ACL, админ API, создание runtime-сессий, cleanup.db(PostgreSQL): пользователи, сервисы, ACL, сессии, аудит.portal-kioskimage: Chromium kiosk + noVNC (для WEB target).portal-rdp-proxyimage: xfreerdp + websockify/noVNC proxy до удаленного RDP host:port.portal-universal-runtimeimage: универсальный runtime (WEB/RDP), используется общим warm poolUNIVERSAL_POOL_SIZE.- Опциональный prewarm pool:
- глобальный fallback
PREWARM_POOL_SIZE; - приоритетный
warm_pool_sizeна каждом сервисе в админке.
- глобальный fallback
Поток:
- Пользователь логинится на
/. - Dashboard показывает разрешённые сервисы.
- Клик по
/go/<slug>-> ACL check + проверкаexpires_at+ создание записиsessions+ старт отдельного контейнера. - Пользователь редиректится на
/s/<session_id>/. - Traefik отправляет
/s/<session_id>/...в конкретный runtime контейнер по dynamic labels. - Runtime страница шлёт heartbeat в
/api/sessions/<session_id>/touch. - Фоновый cleanup завершает сессии при idle > 30 минут.
При prewarm:
- создаются warm-контейнеры с маршрутом
/svc/<slug>/; - клик по плитке использует prewarmed runtime без задержки cold start;
- сессии в БД создаются, но контейнеры остаются пулом (без стопа на каждую сессию).
- в
/adminесть:- отдельные разделы Users / WEB / RDP (список + форма выбранной записи);
pool sizeна сервис;- кнопка
Prewarm now; - health
running/desiredпо каждому пулу. - авто-генерация
slugиз названия (поддержка кириллицы -> латиница).
2. Схема БД
Основные таблицы:
usersservicesuser_service_accesssessions
Дополнительно:
audit_logs
SQL-схема: scripts/schema.sql.
3. Безопасность
- Пароли:
argon2(passlib[argon2]). - Cookie auth:
HttpOnly,Secure,SameSite=Strict. - CSRF:
- формы (
/login) через hidden token + cookie; - admin JSON API через
X-CSRF-Token.
- формы (
- Проверки при каждом запросе:
- пользователь
active=true; expires_at > now();- ACL на
/go/<slug>.
- пользователь
- Аудит: события входа и создания сессий в
audit_logs.
4. Файлы
docker-compose.ymltraefik/traefik.ymltraefik/dynamic/security.ymlapp/main.pykiosk/Dockerfile,kiosk/entrypoint.shrdp-proxy/Dockerfile,rdp-proxy/entrypoint.sh
5. Запуск
cp .env.example .env
mkdir -p traefik/letsencrypt
touch traefik/letsencrypt/acme.json
chmod 600 traefik/letsencrypt/acme.json
Собрать runtime образы:
docker compose --profile build-only build kiosk-image rdp-proxy-image universal-runtime-image
Поднять систему:
docker compose up -d --build
Проверка:
docker compose ps
docker compose logs -f api traefik
Дефолтный админ берётся из .env (ADMIN_USERNAME, ADMIN_PASSWORD).
5.1 Развертывание На Другом Сервере (Gitea)
Пример для чистого Ubuntu-сервера.
- Установить Docker + Compose plugin:
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin git
sudo usermod -aG docker $USER
newgrp docker
- Клонировать проект из Gitea:
git clone http://gitea.ruslan.xyz/ruslan/Stend_mont.git
cd Stend_mont
- Подготовить конфиг:
cp .env.example .env
mkdir -p traefik/letsencrypt
touch traefik/letsencrypt/acme.json
chmod 600 traefik/letsencrypt/acme.json
- Отредактировать
.env:
PUBLIC_HOST-> домен нового стенда (напримерstend.example.com)LETSENCRYPT_EMAIL-> рабочий emailADMIN_USERNAME,ADMIN_PASSWORD-> учетка администратораDATABASE_URLпри необходимости оставить как есть (внутренний docker postgres)
- Собрать и запустить:
docker compose --profile build-only build kiosk-image rdp-proxy-image universal-runtime-image
docker compose up -d --build
- Проверка:
docker compose ps
docker compose logs -f api traefik
Если домен уже смотрит на этот сервер и 80/443 открыты, админка будет доступна по:
https://<PUBLIC_HOST>/admin
5.2 Вариант С Nginx Proxy Manager (NPM)
Если на сервере уже используется NPM, внешний трафик можно вести через него, а Traefik оставить только внутренним роутером проекта.
В этом репозитории уже настроены внутренние порты:
127.0.0.1:2288 -> traefik:443(HTTPS upstream)127.0.0.1:8288 -> traefik:80(HTTP upstream, технический)
Важно: Traefik убирать нельзя, он нужен для динамических маршрутов сессий (/s/*, /svc/*).
Как настроить NPM:
- Создать
Proxy Hostдля домена (напримерstend.example.com):
Forward Hostname / IP:127.0.0.1Forward Port:2288Scheme:https- включить
Websockets Support
-
На вкладке SSL в NPM выпустить/подключить сертификат для домена.
-
В
.envпроекта оставить:
PUBLIC_HOST=stend.example.com
- Перезапустить проект:
docker compose up -d --build
После этого вход в портал и сессии будут работать через NPM, а Traefik останется внутренним компонентом.
6. Примеры admin API
- Создать сервис WEB:
curl -k -X POST "https://stend.4mont.ru/api/admin/services" \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: <csrf_from_cookie>" \
-H "Cookie: portal_auth=<cookie>; csrf_token=<csrf>" \
-d '{"name":"CRM","slug":"crm","type":"WEB","target":"http://192.168.1.10:3000","active":true}'
- Создать сервис RDP:
curl -k -X POST "https://stend.4mont.ru/api/admin/services" \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: <csrf_from_cookie>" \
-H "Cookie: portal_auth=<cookie>; csrf_token=<csrf>" \
-d '{"name":"Windows Desktop","slug":"win-rdp1","type":"RDP","target":"192.168.1.60:3389","active":true}'
Для RDP target также поддерживает креды и параметры:
rdp://user:password@192.168.1.60:3389?domain=AD&sec=nla
- Создать пользователя:
curl -k -X POST "https://stend.4mont.ru/api/admin/users" \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: <csrf_from_cookie>" \
-H "Cookie: portal_auth=<cookie>; csrf_token=<csrf>" \
-d '{"username":"user1","password":"Passw0rd!","expires_at":"2026-12-31T23:59:59+00:00","active":true}'
- Назначить ACL:
curl -k -X PUT "https://stend.4mont.ru/api/admin/users/2/acl" \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: <csrf_from_cookie>" \
-H "Cookie: portal_auth=<cookie>; csrf_token=<csrf>" \
-d '{"service_ids":[1,2]}'
7. Ограничения MVP
- Нет отдельной UI-админки (есть admin API).
- TTL неактивности основан на heartbeat runtime-страницы.
- Для production стоит добавить Alembic-миграции, rate limiting и централизованный логинг.
8. Нагрузочное тестирование (актуально на 2026-04-24)
В проекте подтвержден рабочий плавный профиль нагрузки без резкого burst:
- +1 пользователь в минуту;
- целевой online: 20 пользователей;
- каждый запускает 2 WEB-сервиса (termidesk и vmmanager).
Результат прогона 20x2:
- логин и открытие сервисов: 100% успешных проверок;
- HTTP 5xx в тесте: 0;
- достигнуто 40 ACTIVE WEB-сессий (20 + 20).
Важно для повторения теста:
- Перед прогоном временно увеличьте idle timeout (SESSION_IDLE_SECONDS) и пересоздайте WEB-пул, чтобы слоты получили новый IDLE_TIMEOUT.
- Параллельно собирайте метрики хоста (uptime/free/df/docker stats) в файл.
- После прогона проверяйте согласованность: ACTIVE-сессии в БД vs реально существующие portal-webpool-* контейнеры.
Замеченный риск:
- при агрессивном авторасширении/сжатии WEB-пула возможны ACTIVE-сессии, привязанные к уже удаленным слотам.