feat: loading overlay on dashboard, RDP pooled session routing fix

- dashboard.html: overlay div moved before <script> so getElementById works;
  double rAF ensures browser paints spinner before navigation
- main.py: pooled_rdp route fix — session_status now returns /svc/<slug>/
  route and redirect_url for POOL: RDP sessions (was always ready instantly)
- docker-compose.yml: parametrise env vars via .env for easier tuning

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 19:07:55 +00:00
parent 419b495020
commit 6f17193312
4 changed files with 68 additions and 19 deletions
+8 -2
View File
@@ -2419,7 +2419,13 @@ def session_status(session_id: str, user: User = Depends(require_user), db: Sess
universal_pool_idx = int(sess.container_id.split(":", 1)[1])
except Exception:
universal_pool_idx = None
route_path = f"/svc/{service.slug}/" if pooled_web and service else f"/s/{session_id}/"
pooled_rdp = bool(sess.container_id and sess.container_id.startswith("POOL:") and service and service.type == ServiceType.RDP)
if pooled_web and service:
route_path = f"/svc/{service.slug}/"
elif pooled_rdp and service:
route_path = f"/svc/{service.slug}/"
else:
route_path = f"/s/{session_id}/"
if web_pool_idx is not None:
route_path = f"/w/{web_pool_idx}/"
if universal_pool_idx is not None:
@@ -2436,7 +2442,7 @@ def session_status(session_id: str, user: User = Depends(require_user), db: Sess
"message": "Готово, открываем..." if ready else "Запуск сессии...",
"steps": steps,
}
if pooled_web:
if pooled_web or pooled_rdp:
payload["redirect_url"] = f"/s/{session_id}/view"
if web_pool_idx is not None:
payload["redirect_url"] = f"/s/{session_id}/view"
+29 -3
View File
@@ -103,6 +103,18 @@
</section>
<footer class="made-by-wrap"><a class="made-by" href="mailto:rgalyaviev@mont.com">Made by Galyaviev</a></footer>
</main>
<style>
#loading-overlay{display:none;position:fixed;inset:0;z-index:8888;background:rgba(10,18,28,.88);
backdrop-filter:blur(4px);flex-direction:column;align-items:center;justify-content:center;gap:1.2rem}
#loading-overlay .lo-spinner{width:52px;height:52px;border:4px solid rgba(220,232,245,.15);
border-top-color:#2a8cd6;border-radius:50%;animation:lo-spin .85s linear infinite}
#loading-overlay .lo-text{color:#a0b8cc;font:600 1rem sans-serif}
@keyframes lo-spin{to{transform:rotate(360deg)}}
</style>
<div id="loading-overlay">
<div class="lo-spinner"></div>
<div class="lo-text">Ожидайте...</div>
</div>
<script>
(function () {
const username = {{ user.username|tojson }};
@@ -153,16 +165,30 @@
return sp;
}
const loadingOverlay = document.getElementById('loading-overlay');
document.querySelectorAll('a.tile[href^="/go/"]').forEach(function (link) {
link.addEventListener('click', function () {
link.addEventListener('click', function (e) {
e.preventDefault();
let href = link.getAttribute('href');
try {
const url = new URL(link.getAttribute('href'), window.location.origin);
const url = new URL(href, window.location.origin);
const params = currentScreenParams();
url.search = params.toString();
link.setAttribute('href', url.pathname + '?' + url.searchParams.toString());
href = url.pathname + '?' + url.searchParams.toString();
} catch (e) {}
if (loadingOverlay) loadingOverlay.style.display = 'flex';
requestAnimationFrame(function () {
requestAnimationFrame(function () {
window.location.href = href;
});
});
}, { capture: true });
});
window.addEventListener('pageshow', function (e) {
if (loadingOverlay) loadingOverlay.style.display = 'none';
});
})();
</script>
</body>
+29 -13
View File
@@ -30,7 +30,7 @@ services:
api:
build:
context: ./app
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "6"]
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "${UVICORN_WORKERS:-6}"]
environment:
DATABASE_URL: postgresql+psycopg2://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
SIGNING_KEY: ${SIGNING_KEY}
@@ -41,14 +41,22 @@ services:
PREWARM_POOL_SIZE: ${PREWARM_POOL_SIZE:-2}
UNIVERSAL_POOL_SIZE: ${UNIVERSAL_POOL_SIZE:-0}
WEB_POOL_SIZE: ${WEB_POOL_SIZE:-20}
WEB_POOL_BUFFER: ${WEB_POOL_BUFFER:-10}
X11VNC_FLAGS: ${X11VNC_FLAGS:--wait 5 -defer 5 -ncache 10 -threads}
WEB_POOL_BUFFER: ${WEB_POOL_BUFFER:-2}
X11VNC_FLAGS: ${X11VNC_FLAGS:--wait 5 -defer 5 -threads}
MAX_ACTIVE_SERVICES_PER_USER: ${MAX_ACTIVE_SERVICES_PER_USER:-4}
LOG_LEVEL: ${LOG_LEVEL:-INFO}
GO_USER_LOCK_TIMEOUT_SECONDS: 8
GO_POOL_LOCK_TIMEOUT_SECONDS: 20
POOL_DISPATCH_RETRIES: 6
ENABLE_STARTUP_MAINTENANCE: 0
LOG_SLOW_REQUEST_MS: ${LOG_SLOW_REQUEST_MS:-2000}
GO_USER_LOCK_TIMEOUT_SECONDS: ${GO_USER_LOCK_TIMEOUT_SECONDS:-8}
GO_POOL_LOCK_TIMEOUT_SECONDS: ${GO_POOL_LOCK_TIMEOUT_SECONDS:-20}
POOL_DISPATCH_RETRIES: ${POOL_DISPATCH_RETRIES:-6}
POOL_DISPATCH_REQUEST_TIMEOUT_SECONDS: ${POOL_DISPATCH_REQUEST_TIMEOUT_SECONDS:-2.0}
POOL_DISPATCH_SLEEP_SECONDS: ${POOL_DISPATCH_SLEEP_SECONDS:-0.3}
TRAEFIK_INTERNAL_URL: ${TRAEFIK_INTERNAL_URL:-http://traefik}
WEB_RESOLUTION_MIN_WIDTH: ${WEB_RESOLUTION_MIN_WIDTH:-1024}
WEB_RESOLUTION_MIN_HEIGHT: ${WEB_RESOLUTION_MIN_HEIGHT:-720}
WEB_RESOLUTION_MAX_WIDTH: ${WEB_RESOLUTION_MAX_WIDTH:-3840}
WEB_RESOLUTION_MAX_HEIGHT: ${WEB_RESOLUTION_MAX_HEIGHT:-2160}
ENABLE_STARTUP_MAINTENANCE: ${ENABLE_STARTUP_MAINTENANCE:-0}
depends_on:
- db
volumes:
@@ -83,14 +91,22 @@ services:
PREWARM_POOL_SIZE: ${PREWARM_POOL_SIZE:-2}
UNIVERSAL_POOL_SIZE: ${UNIVERSAL_POOL_SIZE:-0}
WEB_POOL_SIZE: ${WEB_POOL_SIZE:-20}
WEB_POOL_BUFFER: ${WEB_POOL_BUFFER:-10}
X11VNC_FLAGS: ${X11VNC_FLAGS:--wait 5 -defer 5 -ncache 10 -threads}
WEB_POOL_BUFFER: ${WEB_POOL_BUFFER:-2}
X11VNC_FLAGS: ${X11VNC_FLAGS:--wait 5 -defer 5 -threads}
MAX_ACTIVE_SERVICES_PER_USER: ${MAX_ACTIVE_SERVICES_PER_USER:-4}
LOG_LEVEL: ${LOG_LEVEL:-INFO}
GO_USER_LOCK_TIMEOUT_SECONDS: 8
GO_POOL_LOCK_TIMEOUT_SECONDS: 20
POOL_DISPATCH_RETRIES: 6
ENABLE_STARTUP_MAINTENANCE: 0
LOG_SLOW_REQUEST_MS: ${LOG_SLOW_REQUEST_MS:-2000}
GO_USER_LOCK_TIMEOUT_SECONDS: ${GO_USER_LOCK_TIMEOUT_SECONDS:-8}
GO_POOL_LOCK_TIMEOUT_SECONDS: ${GO_POOL_LOCK_TIMEOUT_SECONDS:-20}
POOL_DISPATCH_RETRIES: ${POOL_DISPATCH_RETRIES:-6}
POOL_DISPATCH_REQUEST_TIMEOUT_SECONDS: ${POOL_DISPATCH_REQUEST_TIMEOUT_SECONDS:-2.0}
POOL_DISPATCH_SLEEP_SECONDS: ${POOL_DISPATCH_SLEEP_SECONDS:-0.3}
TRAEFIK_INTERNAL_URL: ${TRAEFIK_INTERNAL_URL:-http://traefik}
WEB_RESOLUTION_MIN_WIDTH: ${WEB_RESOLUTION_MIN_WIDTH:-1024}
WEB_RESOLUTION_MIN_HEIGHT: ${WEB_RESOLUTION_MIN_HEIGHT:-720}
WEB_RESOLUTION_MAX_WIDTH: ${WEB_RESOLUTION_MAX_WIDTH:-3840}
WEB_RESOLUTION_MAX_HEIGHT: ${WEB_RESOLUTION_MAX_HEIGHT:-2160}
ENABLE_STARTUP_MAINTENANCE: ${ENABLE_STARTUP_MAINTENANCE:-0}
depends_on:
- db
volumes:
+2 -1
View File
@@ -107,7 +107,8 @@ cat > /opt/portal/index.html <<HTML
connected = true;
reconnectAttempts = 0;
clearTimeout(reconnectTimer);
hideStatus();
showStatus('Устанавливается соединение с рабочим столом...');
setTimeout(hideStatus, 6000);
});
rfb.addEventListener('disconnect', () => {
connected = false;