Bundles feature WIP + fix request-access products cap (20 -> 200)

- 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>
This commit is contained in:
2026-09-29 13:58:09 +00:00
parent f5c53f01bf
commit 40de4d5a0f
12 changed files with 2407 additions and 457 deletions
+207
View File
@@ -0,0 +1,207 @@
<!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;
}
.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 14px;overflow-wrap:break-word}
.dv-hero-lede{font:400 15px/1.6 "Ubuntu",sans-serif;color:var(--dv-fg-dim);max-width:70ch;margin:0}
.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-scn-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px}
.dv-scn-card{position:relative;background:var(--dv-panel);border:1px solid var(--dv-line);border-radius:12px;
padding:18px 18px 16px;opacity:.85}
.dv-scn-title{font:600 15px/1.35 "Ubuntu",sans-serif;margin:0 0 8px;padding-right:70px}
.dv-scn-desc{font:400 13.5px/1.55 "Ubuntu",sans-serif;color:var(--dv-fg-dim);margin:0}
.dv-scn-badge{position:absolute;top:16px;right:16px;font:700 10px/1 "Ubuntu Mono",monospace;letter-spacing:.06em;
text-transform:uppercase;color:var(--dv-accent);background:var(--dv-accent-dim);border:1px solid var(--dv-accent);
border-radius:999px;padding:4px 9px}
.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)}
</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" href="/">
<span class="dv-rail-dot"></span>Все сервисы
</a>
</nav>
</div>
<div>
<div class="dv-rail-kicker">Скоро</div>
<nav class="dv-rail-nav">
<a class="dv-rail-link active" 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>
<p class="dv-hero-lede">Заказчику редко нужен один продукт в вакууме — обычно решение должно встроиться в существующую или новую инфраструктуру. Мы собираем на полигоне связку из нескольких продуктов, имитирующую реальный сценарий внедрения, чтобы показать не «продукт», а готовое решение задачи. Раздел готовится — ниже сценарии, которые уже прорабатываются.</p>
</div>
<section class="dv-section">
<div class="dv-section-head"><h2>В подготовке</h2></div>
<div class="dv-scn-grid">
<div class="dv-scn-card">
<span class="dv-scn-badge">Готовится</span>
<div class="dv-scn-title">Миграция домена</div>
<div class="dv-scn-desc">ALDpro в связке с рабочими станциями на Astra Linux, РЕД ОС и Альт Рабочая станция — вход, групповые политики и совместная работа каталога с разными ОС на местах.</div>
</div>
<div class="dv-scn-card">
<span class="dv-scn-badge">Готовится</span>
<div class="dv-scn-title">VDI-инфраструктура</div>
<div class="dv-scn-desc">Termidesk поверх Ред Виртуализация — типовой сценарий виртуальных рабочих столов, от сервера виртуализации до подключения пользователя.</div>
</div>
<div class="dv-scn-card">
<span class="dv-scn-badge">Готовится</span>
<div class="dv-scn-title">Резервное копирование виртуальной инфраструктуры</div>
<div class="dv-scn-desc">RuBackup или Vinchin в связке с Альт Виртуализация (PVE) — бэкап и восстановление виртуальных машин на живом кластере.</div>
</div>
<div class="dv-scn-card">
<span class="dv-scn-badge">Готовится</span>
<div class="dv-scn-title">Импортозамещение рабочего места целиком</div>
<div class="dv-scn-desc">ОС (Astra Linux / РЕД ОС / Альт Рабочая станция) + офисный пакет (АльтерОфис / P7-КС2024) + почта (CommuniGate Pro / RuPost) — полный стек рабочего места сотрудника.</div>
</div>
<div class="dv-scn-card">
<span class="dv-scn-badge">Готовится</span>
<div class="dv-scn-title">Контейнеризированное приложение с базой данных</div>
<div class="dv-scn-desc">Deckhouse Platform (Kubernetes) + Postgres Pro или Tantor — развёртывание приложения с реальной СУБД под капотом.</div>
</div>
<div class="dv-scn-card">
<span class="dv-scn-badge">Готовится</span>
<div class="dv-scn-title">Наблюдаемость виртуальной инфраструктуры</div>
<div class="dv-scn-desc">Astra Мониторинг поверх Ред Виртуализация или Альт Виртуализация (PVE) — контроль состояния кластера и виртуальных машин.</div>
</div>
</div>
</section>
<footer class="dv-page-footer"><span>MONT PROVING GROUND</span><a href="mailto:ruslan@ipcom.su">Made by Galyaviev</a></footer>
</main>
</div>
</body>
</html>