46e5b5a41e
New Service.is_pilot flag - a pilot is a Service (type RDP) that: - is excluded from /api/public/services-by-category and /api/request-more-access (admin-granted only, no self-service) - still shows as a locked card on the dashboard for users without access (Доступно по запросу section), but with a По приглашению badge instead of the self-request button/flow - gets its own non-clickable teaser row on the public /login page (logos only, informational) RdpSlot.assigned_user_id (nullable) - pilot slots are bound to one specific user instead of being drawn from the shared pool; regular RDP services are unaffected (field stays NULL, same pool behaviour as before). The /go/ allocator branches on service.is_pilot to pick the caller's assigned slot instead of any free one. Slots release automatically (cleanup_loop) when the owning grant is revoked or expires, and immediately on manual ACL revoke. UserServiceAccess.expires_at (nullable) - per-grant access window, used by pilots so their access can be shorter than the account's own expires_at; NULL (unchanged default) means "follow the account". has_access() and the dashboard's granted/locked split both honour it. Admin UI: "Это пилот" checkbox on the RDP service form, an assign-user dropdown on a pilot's slot table (replaces the occupied-by column), and a per-pilot expiry date field in the user ACL grid. Schema is applied via the existing ensure_schema_compatibility() idempotent ALTER TABLE pattern (no alembic in this project) - no manual migration step needed, it runs at container startup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
577 lines
35 KiB
HTML
577 lines
35 KiB
HTML
{% set _scheme = request.headers.get('x-forwarded-proto', request.url.scheme) %}
|
|
{% set base_url = _scheme + '://' + request.url.netloc + '/' %}
|
|
<!doctype html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Инфраструктурный полигон MONT — демо и пилоты российского ПО</title>
|
|
<meta name="description" content="Инфраструктурный полигон MONT: демонстрация и пилотное тестирование российского ПО для партнёров и заказчиков. Браузерный доступ к рабочим стендам — без установки и настройки." />
|
|
<meta name="keywords" content="инфраструктурный полигон MONT, пилоты MONT, демо MONT, партнёры MONT, демонстрация MONT, российское ПО демо, отечественное ПО тестирование, демостенд ПО" />
|
|
<meta name="robots" content="index, follow" />
|
|
<link rel="canonical" href="{{ base_url }}" />
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:url" content="{{ base_url }}" />
|
|
<meta property="og:title" content="Инфраструктурный полигон MONT — демо и пилоты российского ПО" />
|
|
<meta property="og:description" content="Демонстрация и тестирование российского ПО для партнёров и заказчиков MONT. Доступ к рабочим стендам прямо в браузере." />
|
|
<meta property="og:image" content="{{ base_url }}static/logo.png?v=4" />
|
|
<meta property="og:locale" content="ru_RU" />
|
|
<meta property="og:site_name" content="Полигон MONT" />
|
|
<meta name="twitter:card" content="summary" />
|
|
<meta name="twitter:title" content="Инфраструктурный полигон MONT" />
|
|
<meta name="twitter:description" content="Демо и пилоты российского ПО для партнёров и заказчиков MONT." />
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "WebSite",
|
|
"name": "Инфраструктурный полигон MONT",
|
|
"url": "{{ base_url }}",
|
|
"description": "Платформа для демонстрации и пилотного тестирования российского программного обеспечения. Партнеры MONT и их заказчики получают браузерный доступ к рабочим стендам.",
|
|
"publisher": {
|
|
"@type": "Organization",
|
|
"name": "MONT",
|
|
"url": "https://www.mont.ru/"
|
|
}
|
|
}
|
|
</script>
|
|
<link rel="stylesheet" href="/static/style.css?v=2" />
|
|
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon_32.png" />
|
|
<link rel="icon" type="image/png" sizes="192x192" href="/static/favicon_192.png" />
|
|
<link rel="apple-touch-icon" sizes="192x192" href="/static/favicon_192.png" />
|
|
<link rel="manifest" href="/static/manifest.json" />
|
|
<!-- Yandex.Metrika counter -->
|
|
<script type="text/javascript">
|
|
(function(m,e,t,r,i,k,a){
|
|
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
|
m[i].l=1*new Date();
|
|
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
|
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
|
|
})(window, document,"script","https://mc.yandex.ru/metrika/tag.js?id=109119977", "ym");
|
|
ym(109119977, "init", {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
|
|
</script>
|
|
<noscript><div><img src="https://mc.yandex.ru/watch/109119977" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
|
<!-- /Yandex.Metrika counter -->
|
|
<style>
|
|
:root{
|
|
--lg2-bg:#eef1f5; --lg2-panel:#ffffff; --lg2-raised:#ffffff;
|
|
--lg2-line:#d7dee6; --lg2-line-soft:#e3e8ee;
|
|
--lg2-fg:#16202c; --lg2-fg-dim:#5c6b7c; --lg2-fg-faint:#93a1b0;
|
|
--lg2-accent:#c96c14; --lg2-accent-dim:rgba(201,108,20,.10); --lg2-accent-ink:#2a1400;
|
|
--lg2-bad:#d3453f; --lg2-bad-dim:rgba(211,69,63,.08);
|
|
--lg2-info:#1f6fb2; --lg2-info-dim:rgba(31,111,178,.08);
|
|
--lg2-good:#1f9d63;
|
|
}
|
|
html{ background: var(--lg2-bg); }
|
|
body.login-page{
|
|
margin:0;min-height:100vh;background:var(--lg2-bg);color:var(--lg2-fg);
|
|
font-family:"Ubuntu","IBM Plex Sans",system-ui,sans-serif;overflow-x:hidden;
|
|
}
|
|
.lg2-wrap{display:grid;grid-template-columns:1.15fr 420px;min-height:100vh}
|
|
@media (max-width:980px){ .lg2-wrap{grid-template-columns:minmax(0,1fr)} }
|
|
|
|
.lg2-pitch{
|
|
position:relative;min-width:0;padding:56px clamp(28px,5vw,72px) 48px;border-right:1px solid var(--lg2-line);
|
|
background-image:linear-gradient(var(--lg2-line-soft) 1px, transparent 1px),linear-gradient(90deg, var(--lg2-line-soft) 1px, transparent 1px);
|
|
background-size:34px 34px;display:flex;flex-direction:column;
|
|
}
|
|
@media (max-width:980px){ .lg2-pitch{border-right:none;border-bottom:1px solid var(--lg2-line)} }
|
|
.lg2-brand{display:flex;flex-wrap:wrap;align-items:center;gap:12px 16px;margin-bottom:40px}
|
|
.lg2-brand img{height:59px;display:block}
|
|
.lg2-brand-mark{font:700 11px/1 "Ubuntu Mono",monospace;letter-spacing:.14em;color:var(--lg2-fg-faint)}
|
|
@media (max-width:520px){
|
|
.lg2-brand{flex-direction:column;align-items:flex-start;gap:10px}
|
|
.lg2-brand img{height:40px}
|
|
}
|
|
.lg2-eyebrow{font:700 11px/1 "Ubuntu Mono",monospace;letter-spacing:.18em;text-transform:uppercase;color:var(--lg2-accent);margin:0 0 16px}
|
|
.lg2-pitch h1{font:800 clamp(30px,3.6vw,46px)/1.06 "Ubuntu",sans-serif;letter-spacing:-.01em;margin:0 0 18px;text-wrap:balance;max-width:16ch;color:var(--lg2-fg);overflow-wrap:break-word;hyphens:auto}
|
|
.lg2-pitch, .lg2-pitch *{ overflow-wrap: break-word; }
|
|
.lg2-pitch p.lead{font:400 15px/1.65 "Ubuntu",sans-serif;color:var(--lg2-fg-dim);max-width:52ch;margin:0 0 32px}
|
|
|
|
.lg2-feat-row{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:1px;background:var(--lg2-line);border:1px solid var(--lg2-line);border-radius:10px;overflow:hidden;margin-bottom:40px}
|
|
@media (max-width:720px){ .lg2-feat-row{grid-template-columns:repeat(2,minmax(0,1fr))} }
|
|
@media (max-width:420px){ .lg2-feat-row{grid-template-columns:minmax(0,1fr)} }
|
|
.lg2-feat{position:relative;min-width:0;background:var(--lg2-panel);padding:18px 16px 16px;border-top:2px solid var(--lg2-accent)}
|
|
.lg2-feat-idx{font:700 10px/1 "Ubuntu Mono",monospace;letter-spacing:.08em;color:var(--lg2-fg-faint);display:block;margin-bottom:12px}
|
|
.lg2-feat-icon{width:26px;height:26px;margin-bottom:14px;color:var(--lg2-accent);display:block}
|
|
.lg2-feat-icon svg{width:100%;height:100%;display:block}
|
|
.lg2-feat-label{font:600 13px/1.35 "Ubuntu",sans-serif;color:var(--lg2-fg);margin-bottom:6px}
|
|
.lg2-feat-desc{font:400 11.5px/1.55 "Ubuntu",sans-serif;color:var(--lg2-fg-dim)}
|
|
|
|
.lg2-distrib-btn{
|
|
align-self:flex-start;margin:0 0 44px;padding:11px 22px;border:1px solid var(--lg2-line);border-radius:9px;
|
|
background:transparent;color:var(--lg2-fg-dim);font:600 12.5px/1 "Ubuntu",sans-serif;letter-spacing:.02em;cursor:pointer;
|
|
}
|
|
.lg2-distrib-btn:hover{border-color:var(--lg2-accent);color:var(--lg2-fg)}
|
|
|
|
.lg2-wall-head{display:flex;align-items:baseline;gap:10px;margin-bottom:14px}
|
|
.lg2-wall-head h2{font:700 11px/1 "Ubuntu Mono",monospace;letter-spacing:.14em;text-transform:uppercase;color:var(--lg2-fg-dim);margin:0}
|
|
.lg2-wall-head::after{content:"";flex:1;height:1px;background:var(--lg2-line)}
|
|
.lg2-wall-count{font:600 11px/1 "Ubuntu Mono",monospace;color:var(--lg2-fg-faint)}
|
|
.lg2-logo-wall{display:grid;grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:10px;max-width:760px}
|
|
.lg2-wall-tile{aspect-ratio:1;background:var(--lg2-panel);border:1px solid var(--lg2-line);border-radius:9px;display:flex;align-items:center;justify-content:center;padding:8px}
|
|
a.lg2-wall-tile{text-decoration:none;cursor:pointer}
|
|
.lg2-wall-tile img{width:100%;height:100%;object-fit:contain;filter:grayscale(1) opacity(.75)}
|
|
.lg2-wall-tile:hover img{filter:none}
|
|
.lg2-wall-tile:hover{border-color:var(--lg2-accent)}
|
|
.lg2-pilots-sub{font:400 12.5px/1.5 "Ubuntu",sans-serif;color:var(--lg2-fg-faint);margin:-6px 0 14px;max-width:62ch}
|
|
.lg2-wall-tile-pilot{cursor:default}
|
|
.lg2-wall-tile-pilot:hover{border-color:var(--lg2-line)}
|
|
.lg2-wall-tile-pilot:hover img{filter:grayscale(1) opacity(.75)}
|
|
|
|
.lg2-pitch-footer{margin-top:auto;padding-top:36px;font:400 11px/1 "Ubuntu Mono",monospace;color:var(--lg2-fg-faint)}
|
|
.lg2-pitch-footer a{color:inherit;text-decoration:none}
|
|
.lg2-pitch-footer a:hover{color:var(--lg2-fg-dim)}
|
|
|
|
.lg2-auth{display:flex;flex-direction:column;justify-content:center;min-width:0;padding:48px 40px;background:var(--lg2-bg);
|
|
position:sticky;top:0;height:100vh;box-sizing:border-box}
|
|
@media (max-width:980px){ .lg2-auth{position:static;height:auto} }
|
|
.lg2-auth-card{width:100%;max-width:320px;margin:0 auto}
|
|
.lg2-auth-title{font:700 22px/1.2 "Ubuntu",sans-serif;letter-spacing:-.01em;margin:0 0 4px;color:var(--lg2-fg)}
|
|
.lg2-auth-sub{font:400 12px/1.4 "Ubuntu Mono",monospace;color:var(--lg2-fg-faint);letter-spacing:.02em;margin:0 0 18px;text-transform:uppercase}
|
|
|
|
.lg2-prompt{font:400 12.5px/1 "Ubuntu Mono",monospace;color:var(--lg2-fg-dim);margin:0 0 24px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.lg2-prompt-user{color:var(--lg2-good)}
|
|
.lg2-prompt-path{color:var(--lg2-accent)}
|
|
.lg2-caret{display:inline-block;width:7px;height:13px;background:var(--lg2-accent);vertical-align:-2px;margin-left:3px;animation:lg2-blink 1.1s step-end infinite}
|
|
@keyframes lg2-blink{50%{opacity:0}}
|
|
@media (prefers-reduced-motion: reduce){ .lg2-caret{animation:none} }
|
|
|
|
.lg2-notice, .lg2-error{
|
|
border-radius:9px;padding:10px 13px;font:500 12.5px/1.5 "Ubuntu",sans-serif;margin-bottom:18px;
|
|
}
|
|
.lg2-notice{background:var(--lg2-info-dim);border:1px solid rgba(127,199,255,.35);color:var(--lg2-info)}
|
|
.lg2-error{background:var(--lg2-bad-dim);border:1px solid rgba(255,107,107,.35);color:var(--lg2-bad)}
|
|
|
|
.lg2-field{display:flex;flex-direction:column;gap:7px;margin-bottom:16px}
|
|
.lg2-field label{font:700 10.5px/1 "Ubuntu Mono",monospace;letter-spacing:.09em;text-transform:uppercase;color:var(--lg2-fg-faint)}
|
|
.lg2-field input{background:var(--lg2-panel);border:1px solid var(--lg2-line);border-radius:9px;padding:11px 13px;color:var(--lg2-fg);font:400 14px/1 "Ubuntu",sans-serif;outline:none;width:100%;box-sizing:border-box}
|
|
.lg2-field input::placeholder{color:var(--lg2-fg-faint)}
|
|
.lg2-field input:focus{border-color:var(--lg2-accent)}
|
|
|
|
.lg2-submit{width:100%;margin-top:6px;padding:12px 16px;border:none;border-radius:9px;cursor:pointer;background:var(--lg2-accent);color:var(--lg2-accent-ink);font:700 13.5px/1 "Ubuntu",sans-serif;letter-spacing:.02em}
|
|
.lg2-submit:hover{filter:brightness(1.06)}
|
|
.lg2-request-btn{width:100%;margin-top:12px;padding:10px 16px;border-radius:9px;border:1px solid var(--lg2-line);background:transparent;color:var(--lg2-fg-dim);font:600 12.5px/1 "Ubuntu",sans-serif;cursor:pointer}
|
|
.lg2-request-btn:hover{border-color:var(--lg2-accent);color:var(--lg2-fg)}
|
|
.lg2-auth-foot{margin-top:40px;text-align:center}
|
|
.lg2-auth-foot a{color:var(--lg2-fg-faint);font-size:11px;text-decoration:none}
|
|
.lg2-auth-foot a:hover{color:var(--lg2-fg-dim)}
|
|
|
|
/* Re-skin the shared access-modal component to match the new panel palette
|
|
(it's used by "Запросить доступ" here and on the dashboard alike, so we
|
|
override on top of style.css instead of editing it there). */
|
|
.access-modal-overlay{ background: rgba(6,9,14,.82) !important; }
|
|
.access-modal{
|
|
background: var(--lg2-panel) !important; border: 1px solid var(--lg2-line) !important;
|
|
box-shadow: 0 24px 80px rgba(0,0,0,.45) !important;
|
|
}
|
|
.access-modal-title{ color: var(--lg2-fg) !important; font-family:"Ubuntu",sans-serif; }
|
|
.access-modal-sub{ color: var(--lg2-fg-faint) !important; }
|
|
.access-field label{ color: var(--lg2-fg-dim) !important; font-family:"Ubuntu",sans-serif; }
|
|
.access-field .req{ color: var(--lg2-accent) !important; }
|
|
.access-field input[type=text],
|
|
.access-field input[type=email],
|
|
.access-field input[type=tel],
|
|
.access-textarea{
|
|
background: var(--lg2-raised) !important; border: 1px solid var(--lg2-line) !important;
|
|
color: var(--lg2-fg) !important; font-family:"Ubuntu",sans-serif;
|
|
}
|
|
.access-field input[type=text]::placeholder,
|
|
.access-field input[type=email]::placeholder,
|
|
.access-field input[type=tel]::placeholder,
|
|
.access-textarea::placeholder{ color: var(--lg2-fg-faint) !important; }
|
|
.access-field input[type=text]:focus,
|
|
.access-field input[type=email]:focus,
|
|
.access-field input[type=tel]:focus,
|
|
.access-textarea:focus{
|
|
border-color: var(--lg2-accent) !important; box-shadow: 0 0 0 3px var(--lg2-accent-dim) !important;
|
|
}
|
|
.access-products-wrap{ background: var(--lg2-raised) !important; border: 1px solid var(--lg2-line) !important; }
|
|
.access-products-loading{ color: var(--lg2-fg-faint) !important; }
|
|
.access-products-cat{ color: var(--lg2-fg-faint) !important; }
|
|
.access-product-item{ color: var(--lg2-fg-dim) !important; font-family:"Ubuntu",sans-serif; }
|
|
.access-product-item:hover span{ color: var(--lg2-fg) !important; }
|
|
.access-product-item input[type=checkbox]{ accent-color: var(--lg2-accent); }
|
|
.access-modal-error{
|
|
background: var(--lg2-bad-dim) !important; border: 1px solid rgba(255,107,107,.35) !important;
|
|
color: var(--lg2-bad) !important;
|
|
}
|
|
.access-btn-cancel{
|
|
background: transparent !important; border: 1px solid var(--lg2-line) !important; color: var(--lg2-fg-dim) !important;
|
|
font-family:"Ubuntu",sans-serif;
|
|
}
|
|
.access-btn-cancel:hover{ border-color: var(--lg2-fg-faint) !important; color: var(--lg2-fg) !important; background: transparent !important; }
|
|
.access-btn-submit{
|
|
background: var(--lg2-accent) !important; color: var(--lg2-accent-ink) !important; box-shadow: none !important;
|
|
font-family:"Ubuntu",sans-serif; font-weight: 700 !important;
|
|
}
|
|
.access-btn-submit:hover:not(:disabled){ opacity: 1 !important; filter: brightness(1.06); }
|
|
.access-consent-label span{ color: var(--lg2-fg-dim) !important; font-family:"Ubuntu",sans-serif; }
|
|
.access-consent-link{ color: var(--lg2-accent) !important; }
|
|
.am-success-title{ color: var(--lg2-fg) !important; }
|
|
.am-success-sub{ color: var(--lg2-fg-dim) !important; }
|
|
.am-success-icon{ color: var(--lg2-good) !important; }
|
|
|
|
@media (max-width:820px){
|
|
.lg2-pitch{padding:40px 22px 34px}
|
|
.lg2-auth{padding:36px 22px}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="login-page">
|
|
<div class="lg2-wrap">
|
|
<aside class="lg2-pitch">
|
|
<div class="lg2-brand">
|
|
<a href="#" onclick="window.open(location.hostname==='stand.mont.ru'?'https://www.mont.ru':'https://4mont.ru','_blank');return false;">
|
|
<img src="/static/logo-color.png?v=1" alt="MONT" />
|
|
</a>
|
|
<span class="lg2-brand-mark">Инфраструктурный полигон</span>
|
|
</div>
|
|
|
|
<p class="lg2-eyebrow">Демо и пилоты российского ПО</p>
|
|
<h1>Полигон импортонезависимой инфраструктуры</h1>
|
|
<p class="lead">Платформа для демонстрации и пилотного тестирования российского ПО. Партнёры MONT
|
|
и их заказчики получают браузерный доступ к рабочим стендам с отечественными ОС, платформами
|
|
виртуализации, СРК и другими решениями — без установки и настройки.</p>
|
|
|
|
<div class="lg2-feat-row">
|
|
<div class="lg2-feat">
|
|
<span class="lg2-feat-idx">01</span>
|
|
<span class="lg2-feat-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="12" rx="1.5"/><path d="M8 20h8M12 16v4"/></svg></span>
|
|
<div class="lg2-feat-label">Доступ к рабочим столам ОС</div>
|
|
<div class="lg2-feat-desc">Полный рабочий стол Astra, RED OS, Uncom и других ОС прямо в браузере, без RDP-клиента.</div>
|
|
</div>
|
|
<div class="lg2-feat">
|
|
<span class="lg2-feat-idx">02</span>
|
|
<span class="lg2-feat-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.6 3.8 5.7 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.7-3.8-9s1.3-6.4 3.8-9z"/></svg></span>
|
|
<div class="lg2-feat-label">Веб-интерфейсы сервисов</div>
|
|
<div class="lg2-feat-desc">Админ-панели и порталы продуктов открываются как обычная вкладка браузера.</div>
|
|
</div>
|
|
<div class="lg2-feat">
|
|
<span class="lg2-feat-idx">03</span>
|
|
<span class="lg2-feat-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 4 14h7l-1 8 9-12h-7l1-8z"/></svg></span>
|
|
<div class="lg2-feat-label">Доступ в один клик</div>
|
|
<div class="lg2-feat-desc">Стенд поднимается автоматически — ничего разворачивать и настраивать не нужно.</div>
|
|
</div>
|
|
<div class="lg2-feat">
|
|
<span class="lg2-feat-idx">04</span>
|
|
<span class="lg2-feat-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="11" width="16" height="9" rx="1.5"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg></span>
|
|
<div class="lg2-feat-label">Защищённый контур</div>
|
|
<div class="lg2-feat-desc">Изолированная сеть с резервированием и регулярными бэкапами всех стендов.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="button" class="lg2-distrib-btn" onclick="window.open(location.hostname==='stand.mont.ru'?'https://maps.mont.ru':'https://maps.4mont.ru','_blank')">Продукты нашей дистрибуции</button>
|
|
|
|
{% set wall_services = public_services|rejectattr('slug', 'equalto', 'communigate-pro-user')|list %}
|
|
{% if wall_services %}
|
|
<div class="lg2-wall-head" id="products"><h2>Что на полигоне</h2><span class="lg2-wall-count">{{ wall_services|length }} продуктов</span></div>
|
|
<div class="lg2-logo-wall">
|
|
{% for service in wall_services %}
|
|
{% set wall_name = 'CommuniGate Pro' if service.slug == 'communigate-pro-admin' else service.name %}
|
|
<a class="lg2-wall-tile" href="/product/{{ service.slug }}" title="{{ wall_name }} — подробнее">
|
|
<img src="{{ service.icon_path or '/static/service-placeholder.svg' }}" alt="{{ wall_name }}" />
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if public_pilots %}
|
|
<div class="lg2-wall-head"><h2>Пилотные проекты</h2><span class="lg2-wall-count">по приглашению</span></div>
|
|
<p class="lg2-pilots-sub">Отдельные среды для пилотных внедрений — доступ выдаёт ваш менеджер MONT, здесь их нельзя запросить самостоятельно.</p>
|
|
<div class="lg2-logo-wall lg2-logo-wall-pilots">
|
|
{% for service in public_pilots %}
|
|
<div class="lg2-wall-tile lg2-wall-tile-pilot" title="{{ service.name }}">
|
|
<img src="{{ service.icon_path or '/static/service-placeholder.svg' }}" alt="{{ service.name }}" />
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="lg2-pitch-footer">MONT PROVING GROUND</div>
|
|
</aside>
|
|
|
|
<main class="lg2-auth">
|
|
<div class="lg2-auth-card">
|
|
<div class="lg2-auth-title">Вход в систему</div>
|
|
<div class="lg2-auth-sub">Инфраструктурный полигон</div>
|
|
<div class="lg2-prompt"><span class="lg2-prompt-user">mont@polygon</span><span class="lg2-prompt-path">:~/access</span>$ ./login.sh<span class="lg2-caret"></span></div>
|
|
{% if session_notice %}<div class="lg2-notice">{{ session_notice }}</div>{% endif %}
|
|
{% if login_error %}<div class="lg2-error">{{ login_error }}</div>{% endif %}
|
|
<form method="post" action="/login">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
|
<div class="lg2-field">
|
|
<label>Логин</label>
|
|
<input type="text" name="username" placeholder="Введите логин" required autocomplete="username" />
|
|
</div>
|
|
<div class="lg2-field">
|
|
<label>Пароль</label>
|
|
<input type="password" name="password" placeholder="Введите пароль" required autocomplete="current-password" />
|
|
</div>
|
|
<button type="submit" class="lg2-submit">Войти</button>
|
|
</form>
|
|
<button type="button" class="lg2-request-btn" id="btn-request-access" data-open-access-modal="1">Запросить доступ</button>
|
|
<div class="lg2-auth-foot"><a href="mailto:ruslan@ipcom.su">Made by Galyaviev</a></div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Request Access Modal -->
|
|
<div id="access-modal" class="access-modal-overlay" style="display:none" aria-modal="true" role="dialog">
|
|
<div class="access-modal">
|
|
<div class="access-modal-header">
|
|
<div class="access-modal-title">Запрос на доступ</div>
|
|
|
|
</div>
|
|
<div class="access-modal-body">
|
|
<div class="access-field">
|
|
<label>Имя и фамилия <span class="req">*</span></label>
|
|
<input id="am-name" type="text" placeholder="Иван Иванов" />
|
|
</div>
|
|
<div class="access-field">
|
|
<label>Название компании <span class="req">*</span></label>
|
|
<input id="am-company" type="text" placeholder="ООО Компания" />
|
|
</div>
|
|
<div class="access-field">
|
|
<label>Email <span class="req">*</span></label>
|
|
<input id="am-email" type="email" placeholder="ivan@company.ru" />
|
|
</div>
|
|
<div class="access-field">
|
|
<label>Телефон <span class="req">*</span></label>
|
|
<input id="am-phone" type="tel" placeholder="+7 (999) 000-00-00" />
|
|
</div>
|
|
<div class="access-field">
|
|
<label>Ваш менеджер в MONT</label>
|
|
<input id="am-manager" type="text" placeholder="Если известно — укажите имя" />
|
|
</div>
|
|
<div class="access-field">
|
|
<label style="display:flex;align-items:center;justify-content:space-between">
|
|
Интересующие продукты
|
|
<label id="am-select-all-wrap" style="display:none;font-weight:400;font-size:13px;cursor:pointer;gap:6px;align-items:center">
|
|
<input type="checkbox" id="am-select-all" style="cursor:pointer" />
|
|
<span>Выбрать все</span>
|
|
</label>
|
|
</label>
|
|
<div id="am-products" class="access-products-wrap">
|
|
<div class="access-products-loading">Загрузка...</div>
|
|
</div>
|
|
</div>
|
|
<div class="access-consent-field">
|
|
<label class="access-consent-label">
|
|
<input type="checkbox" id="am-consent" />
|
|
<span>Согласен на <a href="/privacy" target="_blank" class="access-consent-link">обработку персональных данных</a></span>
|
|
</label>
|
|
</div>
|
|
<div id="am-error" class="access-modal-error" style="display:none"></div>
|
|
</div>
|
|
<div class="access-modal-footer">
|
|
<button type="button" class="access-btn-cancel" id="am-cancel">Отмена</button>
|
|
<button type="button" class="access-btn-submit" id="am-submit">Запросить доступ</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
const overlay = document.getElementById('access-modal');
|
|
const btnCancel = document.getElementById('am-cancel');
|
|
const btnSubmit = document.getElementById('am-submit');
|
|
const errEl = document.getElementById('am-error');
|
|
let productsLoaded = false;
|
|
|
|
function resetAccessForm() {
|
|
if (!document.getElementById('am-name')) {
|
|
document.querySelector('.access-modal-body').innerHTML = `
|
|
<div class="access-field"><label>Имя и фамилия <span class="req">*</span></label><input id="am-name" type="text" placeholder="Иван Иванов" /></div>
|
|
<div class="access-field"><label>Название компании <span class="req">*</span></label><input id="am-company" type="text" placeholder="ООО Компания" /></div>
|
|
<div class="access-field"><label>Email <span class="req">*</span></label><input id="am-email" type="email" placeholder="ivan@company.ru" /></div>
|
|
<div class="access-field"><label>Телефон <span class="req">*</span></label><input id="am-phone" type="tel" placeholder="+7 (999) 000-00-00" /></div>
|
|
<div class="access-field"><label>Ваш менеджер в MONT</label><input id="am-manager" type="text" placeholder="Если известно — укажите имя" /></div>
|
|
<div class="access-field"><label>Интересующие продукты</label><div id="am-products" class="access-products-wrap"><div class="access-products-loading">Загрузка...</div></div></div>
|
|
<div class="access-consent-field"><label class="access-consent-label"><input type="checkbox" id="am-consent" /><span>Согласен на <a href="/privacy" target="_blank" class="access-consent-link">обработку персональных данных</a></span></label></div>
|
|
<div id="am-error" class="access-modal-error" style="display:none"></div>`;
|
|
document.querySelector('.access-modal-footer').innerHTML = `<button type="button" class="access-btn-cancel" id="am-cancel">Отмена</button><button type="button" class="access-btn-submit" id="am-submit">Запросить доступ</button>`;
|
|
document.getElementById('am-cancel').addEventListener('click', closeModal);
|
|
document.getElementById('am-submit').addEventListener('click', submitForm);
|
|
document.querySelectorAll('#am-name,#am-company,#am-email,#am-phone').forEach(function(el){ el.addEventListener('input', function(){ el.classList.remove('am-invalid'); }); });
|
|
productsLoaded = false;
|
|
} else {
|
|
['am-name','am-company','am-email','am-phone','am-manager'].forEach(function(id){ var el=document.getElementById(id); if(el){el.value='';el.classList.remove('am-invalid');} });
|
|
document.querySelectorAll('#am-products input[type=checkbox]').forEach(function(cb){ cb.checked=false; });
|
|
var saBox = document.getElementById('am-select-all'); if (saBox) saBox.checked = false;
|
|
var err=document.getElementById('am-error'); if(err) err.style.display='none';
|
|
var btn=document.getElementById('am-submit'); if(btn){btn.disabled=false;btn.textContent='Запросить доступ';}
|
|
}
|
|
}
|
|
|
|
function openModal() {
|
|
resetAccessForm();
|
|
overlay.style.display = 'flex';
|
|
document.body.style.overflow = 'hidden';
|
|
if (!productsLoaded) loadProducts();
|
|
}
|
|
|
|
function closeModal() {
|
|
overlay.style.display = 'none';
|
|
document.body.style.overflow = '';
|
|
errEl.style.display = 'none';
|
|
document.querySelectorAll('.am-invalid').forEach(el => el.classList.remove('am-invalid'));
|
|
}
|
|
window._closeAccessModal = function() { closeModal(); productsLoaded = false; };
|
|
|
|
async function loadProducts() {
|
|
const wrap = document.getElementById('am-products');
|
|
try {
|
|
const res = await fetch('/api/public/services-by-category');
|
|
const data = await res.json();
|
|
wrap.innerHTML = '';
|
|
for (const [cat, svcs] of Object.entries(data)) {
|
|
const group = document.createElement('div');
|
|
group.className = 'access-products-group';
|
|
group.innerHTML = '<div class="access-products-cat">' + cat + '</div>';
|
|
for (const svc of svcs) {
|
|
const lbl = document.createElement('label');
|
|
lbl.className = 'access-product-item';
|
|
lbl.innerHTML = '<input type="checkbox" value="' + svc.name.replace(/"/g, '"') + '" /><span>' + svc.name + '</span>';
|
|
group.appendChild(lbl);
|
|
}
|
|
wrap.appendChild(group);
|
|
}
|
|
productsLoaded = true;
|
|
// Show "select all" checkbox
|
|
const saWrap = document.getElementById('am-select-all-wrap');
|
|
if (saWrap) saWrap.style.display = 'flex';
|
|
const saBox = document.getElementById('am-select-all');
|
|
if (saBox) {
|
|
saBox.addEventListener('change', function() {
|
|
document.querySelectorAll('#am-products input[type=checkbox]').forEach(function(cb){ cb.checked = saBox.checked; });
|
|
});
|
|
// Keep "select all" in sync when individual boxes change
|
|
wrap.addEventListener('change', function(e) {
|
|
if (e.target === saBox) return;
|
|
const all = [...document.querySelectorAll('#am-products input[type=checkbox]')];
|
|
saBox.checked = all.length > 0 && all.every(function(cb){ return cb.checked; });
|
|
});
|
|
}
|
|
} catch(e) {
|
|
wrap.innerHTML = '<div class="access-products-loading">Не удалось загрузить список</div>';
|
|
}
|
|
}
|
|
|
|
async function submitForm() {
|
|
const nameEl = document.getElementById('am-name');
|
|
const companyEl = document.getElementById('am-company');
|
|
const emailEl = document.getElementById('am-email');
|
|
const phoneEl = document.getElementById('am-phone');
|
|
const managerEl = document.getElementById('am-manager');
|
|
const submitBtn = document.getElementById('am-submit');
|
|
const errorEl = document.getElementById('am-error');
|
|
|
|
const name = nameEl ? nameEl.value.trim() : '';
|
|
const company = companyEl ? companyEl.value.trim() : '';
|
|
const email = emailEl ? emailEl.value.trim() : '';
|
|
const phone = phoneEl ? phoneEl.value.trim() : '';
|
|
const manager = managerEl ? managerEl.value.trim() : '';
|
|
const checked = [...document.querySelectorAll('#am-products input[type=checkbox]:checked')];
|
|
const products = checked.map(c => c.value);
|
|
|
|
const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
const phoneRe = /^[\+\d][\d\s\-\(\)]{6,18}$/;
|
|
|
|
const consentEl = document.getElementById('am-consent');
|
|
const fields = [
|
|
{ el: nameEl, check: () => !!name, msg: 'Введите имя и фамилию' },
|
|
{ el: companyEl, check: () => !!company, msg: 'Введите название компании' },
|
|
{ el: emailEl, check: () => emailRe.test(email), msg: 'Введите корректный email' },
|
|
{ el: phoneEl, check: () => phoneRe.test(phone), msg: 'Введите корректный номер телефона' },
|
|
];
|
|
|
|
const errors = [];
|
|
fields.forEach(f => {
|
|
if (f.el && !f.check()) { f.el.classList.add('am-invalid'); errors.push(f.msg); }
|
|
else if (f.el) f.el.classList.remove('am-invalid');
|
|
});
|
|
if (!consentEl || !consentEl.checked) {
|
|
errors.push('Необходимо согласие на обработку персональных данных');
|
|
const cf = document.querySelector('.access-consent-field');
|
|
if (cf) cf.classList.add('am-invalid-consent');
|
|
} else {
|
|
const cf = document.querySelector('.access-consent-field');
|
|
if (cf) cf.classList.remove('am-invalid-consent');
|
|
}
|
|
|
|
if (errors.length) {
|
|
if (errorEl) { errorEl.textContent = errors.join(' • '); errorEl.style.display = 'block'; }
|
|
return;
|
|
}
|
|
|
|
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Отправка...'; }
|
|
if (errorEl) errorEl.style.display = 'none';
|
|
|
|
try {
|
|
const res = await fetch('/api/request-access', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({name, company, email, phone, manager, products}),
|
|
});
|
|
if (!res.ok) {
|
|
const d = await res.json().catch(() => ({}));
|
|
throw new Error(d.detail || 'Ошибка отправки');
|
|
}
|
|
const body = document.querySelector('#access-modal .access-modal-body');
|
|
const footer = document.querySelector('#access-modal .access-modal-footer');
|
|
body.innerHTML = '<div class="am-success-msg">' +
|
|
'<div class="am-success-icon">✓</div>' +
|
|
'<div class="am-success-title">Запрос отправлен</div>' +
|
|
'<div class="am-success-sub">После утверждения доступы придут на электронную почту <strong>' + email + '</strong></div>' +
|
|
'</div>';
|
|
footer.innerHTML = '<button type="button" class="access-btn-cancel" onclick="window._closeAccessModal()">Закрыть</button>';
|
|
} catch(e) {
|
|
const eb = document.getElementById('am-error');
|
|
if (eb) { eb.textContent = e.message || 'Ошибка отправки, попробуйте позже'; eb.style.display = 'block'; }
|
|
const sb = document.getElementById('am-submit');
|
|
if (sb) { sb.disabled = false; sb.textContent = 'Запросить доступ'; }
|
|
}
|
|
}
|
|
|
|
// Clear invalid highlight on input
|
|
document.querySelectorAll('#am-name,#am-company,#am-email,#am-phone').forEach(el => {
|
|
el.addEventListener('input', () => el.classList.remove('am-invalid'));
|
|
});
|
|
|
|
// Wire up request-access button
|
|
document.querySelectorAll('.login-request-btn, [data-open-access-modal]').forEach(el => {
|
|
el.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
openModal();
|
|
});
|
|
});
|
|
|
|
btnCancel.addEventListener('click', closeModal);
|
|
btnSubmit.addEventListener('click', submitForm);
|
|
overlay.addEventListener('click', function(e) {
|
|
if (e.target === overlay) closeModal();
|
|
});
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') closeModal();
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
<!-- Cookie Banner -->
|
|
<div id="cookie-banner" style="display:none;position:fixed;bottom:0;left:0;right:0;z-index:99999;background:rgba(7,15,28,0.97);border-top:1px solid rgba(255,255,255,0.1);backdrop-filter:blur(8px);padding:14px 24px;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;">
|
|
<p style="margin:0;font-size:0.85rem;color:#c8d8ea;line-height:1.5;flex:1;min-width:200px;">
|
|
Мы используем файлы cookie, чтобы сделать работу с сайтом удобнее. Нажмите «Принять», чтобы согласиться с использованием файлов cookie в соответствии с <a href="https://www.mont.ru/ru-ru/confidential" target="_blank" style="color:#ff9d3d;text-decoration:underline;">Политикой конфиденциальности</a>.
|
|
</p>
|
|
<button onclick="document.getElementById('cookie-banner').style.display='none';localStorage.setItem('cookie_accepted','1');" style="flex-shrink:0;padding:8px 22px;background:linear-gradient(135deg,#ffb15c,#e88420);color:#1a0f00;border:none;border-radius:8px;font-size:0.88rem;font-weight:700;cursor:pointer;white-space:nowrap;">Принять</button>
|
|
</div>
|
|
<script>
|
|
if (!localStorage.getItem('cookie_accepted')) {
|
|
document.getElementById('cookie-banner').style.display = 'flex';
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|