40de4d5a0f
- app/main.py: raise products_raw[:20] cap to [:200] in /api/request-access and /api/request-more-access - was silently truncating access requests with more than 20 selected products - accumulated bundles/product-page WIP (config, models, runtime, templates) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
423 lines
26 KiB
HTML
423 lines
26 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>MONT - инфрастуктурный полигон</title>
|
||
<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>
|
||
#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}
|
||
|
||
:root{
|
||
--dv-bg:#eef1f5; --dv-panel:#fff; --dv-raised:#fff;
|
||
--dv-line:#d7dee6; --dv-line-soft:#e3e8ee;
|
||
--dv-fg:#16202c; --dv-fg-dim:#5c6b7c; --dv-fg-faint:#93a1b0;
|
||
--dv-accent:#c96c14; --dv-accent-dim:rgba(201,108,20,.10); --dv-accent-ink:#2a1400;
|
||
--dv-good:#1f9d63;
|
||
}
|
||
/* style.css has several older `.dashboard-page{background:...!important}`
|
||
sky-gradient rules left over from the previous design - beat them with
|
||
!important too, otherwise they win regardless of source order. */
|
||
.dashboard-page{background:var(--dv-bg) !important;color:var(--dv-fg);
|
||
font-family:"Ubuntu","IBM Plex Sans",system-ui,sans-serif;min-height:100vh;overflow-x:hidden}
|
||
.dashboard-page *{box-sizing:border-box;min-width:0}
|
||
.dashboard-page a{color:inherit;text-decoration:none}
|
||
|
||
.dv-shell{display:grid;grid-template-columns:248px minmax(0,1fr);min-height:100vh}
|
||
@media (max-width:980px){ .dv-shell{grid-template-columns:minmax(0,1fr)} }
|
||
|
||
.dv-rail{border-right:1px solid var(--dv-line);padding:26px 16px 26px 24px;position:sticky;top:0;
|
||
align-self:start;height:100vh;overflow:auto;display:flex;flex-direction:column;gap:22px}
|
||
@media (max-width:980px){ .dv-rail{position:static;height:auto;border-right:none;border-bottom:1px solid var(--dv-line);padding:20px 18px} }
|
||
.dv-rail-brand{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||
.dv-rail-brand img{height:30px}
|
||
.dv-rail-kicker{font:700 11px/1 "Ubuntu Mono",monospace;letter-spacing:.16em;color:var(--dv-fg-faint);text-transform:uppercase;margin:0 0 8px 10px}
|
||
.dv-rail-nav{display:flex;flex-direction:column;gap:2px}
|
||
@media (max-width:980px){ .dv-rail-nav{flex-direction:row;flex-wrap:wrap;gap:6px} }
|
||
.dv-rail-link{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:6px;
|
||
font:500 14px/1.3 "Ubuntu",sans-serif;color:var(--dv-fg-dim);border:1px solid transparent}
|
||
.dv-rail-link:hover{background:var(--dv-panel);color:var(--dv-fg);border-color:var(--dv-line)}
|
||
.dv-rail-dot{width:6px;height:6px;border-radius:50%;background:var(--dv-fg-faint);flex-shrink:0}
|
||
.dv-rail-link:hover .dv-rail-dot{background:var(--dv-accent)}
|
||
.dv-rail-count{margin-left:auto;font:600 11px/1 "Ubuntu Mono",monospace;color:var(--dv-fg-faint)}
|
||
.dv-rail-link.active{background:var(--dv-accent-dim);border-color:var(--dv-accent);color:var(--dv-fg)}
|
||
.dv-rail-link.active .dv-rail-dot{background:var(--dv-accent)}
|
||
.dv-rail-link.active .dv-rail-count{color:var(--dv-accent)}
|
||
.dv-rail-foot{margin-top:auto;font:400 12px/1.6 "Ubuntu Mono",monospace;color:var(--dv-fg-faint)}
|
||
@media (max-width:980px){ .dv-rail-foot{display:none} }
|
||
|
||
.dv-main{padding:0 clamp(18px,3vw,48px) 70px;min-width:0}
|
||
.dv-topbar{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:20px 0;
|
||
border-bottom:1px solid var(--dv-line);flex-wrap:wrap}
|
||
.dv-who{display:flex;align-items:center;gap:10px}
|
||
.dv-who-avatar{width:32px;height:32px;border-radius:8px;background:var(--dv-raised);border:1px solid var(--dv-line);
|
||
display:flex;align-items:center;justify-content:center;font:700 13px/1 "Ubuntu Mono",monospace;color:var(--dv-accent)}
|
||
.dv-who-name{font:500 14.5px/1.2 "Ubuntu",sans-serif}
|
||
.dv-who-role{font:400 12px/1.2 "Ubuntu Mono",monospace;color:var(--dv-fg-faint)}
|
||
.dv-topbar-actions{display:flex;gap:8px}
|
||
.dv-btn{font:600 13px/1 "Ubuntu",sans-serif;padding:9px 16px;border-radius:7px;border:1px solid var(--dv-line);
|
||
background:var(--dv-panel);color:var(--dv-fg-dim);cursor:pointer}
|
||
.dv-btn:hover{color:var(--dv-fg);border-color:var(--dv-fg-faint)}
|
||
|
||
.dv-hero{padding:32px 0 30px;border-bottom:1px solid var(--dv-line)}
|
||
.dv-hero-eyebrow{font:700 12.5px/1 "Ubuntu Mono",monospace;letter-spacing:.16em;text-transform:uppercase;color:var(--dv-accent);margin:0 0 10px}
|
||
.dv-hero h1{font:800 clamp(28px,3.8vw,40px)/1.1 "Ubuntu",sans-serif;letter-spacing:-.01em;margin:0 0 4px;overflow-wrap:break-word}
|
||
.dv-filter-note{font:400 13.5px/1.4 "Ubuntu",sans-serif;color:var(--dv-fg-faint);margin:10px 0 0}
|
||
.dv-filter-note b{color:var(--dv-accent)}
|
||
.dv-filter-note a{margin-left:8px;text-decoration:underline;color:var(--dv-fg-faint)}
|
||
.dv-readouts{display:flex;flex-wrap:wrap;gap:0;border:1px solid var(--dv-line);border-radius:10px;overflow:hidden;width:fit-content;margin-top:16px}
|
||
.dv-readout{padding:12px 22px;border-right:1px solid var(--dv-line)}
|
||
.dv-readout:last-child{border-right:none}
|
||
.dv-readout-val{font:700 23px/1 "Ubuntu Mono",monospace;font-variant-numeric:tabular-nums}
|
||
.dv-readout-label{font:600 10.5px/1 "Ubuntu Mono",monospace;letter-spacing:.08em;text-transform:uppercase;color:var(--dv-fg-faint);margin-top:6px}
|
||
|
||
.dv-section{padding:36px 0 6px}
|
||
.dv-section-head{display:flex;align-items:baseline;gap:12px;margin-bottom:16px}
|
||
.dv-section-head h2{font:700 13.5px/1 "Ubuntu Mono",monospace;letter-spacing:.1em;text-transform:uppercase;margin:0}
|
||
.dv-section-head::after{content:"";flex:1;height:1px;background:var(--dv-line)}
|
||
.dv-section-count{font:600 12px/1 "Ubuntu Mono",monospace;color:var(--dv-fg-faint)}
|
||
.dv-empty{padding:18px 0;color:var(--dv-fg-faint);font:400 13px/1.5 "Ubuntu",sans-serif}
|
||
|
||
.dv-tile-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:14px}
|
||
.dv-tile{position:relative;background:var(--dv-panel);border:1px solid var(--dv-line);border-radius:12px;
|
||
padding:16px;display:flex;flex-direction:column;gap:10px;height:266px;
|
||
transition:transform .15s,border-color .15s}
|
||
.dv-tile:hover{border-color:var(--dv-accent);transform:translateY(-2px)}
|
||
.dv-tile-hit{position:absolute;inset:0;z-index:0;border-radius:12px}
|
||
.dv-tile-top{display:flex;align-items:center;gap:12px;position:relative;z-index:1;pointer-events:none}
|
||
.dv-tile-plate{flex:none;width:84px;height:84px;border-radius:14px;background:var(--dv-raised);border:1px solid var(--dv-line-soft);
|
||
display:flex;align-items:center;justify-content:center}
|
||
.dv-tile-plate img{width:60px;height:60px;object-fit:contain}
|
||
.dv-tile-name{font:600 15px/1.3 "Ubuntu",sans-serif;overflow-wrap:break-word}
|
||
.dv-tile-desc{
|
||
margin:0;font:400 13.5px/1.55 "Ubuntu",sans-serif;color:var(--dv-fg-dim);position:relative;z-index:1;
|
||
flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;pointer-events:auto;padding-right:2px;
|
||
}
|
||
.dv-tile-desc p{margin:0}
|
||
.dv-tile-desc::-webkit-scrollbar{width:5px}
|
||
.dv-tile-desc::-webkit-scrollbar-thumb{background:var(--dv-line);border-radius:3px}
|
||
.dv-tile-desc::-webkit-scrollbar-thumb:hover{background:var(--dv-fg-faint)}
|
||
.dv-tile-enter{
|
||
position:relative;z-index:2;margin-top:auto;align-self:flex-start;padding:7px 14px;border-radius:7px;
|
||
border:1px solid var(--dv-line);background:transparent;color:var(--dv-fg-dim);font:600 12.5px/1 "Ubuntu",sans-serif;cursor:pointer;
|
||
}
|
||
.dv-tile:hover .dv-tile-enter{border-color:var(--dv-accent);color:var(--dv-accent)}
|
||
.dv-tile-led{position:absolute;top:14px;right:14px;width:6px;height:6px;border-radius:50%;background:var(--dv-good);box-shadow:0 0 6px var(--dv-good);z-index:1}
|
||
|
||
.dv-tile.locked{opacity:.6;filter:grayscale(.4)}
|
||
.dv-tile.locked:hover{border-color:var(--dv-line);transform:none}
|
||
.dv-tile.locked .dv-tile-plate img{filter:grayscale(1)}
|
||
.dv-tile-lock{position:absolute;top:14px;right:14px;font-size:13px;opacity:.7;z-index:1}
|
||
.dv-tile-request{
|
||
position:relative;z-index:2;margin-top:auto;align-self:flex-start;padding:7px 14px;border-radius:7px;
|
||
border:1px dashed var(--dv-fg-faint);background:transparent;color:var(--dv-fg-faint);font:600 12.5px/1 "Ubuntu",sans-serif;cursor:pointer;
|
||
}
|
||
.dv-tile-request:hover{border-color:var(--dv-accent);border-style:solid;color:var(--dv-accent)}
|
||
|
||
.dv-page-footer{margin-top:50px;padding-top:20px;border-top:1px solid var(--dv-line);display:flex;
|
||
justify-content:space-between;flex-wrap:wrap;gap:8px;font:400 12px/1 "Ubuntu Mono",monospace;color:var(--dv-fg-faint)}
|
||
|
||
/* --- request-more-access modal --- */
|
||
.req-modal-overlay{display:none;position:fixed;inset:0;z-index:9000;background:rgba(6,9,14,.72);
|
||
align-items:center;justify-content:center;padding:1rem}
|
||
.req-modal-overlay.open{display:flex}
|
||
.req-modal{background:var(--dv-panel);border:1px solid var(--dv-line);border-radius:14px;width:100%;max-width:420px;
|
||
box-shadow:0 24px 80px rgba(0,0,0,.45);padding:22px 22px 20px}
|
||
.req-modal h3{margin:0 0 4px;font:700 17px/1.3 "Ubuntu",sans-serif}
|
||
.req-modal .sub{margin:0 0 16px;font:400 12.5px/1.5 "Ubuntu",sans-serif;color:var(--dv-fg-faint)}
|
||
.req-list{display:flex;flex-direction:column;gap:8px;margin-bottom:16px;max-height:220px;overflow-y:auto}
|
||
.req-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--dv-line);border-radius:8px;font:500 13px/1.3 "Ubuntu",sans-serif}
|
||
.req-item input{accent-color:var(--dv-accent);width:15px;height:15px}
|
||
.req-note{width:100%;box-sizing:border-box;background:var(--dv-raised);border:1px solid var(--dv-line);border-radius:8px;
|
||
padding:9px 11px;color:var(--dv-fg);font:400 13px/1.5 "Ubuntu",sans-serif;resize:vertical;min-height:56px;margin-bottom:16px}
|
||
.req-note::placeholder{color:var(--dv-fg-faint)}
|
||
.req-actions{display:flex;justify-content:flex-end;gap:10px}
|
||
.req-cancel{padding:9px 16px;border-radius:8px;border:1px solid var(--dv-line);background:transparent;color:var(--dv-fg-dim);font:600 12.5px/1 "Ubuntu",sans-serif;cursor:pointer}
|
||
.req-submit{padding:9px 18px;border-radius:8px;border:none;background:var(--dv-accent);color:var(--dv-accent-ink);font:700 12.5px/1 "Ubuntu",sans-serif;cursor:pointer}
|
||
.req-submit:disabled{opacity:.55;cursor:default}
|
||
.req-error{background:rgba(255,107,107,.12);border:1px solid rgba(255,107,107,.35);color:#ff6b6b;
|
||
border-radius:8px;padding:8px 11px;font:500 12.5px/1.4 "Ubuntu",sans-serif;margin-bottom:14px;display:none}
|
||
.req-success{text-align:center;padding:10px 0 4px}
|
||
.req-success .ico{font-size:30px;margin-bottom:10px;color:var(--dv-good)}
|
||
.req-success h4{margin:0 0 6px;font:700 16px/1.3 "Ubuntu",sans-serif}
|
||
.req-success p{margin:0;font:400 12.5px/1.5 "Ubuntu",sans-serif;color:var(--dv-fg-dim)}
|
||
|
||
</style>
|
||
</head>
|
||
<body class="dashboard-page">
|
||
<div id="mobile-wall">
|
||
<a href="https://4mont.ru"><img src="/static/logo.png?v=4" alt="MONT" style="position:absolute;top:1.2rem;left:50%;transform:translateX(-50%);height:clamp(4rem,16vw,6rem);opacity:.9"></a>
|
||
<div class="mw-icon">🖥️</div>
|
||
<div class="mw-title">Только для компьютера</div>
|
||
<div class="mw-sub">Инфраструктурный полигон MONT оптимизирован для работы на ПК.<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:ruslan@ipcom.su" style="color:inherit;text-decoration:none">Made by Galyaviev</a></div>
|
||
</div>
|
||
|
||
<div class="dv-shell">
|
||
<aside class="dv-rail">
|
||
<div class="dv-rail-brand"><a href="https://4mont.ru"><img src="/static/logo-color.png?v=1" alt="MONT" /></a></div>
|
||
<div>
|
||
<div class="dv-rail-kicker">Категории</div>
|
||
<nav class="dv-rail-nav">
|
||
<a class="dv-rail-link {% if not selected_category_slug %}active{% endif %}" href="/">
|
||
<span class="dv-rail-dot" style="{% if not selected_category_slug %}background:var(--dv-accent){% endif %}"></span>
|
||
Все сервисы<span class="dv-rail-count">{{ '%02d'|format(total_catalog_count) }}</span>
|
||
</a>
|
||
{% for category in categories %}
|
||
<a class="dv-rail-link {% if selected_category_slug == category.slug %}active{% endif %}" href="/?category={{ category.slug }}">
|
||
<span class="dv-rail-dot"></span>{{ category.name }}<span class="dv-rail-count">{{ '%02d'|format(category_counts.get(category.slug, 0)) }}</span>
|
||
</a>
|
||
{% endfor %}
|
||
</nav>
|
||
</div>
|
||
<div>
|
||
<div class="dv-rail-kicker">Скоро</div>
|
||
<nav class="dv-rail-nav">
|
||
<a class="dv-rail-link" href="/bundles">
|
||
<span class="dv-rail-dot"></span>Готовые связки<span class="dv-rail-count">NEW</span>
|
||
</a>
|
||
</nav>
|
||
</div>
|
||
<div class="dv-rail-foot">SESSION LIMIT: {{ max_active_services }}<br/>IDLE TIMEOUT: {{ idle_timeout_min }} MIN<br/>RESET: 00:00 MSK</div>
|
||
</aside>
|
||
|
||
<main class="dv-main">
|
||
<div class="dv-topbar">
|
||
<div class="dv-who">
|
||
<div class="dv-who-avatar">{{ ((user.first_name[0] if user.first_name else user.username[0]) + (user.last_name[0] if user.last_name else ''))|upper }}</div>
|
||
<div>
|
||
<div class="dv-who-name">{{ (user.first_name + ' ' + user.last_name)|trim or user.username }}</div>
|
||
<div class="dv-who-role">оператор стенда</div>
|
||
</div>
|
||
</div>
|
||
<div class="dv-topbar-actions">
|
||
{% if user.is_admin %}
|
||
<a class="dv-btn" href="/admin">Администрирование</a>
|
||
{% endif %}
|
||
<form method="post" action="/logout" style="margin:0">
|
||
<button type="submit" class="dv-btn">Выход</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="dv-hero">
|
||
<p class="dv-hero-eyebrow">MONT · Испытательный контур</p>
|
||
<h1>Инфраструктурный полигон</h1>
|
||
{% if session_notice %}<div class="dv-filter-note" style="color:#ff9d3d">{{ session_notice }}</div>{% endif %}
|
||
{% if selected_category_slug %}
|
||
{% set current_cat = categories|selectattr('slug','equalto',selected_category_slug)|list|first %}
|
||
<p class="dv-filter-note">Показана категория «<b>{{ current_cat.name if current_cat else selected_category_slug }}</b>»<a href="/">сбросить</a></p>
|
||
{% endif %}
|
||
<div class="dv-readouts">
|
||
<div class="dv-readout"><div class="dv-readout-val">{{ '%02d'|format(services|length) }}</div><div class="dv-readout-label">Доступно</div></div>
|
||
<div class="dv-readout"><div class="dv-readout-val">{{ '%02d'|format(categories|length) }}</div><div class="dv-readout-label">Категорий</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="dv-section">
|
||
<div class="dv-section-head"><h2>Ваши сервисы</h2><span class="dv-section-count">{{ '%02d'|format(services|length) }}</span></div>
|
||
{% if services %}
|
||
<div class="dv-tile-grid">
|
||
{% for service in services %}
|
||
<div class="dv-tile">
|
||
<a class="dv-tile-hit go-link" href="/go/{{ service.slug }}" aria-label="Открыть {{ service.name }}"></a>
|
||
<div class="dv-tile-top">
|
||
<div class="dv-tile-plate"><img src="{{ service.icon_path or '/static/service-placeholder.svg' }}" alt="" /></div>
|
||
<div class="dv-tile-name">{{ service.name }}</div>
|
||
</div>
|
||
<div class="dv-tile-desc">{{ service_comment_html.get(service.id, '') }}</div>
|
||
<a class="dv-tile-enter go-link" href="/go/{{ service.slug }}">Войти →</a>
|
||
<span class="dv-tile-led"></span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div class="dv-empty">{% if selected_category_slug %}В этой категории у вас пока нет доступных сервисов.{% else %}Вам пока не назначены сервисы — обратитесь к вашему менеджеру MONT.{% endif %}</div>
|
||
{% endif %}
|
||
</section>
|
||
|
||
{% if locked_services %}
|
||
<section class="dv-section">
|
||
<div class="dv-section-head"><h2>Доступно по запросу</h2><span class="dv-section-count">{{ '%02d'|format(locked_services|length) }}</span></div>
|
||
<div class="dv-tile-grid">
|
||
{% for service in locked_services %}
|
||
<div class="dv-tile locked">
|
||
<div class="dv-tile-top">
|
||
<div class="dv-tile-plate"><img src="{{ service.icon_path or '/static/service-placeholder.svg' }}" alt="" /></div>
|
||
<div class="dv-tile-name">{{ service.name }}</div>
|
||
</div>
|
||
<div class="dv-tile-desc">{{ service_comment_html.get(service.id, '') }}</div>
|
||
<button type="button" class="dv-tile-request" data-product="{{ service.name }}">Запросить доступ</button>
|
||
<span class="dv-tile-lock" title="Нет доступа">🔒</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</section>
|
||
{% endif %}
|
||
|
||
<footer class="dv-page-footer"><span>MONT PROVING GROUND</span><a href="mailto:ruslan@ipcom.su">Made by Galyaviev</a></footer>
|
||
</main>
|
||
</div>
|
||
|
||
<div class="req-modal-overlay" id="req-overlay">
|
||
<div class="req-modal" id="req-modal-body">
|
||
<h3>Запросить доступ</h3>
|
||
<p class="sub">Заявка уйдёт администратору полигона. После одобрения продукт появится у вас автоматически — заново логиниться не нужно.</p>
|
||
<div class="req-error" id="req-error"></div>
|
||
<div class="req-list" id="req-list"></div>
|
||
<textarea class="req-note" id="req-note" placeholder="Комментарий (необязательно) — например, для какого проекта нужен доступ"></textarea>
|
||
<div class="req-actions">
|
||
<button type="button" class="req-cancel" id="req-cancel">Отмена</button>
|
||
<button type="button" class="req-submit" id="req-submit">Отправить заявку</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(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;
|
||
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;
|
||
}
|
||
|
||
document.querySelectorAll('a.go-link[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) {}
|
||
window.location.href = href;
|
||
}, { capture: true });
|
||
});
|
||
})();
|
||
|
||
(function () {
|
||
const csrf = {{ csrf_token|tojson }};
|
||
const overlay = document.getElementById('req-overlay');
|
||
const listEl = document.getElementById('req-list');
|
||
const noteEl = document.getElementById('req-note');
|
||
const errEl = document.getElementById('req-error');
|
||
const modalBody = document.getElementById('req-modal-body');
|
||
|
||
function allLockedTiles() {
|
||
return Array.prototype.slice.call(document.querySelectorAll('.dv-tile.locked'));
|
||
}
|
||
|
||
function openRequestModal(preselect) {
|
||
errEl.style.display = 'none';
|
||
noteEl.value = '';
|
||
listEl.innerHTML = '';
|
||
allLockedTiles().forEach(function (t) {
|
||
const name = t.querySelector('.dv-tile-name').textContent;
|
||
const id = 'rq-' + name.replace(/[^a-zA-Z0-9]+/g, '-');
|
||
const row = document.createElement('label');
|
||
row.className = 'req-item';
|
||
row.innerHTML = '<input type="checkbox" id="' + id + '" value="' + name.replace(/"/g, '"') + '" ' +
|
||
(name === preselect ? 'checked' : '') + ' /><span>' + name + '</span>';
|
||
listEl.appendChild(row);
|
||
});
|
||
overlay.classList.add('open');
|
||
}
|
||
function closeModal() { overlay.classList.remove('open'); }
|
||
|
||
document.querySelectorAll('.dv-tile-request').forEach(function (btn) {
|
||
btn.addEventListener('click', function () { openRequestModal(btn.dataset.product); });
|
||
});
|
||
document.getElementById('req-cancel').addEventListener('click', closeModal);
|
||
overlay.addEventListener('click', function (e) { if (e.target === overlay) closeModal(); });
|
||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeModal(); });
|
||
|
||
async function submitRequest() {
|
||
const checked = Array.prototype.slice.call(document.querySelectorAll('#req-list input[type=checkbox]:checked'));
|
||
const products = checked.map(function (c) { return c.value; });
|
||
if (!products.length) {
|
||
errEl.textContent = 'Отметьте хотя бы один продукт';
|
||
errEl.style.display = 'block';
|
||
return;
|
||
}
|
||
const submitBtn = document.getElementById('req-submit');
|
||
submitBtn.disabled = true;
|
||
submitBtn.textContent = 'Отправка...';
|
||
errEl.style.display = 'none';
|
||
try {
|
||
const res = await fetch('/api/request-more-access', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
|
||
body: JSON.stringify({ products: products, note: noteEl.value.trim() }),
|
||
});
|
||
if (!res.ok) {
|
||
const d = await res.json().catch(function () { return {}; });
|
||
throw new Error(d.detail || 'Ошибка отправки');
|
||
}
|
||
modalBody.innerHTML =
|
||
'<div class="req-success">' +
|
||
'<div class="ico">✓</div><h4>Заявка отправлена</h4>' +
|
||
'<p>Администратор получит уведомление и одобрит доступ — продукт появится у вас автоматически, без повторного входа.</p>' +
|
||
'</div><div class="req-actions" style="margin-top:16px">' +
|
||
'<button type="button" class="req-cancel" id="req-close-success">Закрыть</button></div>';
|
||
document.getElementById('req-close-success').addEventListener('click', closeModal);
|
||
} catch (e) {
|
||
errEl.textContent = e.message || 'Ошибка отправки, попробуйте позже';
|
||
errEl.style.display = 'block';
|
||
submitBtn.disabled = false;
|
||
submitBtn.textContent = 'Отправить заявку';
|
||
}
|
||
}
|
||
document.getElementById('req-submit').addEventListener('click', submitRequest);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|