235 lines
12 KiB
HTML
235 lines
12 KiB
HTML
<!doctype html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>МОНТ - инфрастуктурный полигон</title>
|
|
<link rel="stylesheet" href="/static/style.css" />
|
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
|
<link rel="alternate icon" type="image/png" href="/static/favicon.png" />
|
|
</head>
|
|
<body class="dashboard-page">
|
|
{% raw %}<style>
|
|
#mobile-wall{display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:9999;background:linear-gradient(135deg,#0d1b2a 0%,#1a2e45 60%,#0f2137 100%);flex-direction:column;align-items:center;justify-content:center;padding:clamp(1rem,6vw,2.5rem);text-align:center;font-family:sans-serif;box-sizing:border-box;overflow:hidden}
|
|
@media(max-width:1023px){#mobile-wall{display:flex}}
|
|
.mw-icon{font-size:clamp(2.2rem,12vw,3.5rem);margin-bottom:clamp(.5rem,2vw,1.2rem);filter:drop-shadow(0 0 18px rgba(42,140,214,.5))}
|
|
.mw-title{font-size:clamp(1rem,5.5vw,1.5rem);font-weight:800;color:#fff;margin-bottom:.6rem;letter-spacing:.01em;word-break:break-word;width:100%}
|
|
.mw-sub{font-size:clamp(.8rem,3.8vw,.95rem);color:#a0b8cc;width:100%;max-width:320px;line-height:1.6;margin-bottom:clamp(.8rem,4vw,2rem);word-break:break-word;overflow-wrap:break-word}
|
|
.mw-badge{display:inline-flex;align-items:center;gap:.45rem;background:rgba(42,140,214,.15);border:1px solid rgba(42,140,214,.4);border-radius:999px;padding:.45rem .9rem;color:#6bbfff;font-size:clamp(.7rem,3.2vw,.85rem);font-weight:600;max-width:88vw;flex-wrap:wrap;justify-content:center;word-break:break-word}
|
|
.mw-badge svg{width:16px;height:16px;flex-shrink:0}
|
|
.mw-footer{position:absolute;bottom:1.2rem;left:0;width:100%;text-align:center;font-size:clamp(.65rem,2.8vw,.78rem);color:rgba(160,184,204,.45);font-family:sans-serif}
|
|
</style>{% endraw %}
|
|
<div id="mobile-wall">
|
|
<img src="/static/logo.png" alt="MONT" style="position:absolute;top:1.2rem;left:50%;transform:translateX(-50%);height:clamp(4rem,16vw,6rem);opacity:.9">
|
|
<div class="mw-icon">🖥️</div>
|
|
<div class="mw-title">Только для компьютера</div>
|
|
<div class="mw-sub">Инфраструктурный полигон МОНТ оптимизирован для работы на ПК.<br>Пожалуйста, откройте портал с настольного компьютера или ноутбука.</div>
|
|
<div class="mw-badge">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
|
|
Минимальная ширина экрана: 1024 px
|
|
</div>
|
|
<div class="mw-footer"><a href="mailto:rgalyaviev@mont.com" style="color:inherit;text-decoration:none">Made by Galyaviev</a></div>
|
|
</div>
|
|
|
|
<header class="header">
|
|
<div style="display:flex; align-items:center; gap:0.6rem;">
|
|
<img src="/static/logo.png" alt="MONT" class="header-logo" />
|
|
<div>{{ user.username }}</div>
|
|
</div>
|
|
<div style="display:flex; gap:0.5rem;">
|
|
{% if user.is_admin %}
|
|
<a href="/admin" class="btn-link secondary">Администрирование</a>
|
|
{% endif %}
|
|
<form method="post" action="/logout">
|
|
<button type="submit">Выход</button>
|
|
</form>
|
|
</div>
|
|
</header>
|
|
<main class="admin-layout">
|
|
<section class="panel">
|
|
<div class="admin-intro">Добро пожаловать в инфраструктурную песочницу</div>
|
|
{% if session_notice %}
|
|
<div class="session-notice">{{ session_notice }}</div>
|
|
{% endif %}
|
|
<div class="rules-banner" id="rules-banner">
|
|
<div class="rules-banner-head">
|
|
<div class="rules-banner-title">Правила работы стенда</div>
|
|
</div>
|
|
<div class="rules-banner-grid">
|
|
<div class="rules-pill">Лимит: до 4 сервисов одновременно. При открытии нового сверх лимита самый старый закрывается автоматически.</div>
|
|
<div class="rules-pill">При бездействии более 5 минут сессия закрывается автоматически.</div>
|
|
<div class="rules-pill">Все сервисы работают в защищённом контуре с резервированием и бэкапами.</div>
|
|
<div class="rules-pill">Состояние сервисов возвращается к базовому каждую ночь в 00:00.</div>
|
|
</div>
|
|
<div class="rules-banner-actions">
|
|
<button type="button" class="rules-ack-btn" id="rules-ack-btn">Ознакомлен</button>
|
|
</div>
|
|
</div>
|
|
{% if categories %}
|
|
<div class="category-strip">
|
|
<a class="category-chip {% if not selected_category_slug %}active{% endif %}" href="/">Все сервисы</a>
|
|
{% for category in categories %}
|
|
<a class="category-chip {% if selected_category_slug == category.slug %}active{% endif %}" href="/?category={{ category.slug }}">{{ category.name }}</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
<section class="grid service-grid">
|
|
{% for service in services %}
|
|
{% set svc_cats = service_categories.get(service.id, []) %}
|
|
<div class="tile-wrap">
|
|
<a class="tile" href="/go/{{ service.slug }}">
|
|
<div class="tile-icon-box">
|
|
<img class="tile-icon" src="{{ service.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
|
</div>
|
|
<h3>{{ service.name }}</h3>
|
|
<p>Открыть сервис</p>
|
|
{% if service.comment %}
|
|
<small class="tile-comment">{{ service_comment_html.get(service.id, '') }}</small>
|
|
{% endif %}
|
|
{% if svc_cats %}
|
|
<div class="service-categories">
|
|
{% for category in svc_cats %}
|
|
<span class="service-cat-badge">{{ category.name }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</a>
|
|
{% if service.svc_login or service.svc_password %}
|
|
<div class="svc-credentials">
|
|
{% if service.svc_login %}
|
|
<div class="svc-cred-row">
|
|
<span class="svc-cred-label">Логин</span>
|
|
<span class="svc-cred-value">{{ service.svc_login }}</span>
|
|
<button class="svc-cred-copy" type="button" data-copy="{{ service.svc_login }}" title="Копировать логин"></button>
|
|
</div>
|
|
{% endif %}
|
|
{% if service.svc_password %}
|
|
<div class="svc-cred-row">
|
|
<span class="svc-cred-label">Пароль</span>
|
|
<span class="svc-cred-value svc-cred-masked">{{ service.svc_password }}</span>
|
|
<button class="svc-cred-copy" type="button" data-copy="{{ service.svc_password }}" title="Копировать пароль"></button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<div class="tile">
|
|
{% if selected_category_slug %}
|
|
Нет сервисов в выбранной категории
|
|
{% else %}
|
|
Нет назначенных сервисов
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</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 }};
|
|
const key = `rules_ack_${username}`;
|
|
const banner = document.getElementById('rules-banner');
|
|
const btn = document.getElementById('rules-ack-btn');
|
|
if (!banner || !btn) return;
|
|
if (localStorage.getItem(key) === '1') {
|
|
banner.style.display = 'none';
|
|
return;
|
|
}
|
|
btn.addEventListener('click', function () {
|
|
localStorage.setItem(key, '1');
|
|
banner.style.display = 'none';
|
|
});
|
|
})();
|
|
|
|
(function () {
|
|
function clamp(value, min, max) {
|
|
return Math.max(min, Math.min(max, value));
|
|
}
|
|
|
|
function currentScreenParams() {
|
|
const screenWidth =
|
|
window.screen && Number.isFinite(window.screen.width) && window.screen.width > 0
|
|
? window.screen.width
|
|
: null;
|
|
const screenHeight =
|
|
window.screen && Number.isFinite(window.screen.height) && window.screen.height > 0
|
|
? window.screen.height
|
|
: null;
|
|
const viewportWidth =
|
|
(window.visualViewport && window.visualViewport.width) ||
|
|
window.innerWidth ||
|
|
document.documentElement.clientWidth ||
|
|
1280;
|
|
const viewportHeight =
|
|
(window.visualViewport && window.visualViewport.height) ||
|
|
window.innerHeight ||
|
|
document.documentElement.clientHeight ||
|
|
720;
|
|
// Prefer stable screen dimensions; viewport is fallback.
|
|
const width = clamp(Math.round(screenWidth || viewportWidth), 320, 7680);
|
|
const height = clamp(Math.round(screenHeight || viewportHeight), 240, 4320);
|
|
const sp = new URLSearchParams();
|
|
sp.set('sw', String(width));
|
|
sp.set('sh', String(height));
|
|
return sp;
|
|
}
|
|
|
|
const loadingOverlay = document.getElementById('loading-overlay');
|
|
|
|
document.querySelectorAll('a.tile[href^="/go/"]').forEach(function (link) {
|
|
link.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
let href = link.getAttribute('href');
|
|
try {
|
|
const url = new URL(href, window.location.origin);
|
|
const params = currentScreenParams();
|
|
url.search = params.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>
|
|
<script>
|
|
document.querySelectorAll('.svc-cred-copy').forEach(btn => {
|
|
btn.addEventListener('click', async (e) => {
|
|
e.preventDefault(); e.stopPropagation();
|
|
const text = btn.dataset.copy;
|
|
try { await navigator.clipboard.writeText(text); } catch(_) {
|
|
const ta = document.createElement('textarea');
|
|
ta.value = text; ta.style.position='fixed'; ta.style.opacity='0';
|
|
document.body.appendChild(ta); ta.select();
|
|
document.execCommand('copy'); document.body.removeChild(ta);
|
|
}
|
|
btn.classList.add('copied');
|
|
setTimeout(() => btn.classList.remove('copied'), 1500);
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|