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:
@@ -45,3 +45,8 @@ SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "")
|
|||||||
SMTP_FROM_EMAIL = os.getenv("SMTP_FROM_EMAIL", "stand@4mont.ru")
|
SMTP_FROM_EMAIL = os.getenv("SMTP_FROM_EMAIL", "stand@4mont.ru")
|
||||||
SMTP_FROM_NAME = os.getenv("SMTP_FROM_NAME", "\u0418\u043d\u0444\u0440\u0430\u0441\u0442\u0443\u043a\u0442\u0443\u0440\u043d\u044b\u0439 \u043f\u043e\u043b\u0438\u0433\u043e\u043d MONT")
|
SMTP_FROM_NAME = os.getenv("SMTP_FROM_NAME", "\u0418\u043d\u0444\u0440\u0430\u0441\u0442\u0443\u043a\u0442\u0443\u0440\u043d\u044b\u0439 \u043f\u043e\u043b\u0438\u0433\u043e\u043d MONT")
|
||||||
PORTAL_URL = os.getenv("PORTAL_URL", "https://stend.4mont.ru")
|
PORTAL_URL = os.getenv("PORTAL_URL", "https://stend.4mont.ru")
|
||||||
|
|
||||||
|
# Second approval channel: email notification with click-to-confirm links,
|
||||||
|
# sent alongside the Telegram message when a new access request comes in.
|
||||||
|
ADMIN_NOTIFY_EMAIL = os.getenv("ADMIN_NOTIFY_EMAIL", "RGalyaviev@mont.ru")
|
||||||
|
SIGNING_KEY = os.getenv("SIGNING_KEY", "")
|
||||||
|
|||||||
+796
-80
File diff suppressed because one or more lines are too long
@@ -50,6 +50,7 @@ class Service(Base):
|
|||||||
svc_password: Mapped[str] = mapped_column(String(256), default="")
|
svc_password: Mapped[str] = mapped_column(String(256), default="")
|
||||||
svc_cred_hint: Mapped[str] = mapped_column(Text, default="")
|
svc_cred_hint: Mapped[str] = mapped_column(Text, default="")
|
||||||
icon_path: Mapped[str] = mapped_column(Text, default="")
|
icon_path: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
seo_description: Mapped[str] = mapped_column(Text, default="")
|
||||||
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
warm_pool_size: Mapped[int] = mapped_column(Integer, default=0)
|
warm_pool_size: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc))
|
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc))
|
||||||
@@ -130,3 +131,9 @@ class PendingAccessRequest(Base):
|
|||||||
portal_url: Mapped[str] = mapped_column(String(256), default="")
|
portal_url: Mapped[str] = mapped_column(String(256), default="")
|
||||||
status: Mapped[str] = mapped_column(String(16), default="pending")
|
status: Mapped[str] = mapped_column(String(16), default="pending")
|
||||||
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc))
|
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc))
|
||||||
|
# Telegram delivery tracking: the notification is sent once when the
|
||||||
|
# request is created; if that attempt fails (tel.4mont.ru down/timeout),
|
||||||
|
# a background retry loop resends telegram_message every 5 minutes until
|
||||||
|
# it succeeds, without recomputing geo/IP each time.
|
||||||
|
telegram_notified: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
telegram_message: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
|||||||
@@ -772,7 +772,10 @@ def ensure_schema_compatibility() -> None:
|
|||||||
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS svc_password VARCHAR(256) NOT NULL DEFAULT ''"))
|
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS svc_password VARCHAR(256) NOT NULL DEFAULT ''"))
|
||||||
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS svc_cred_hint TEXT NOT NULL DEFAULT ''"))
|
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS svc_cred_hint TEXT NOT NULL DEFAULT ''"))
|
||||||
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS icon_path TEXT NOT NULL DEFAULT ''"))
|
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS icon_path TEXT NOT NULL DEFAULT ''"))
|
||||||
|
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS seo_description TEXT NOT NULL DEFAULT ''"))
|
||||||
conn.execute(text("ALTER TABLE pending_access_requests ADD COLUMN IF NOT EXISTS portal_url VARCHAR(256) NOT NULL DEFAULT ''"))
|
conn.execute(text("ALTER TABLE pending_access_requests ADD COLUMN IF NOT EXISTS portal_url VARCHAR(256) NOT NULL DEFAULT ''"))
|
||||||
|
conn.execute(text("ALTER TABLE pending_access_requests ADD COLUMN IF NOT EXISTS telegram_notified BOOLEAN NOT NULL DEFAULT false"))
|
||||||
|
conn.execute(text("ALTER TABLE pending_access_requests ADD COLUMN IF NOT EXISTS telegram_message TEXT NOT NULL DEFAULT ''"))
|
||||||
conn.execute(
|
conn.execute(
|
||||||
text(
|
text(
|
||||||
"""
|
"""
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
+172
-1
@@ -23,8 +23,179 @@
|
|||||||
</script>
|
</script>
|
||||||
<noscript><div><img src="https://mc.yandex.ru/watch/109119977" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
<noscript><div><img src="https://mc.yandex.ru/watch/109119977" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||||
<!-- /Yandex.Metrika counter -->
|
<!-- /Yandex.Metrika counter -->
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--av-bg:#eef1f5; --av-panel:#fff; --av-raised:#fff;
|
||||||
|
--av-line:#d7dee6; --av-line-soft:#e3e8ee;
|
||||||
|
--av-fg:#16202c; --av-fg-dim:#5c6b7c; --av-fg-faint:#93a1b0;
|
||||||
|
--av-accent:#c96c14; --av-accent-dim:rgba(201,108,20,.10); --av-accent-ink:#2a1400;
|
||||||
|
--av-good:#1f9d63; --av-warn:#b5680a; --av-bad:#d3453f;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.admin-page-v2{
|
||||||
|
background:var(--av-bg) !important; color:var(--av-fg);
|
||||||
|
font-family:"Ubuntu","IBM Plex Sans",system-ui,sans-serif;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 *{ box-sizing:border-box; min-width:0; }
|
||||||
|
|
||||||
|
/* header */
|
||||||
|
body.admin-page-v2 .header{
|
||||||
|
background:var(--av-panel) !important; border-bottom:1px solid var(--av-line);
|
||||||
|
backdrop-filter:none;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .header-logo{ height:28px; }
|
||||||
|
body.admin-page-v2 .header > div > div{
|
||||||
|
font:600 13px/1 "Ubuntu Mono",monospace; color:var(--av-fg-dim); letter-spacing:.02em;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .btn-link.secondary{
|
||||||
|
background:transparent !important; border:1px solid var(--av-line); color:var(--av-fg-dim) !important;
|
||||||
|
font:600 13px/1 "Ubuntu",sans-serif; padding:9px 16px; border-radius:7px;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .btn-link.secondary:hover{ border-color:var(--av-accent); color:var(--av-fg) !important; }
|
||||||
|
|
||||||
|
/* layout / panels */
|
||||||
|
body.admin-page-v2 .admin-layout{ max-width:1500px; }
|
||||||
|
body.admin-page-v2 .panel{
|
||||||
|
background:var(--av-panel) !important; border:1px solid var(--av-line); box-shadow:none;
|
||||||
|
border-radius:12px;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .admin-intro{
|
||||||
|
background:var(--av-accent-dim) !important; border:1px solid var(--av-line);
|
||||||
|
border-left:3px solid var(--av-accent); color:var(--av-fg-dim) !important;
|
||||||
|
font:500 13px/1.55 "Ubuntu",sans-serif; letter-spacing:normal;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .admin-intro b{ color:var(--av-fg); }
|
||||||
|
|
||||||
|
/* tabs */
|
||||||
|
body.admin-page-v2 .tab-row{ gap:6px; padding-bottom:2px; }
|
||||||
|
body.admin-page-v2 .tab-btn{
|
||||||
|
background:transparent !important; color:var(--av-fg-dim) !important;
|
||||||
|
border:1px solid var(--av-line); border-radius:8px;
|
||||||
|
font:600 13px/1 "Ubuntu",sans-serif; padding:9px 16px;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .tab-btn:hover{ border-color:var(--av-fg-faint); color:var(--av-fg) !important; }
|
||||||
|
body.admin-page-v2 .tab-btn.active-tab{
|
||||||
|
background:var(--av-accent-dim) !important; border-color:var(--av-accent);
|
||||||
|
color:var(--av-fg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* split / lists */
|
||||||
|
body.admin-page-v2 .split{ grid-template-columns:minmax(280px,340px) minmax(0,1fr); }
|
||||||
|
@media (max-width:900px){ body.admin-page-v2 .split{ grid-template-columns:minmax(0,1fr); } }
|
||||||
|
body.admin-page-v2 .list-title{
|
||||||
|
font:700 11px/1 "Ubuntu Mono",monospace; letter-spacing:.1em; text-transform:uppercase;
|
||||||
|
color:var(--av-fg-faint);
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .list-box{
|
||||||
|
background:var(--av-raised) !important; border:1px solid var(--av-line); border-radius:10px;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .list-search{
|
||||||
|
background:var(--av-panel) !important; border:1px solid var(--av-line) !important;
|
||||||
|
color:var(--av-fg); border-radius:8px;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .list-search::placeholder{ color:var(--av-fg-faint); }
|
||||||
|
body.admin-page-v2 .list-item{
|
||||||
|
background:var(--av-panel) !important; border:1px solid var(--av-line) !important;
|
||||||
|
color:var(--av-fg) !important; border-radius:8px; transition:border-color .12s;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .list-item:hover{ border-color:var(--av-fg-faint) !important; }
|
||||||
|
body.admin-page-v2 .list-item.selected-item{
|
||||||
|
border-color:var(--av-accent) !important; background:var(--av-accent-dim) !important;
|
||||||
|
box-shadow:none !important;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .list-item small{ color:var(--av-fg-faint); }
|
||||||
|
body.admin-page-v2 .user-days{ color:var(--av-fg-faint) !important; }
|
||||||
|
|
||||||
|
/* forms */
|
||||||
|
body.admin-page-v2 input,
|
||||||
|
body.admin-page-v2 button,
|
||||||
|
body.admin-page-v2 textarea,
|
||||||
|
body.admin-page-v2 select{
|
||||||
|
background:var(--av-panel); border:1px solid var(--av-line); color:var(--av-fg);
|
||||||
|
border-radius:8px; font:400 13.5px/1.4 "Ubuntu",sans-serif;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 input::placeholder,
|
||||||
|
body.admin-page-v2 textarea::placeholder{ color:var(--av-fg-faint); }
|
||||||
|
body.admin-page-v2 input:focus,
|
||||||
|
body.admin-page-v2 textarea:focus,
|
||||||
|
body.admin-page-v2 select:focus{ outline:none; border-color:var(--av-accent); }
|
||||||
|
body.admin-page-v2 input[readonly]{ opacity:.6; cursor:default; }
|
||||||
|
body.admin-page-v2 button{
|
||||||
|
background:var(--av-accent); color:var(--av-accent-ink); border:none;
|
||||||
|
font-weight:700; cursor:pointer; padding:8px 16px;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 button:hover{ filter:brightness(1.06); }
|
||||||
|
body.admin-page-v2 .field-col > span{
|
||||||
|
font:600 11.5px/1 "Ubuntu",sans-serif; color:var(--av-fg-faint);
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .field-help{ color:var(--av-fg-faint); }
|
||||||
|
body.admin-page-v2 .actions{ flex-wrap:wrap; }
|
||||||
|
|
||||||
|
/* acl / category grids */
|
||||||
|
body.admin-page-v2 .acl-grid label,
|
||||||
|
body.admin-page-v2 .compact-grid label{
|
||||||
|
display:flex; align-items:center; gap:8px; padding:6px 8px;
|
||||||
|
border:1px solid var(--av-line); border-radius:7px; background:var(--av-panel);
|
||||||
|
font:400 13px/1.3 "Ubuntu",sans-serif; color:var(--av-fg-dim);
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .acl-grid input[type=checkbox],
|
||||||
|
body.admin-page-v2 .compact-grid input[type=checkbox]{ accent-color:var(--av-accent); }
|
||||||
|
body.admin-page-v2 .acl-owner{ color:var(--av-accent) !important; }
|
||||||
|
|
||||||
|
/* tables */
|
||||||
|
body.admin-page-v2 .admin-table{ color:var(--av-fg-dim); }
|
||||||
|
body.admin-page-v2 .admin-table th{
|
||||||
|
color:var(--av-fg-faint); font:700 10.5px/1 "Ubuntu Mono",monospace; letter-spacing:.06em;
|
||||||
|
text-transform:uppercase; border-bottom:1px solid var(--av-line) !important;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .admin-table td{ border-bottom:1px solid var(--av-line) !important; }
|
||||||
|
body.admin-page-v2 .container-table-wrap{
|
||||||
|
background:var(--av-panel) !important; border:1px solid var(--av-line) !important;
|
||||||
|
border-radius:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* summary strip */
|
||||||
|
body.admin-page-v2 .summary-card{
|
||||||
|
background:var(--av-panel) !important; border:1px solid var(--av-line);
|
||||||
|
border-radius:10px;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .summary-label{
|
||||||
|
color:var(--av-fg-faint) !important; font:600 10.5px/1 "Ubuntu Mono",monospace;
|
||||||
|
letter-spacing:.06em; text-transform:uppercase;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .summary-value{
|
||||||
|
color:var(--av-fg) !important; font:700 19px/1 "Ubuntu Mono",monospace; margin-top:6px;
|
||||||
|
font-variant-numeric:tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* health / icon boxes */
|
||||||
|
body.admin-page-v2 .health-box,
|
||||||
|
body.admin-page-v2 .icon-box{
|
||||||
|
background:var(--av-raised) !important; border:1px solid var(--av-line) !important;
|
||||||
|
border-radius:10px;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .service-thumb,
|
||||||
|
body.admin-page-v2 .service-icon-preview{
|
||||||
|
background:var(--av-panel) !important; border:1px solid var(--av-line-soft) !important;
|
||||||
|
border-radius:9px;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .status-ok{ background:var(--av-good) !important; }
|
||||||
|
body.admin-page-v2 .status-degraded{ background:var(--av-warn) !important; }
|
||||||
|
body.admin-page-v2 .status-down{ background:var(--av-bad) !important; }
|
||||||
|
|
||||||
|
/* categories tab rows */
|
||||||
|
body.admin-page-v2 .category-item-row small{ color:var(--av-fg-faint); }
|
||||||
|
body.admin-page-v2 .category-item-row button{
|
||||||
|
background:transparent; border:1px solid var(--av-bad); color:var(--av-bad);
|
||||||
|
font-weight:600; padding:6px 12px;
|
||||||
|
}
|
||||||
|
body.admin-page-v2 .category-item-row button:hover{ background:rgba(255,107,107,.12); }
|
||||||
|
|
||||||
|
body.admin-page-v2 .muted{ color:var(--av-fg-faint) !important; }
|
||||||
|
body.admin-page-v2 hr{ border:none; border-top:1px solid var(--av-line); margin:1.2rem 0; }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="admin-page-v2">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div style="display:flex; align-items:center; gap:0.6rem;">
|
<div style="display:flex; align-items:center; gap:0.6rem;">
|
||||||
<a href="https://4mont.ru"><img src="/static/logo.png?v=4" alt="MONT" class="header-logo" /></a>
|
<a href="https://4mont.ru"><img src="/static/logo.png?v=4" alt="MONT" class="header-logo" /></a>
|
||||||
|
|||||||
@@ -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>
|
||||||
+359
-159
@@ -23,18 +23,153 @@
|
|||||||
</script>
|
</script>
|
||||||
<noscript><div><img src="https://mc.yandex.ru/watch/109119977" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
<noscript><div><img src="https://mc.yandex.ru/watch/109119977" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||||
<!-- /Yandex.Metrika counter -->
|
<!-- /Yandex.Metrika counter -->
|
||||||
</head>
|
<style>
|
||||||
<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}
|
#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}}
|
@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-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-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-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{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-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}
|
.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 %}
|
|
||||||
|
: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">
|
<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>
|
<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-icon">🖥️</div>
|
||||||
@@ -47,176 +182,241 @@
|
|||||||
<div class="mw-footer"><a href="mailto:ruslan@ipcom.su" style="color:inherit;text-decoration:none">Made by Galyaviev</a></div>
|
<div class="mw-footer"><a href="mailto:ruslan@ipcom.su" style="color:inherit;text-decoration:none">Made by Galyaviev</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<header class="header">
|
<div class="dv-shell">
|
||||||
<div class="header-left">
|
<aside class="dv-rail">
|
||||||
<div class="user-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 class="dv-rail-brand"><a href="https://4mont.ru"><img src="/static/logo-color.png?v=1" alt="MONT" /></a></div>
|
||||||
<span class="header-username">{{ (user.first_name + ' ' + user.last_name)|trim or user.username }}</span>
|
<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="header-right">
|
<div>
|
||||||
{% if user.is_admin %}
|
<div class="dv-rail-kicker">Скоро</div>
|
||||||
<a href="/admin" class="header-btn">Администрирование</a>
|
<nav class="dv-rail-nav">
|
||||||
{% endif %}
|
<a class="dv-rail-link" href="/bundles">
|
||||||
<form method="post" action="/logout">
|
<span class="dv-rail-dot"></span>Готовые связки<span class="dv-rail-count">NEW</span>
|
||||||
<button type="submit" class="header-btn header-btn-logout">Выход</button>
|
</a>
|
||||||
</form>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
<div class="dv-rail-foot">SESSION LIMIT: {{ max_active_services }}<br/>IDLE TIMEOUT: {{ idle_timeout_min }} MIN<br/>RESET: 00:00 MSK</div>
|
||||||
<div class="page-logo-wrap">
|
</aside>
|
||||||
<a href="https://4mont.ru"><img src="/static/logo.png?v=4" alt="MONT" class="page-logo" /></a>
|
|
||||||
</div>
|
<main class="dv-main">
|
||||||
<main class="admin-layout">
|
<div class="dv-topbar">
|
||||||
<section class="panel">
|
<div class="dv-who">
|
||||||
<div class="admin-intro">Инфраструктурный полигон MONT</div>
|
<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>
|
||||||
{% if session_notice %}
|
<div>
|
||||||
<div class="session-notice">{{ session_notice }}</div>
|
<div class="dv-who-name">{{ (user.first_name + ' ' + user.last_name)|trim or user.username }}</div>
|
||||||
{% endif %}
|
<div class="dv-who-role">оператор стенда</div>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
{% if categories %}
|
<div class="dv-topbar-actions">
|
||||||
<div class="category-strip">
|
{% if user.is_admin %}
|
||||||
<a class="category-chip {% if not selected_category_slug %}active{% endif %}" href="/">Все сервисы</a>
|
<a class="dv-btn" href="/admin">Администрирование</a>
|
||||||
{% for category in categories %}
|
{% endif %}
|
||||||
<a class="category-chip {% if selected_category_slug == category.slug %}active{% endif %}" href="/?category={{ category.slug }}">{{ category.name }}</a>
|
<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 %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="dv-empty">{% if selected_category_slug %}В этой категории у вас пока нет доступных сервисов.{% else %}Вам пока не назначены сервисы — обратитесь к вашему менеджеру MONT.{% endif %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
<section class="grid service-grid">
|
|
||||||
{% for service in services %}
|
{% if locked_services %}
|
||||||
{% set svc_cats = service_categories.get(service.id, []) %}
|
<section class="dv-section">
|
||||||
<div class="tile-wrap">
|
<div class="dv-section-head"><h2>Доступно по запросу</h2><span class="dv-section-count">{{ '%02d'|format(locked_services|length) }}</span></div>
|
||||||
<a class="tile-link" href="/go/{{ service.slug }}" aria-label="{{ service.name }}"></a>
|
<div class="dv-tile-grid">
|
||||||
<div class="tile">
|
{% for service in locked_services %}
|
||||||
<div class="tile-icon-box">
|
<div class="dv-tile locked">
|
||||||
<img class="tile-icon" src="{{ service.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
<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>
|
||||||
<h3>{{ service.name }}</h3>
|
<div class="dv-tile-desc">{{ service_comment_html.get(service.id, '') }}</div>
|
||||||
<div class="tile-info-area">
|
<button type="button" class="dv-tile-request" data-product="{{ service.name }}">Запросить доступ</button>
|
||||||
{% if service.comment %}
|
<span class="dv-tile-lock" title="Нет доступа">🔒</span>
|
||||||
<div class="tile-comment">{{ service_comment_html.get(service.id, '') }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if svc_cats %}
|
|
||||||
<div class="service-categories">
|
|
||||||
{% for category in svc_cats %}
|
|
||||||
<span class="service-cat-badge">{{ category.name }}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
|
||||||
<div class="tile">
|
|
||||||
{% if selected_category_slug %}
|
|
||||||
Нет сервисов в выбранной категории
|
|
||||||
{% else %}
|
|
||||||
Нет назначенных сервисов
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</section>
|
</section>
|
||||||
<footer class="made-by-wrap"><a class="made-by" href="mailto:ruslan@ipcom.su">Made by Galyaviev</a></footer>
|
{% endif %}
|
||||||
|
|
||||||
|
<footer class="dv-page-footer"><span>MONT PROVING GROUND</span><a href="mailto:ruslan@ipcom.su">Made by Galyaviev</a></footer>
|
||||||
</main>
|
</main>
|
||||||
<style>
|
</div>
|
||||||
#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}
|
<div class="req-modal-overlay" id="req-overlay">
|
||||||
#loading-overlay .lo-spinner{width:52px;height:52px;border:4px solid rgba(220,232,245,.15);
|
<div class="req-modal" id="req-modal-body">
|
||||||
border-top-color:#2a8cd6;border-radius:50%;animation:lo-spin .85s linear infinite}
|
<h3>Запросить доступ</h3>
|
||||||
#loading-overlay .lo-text{color:#a0b8cc;font:600 1rem sans-serif}
|
<p class="sub">Заявка уйдёт администратору полигона. После одобрения продукт появится у вас автоматически — заново логиниться не нужно.</p>
|
||||||
@keyframes lo-spin{to{transform:rotate(360deg)}}
|
<div class="req-error" id="req-error"></div>
|
||||||
</style>
|
<div class="req-list" id="req-list"></div>
|
||||||
<div id="loading-overlay">
|
<textarea class="req-note" id="req-note" placeholder="Комментарий (необязательно) — например, для какого проекта нужен доступ"></textarea>
|
||||||
<div class="lo-spinner"></div>
|
<div class="req-actions">
|
||||||
<div class="lo-text">Ожидайте...</div>
|
<button type="button" class="req-cancel" id="req-cancel">Отмена</button>
|
||||||
|
<button type="button" class="req-submit" id="req-submit">Отправить заявку</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
</div>
|
||||||
(function () {
|
|
||||||
const username = {{ user.username|tojson }};
|
<script>
|
||||||
const key = `rules_ack_${username}`;
|
(function () {
|
||||||
const banner = document.getElementById('rules-banner');
|
function clamp(value, min, max) {
|
||||||
const btn = document.getElementById('rules-ack-btn');
|
return Math.max(min, Math.min(max, value));
|
||||||
if (!banner || !btn) return;
|
}
|
||||||
if (localStorage.getItem(key) === '1') {
|
function currentScreenParams() {
|
||||||
banner.style.display = 'none';
|
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;
|
return;
|
||||||
}
|
}
|
||||||
btn.addEventListener('click', function () {
|
const submitBtn = document.getElementById('req-submit');
|
||||||
localStorage.setItem(key, '1');
|
submitBtn.disabled = true;
|
||||||
banner.style.display = 'none';
|
submitBtn.textContent = 'Отправка...';
|
||||||
});
|
errEl.style.display = 'none';
|
||||||
})();
|
try {
|
||||||
|
const res = await fetch('/api/request-more-access', {
|
||||||
(function () {
|
method: 'POST',
|
||||||
function clamp(value, min, max) {
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
|
||||||
return Math.max(min, Math.min(max, value));
|
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 = 'Отправить заявку';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function currentScreenParams() {
|
document.getElementById('req-submit').addEventListener('click', submitRequest);
|
||||||
const screenWidth =
|
})();
|
||||||
window.screen && Number.isFinite(window.screen.width) && window.screen.width > 0
|
</script>
|
||||||
? 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>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+235
-215
@@ -41,10 +41,6 @@
|
|||||||
<link rel="icon" type="image/png" sizes="192x192" href="/static/favicon_192.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="apple-touch-icon" sizes="192x192" href="/static/favicon_192.png" />
|
||||||
<link rel="manifest" href="/static/manifest.json" />
|
<link rel="manifest" href="/static/manifest.json" />
|
||||||
<style>
|
|
||||||
body { background: #070f1c; overflow: hidden; height: 100vh; }
|
|
||||||
@media (max-width: 820px) { body { overflow: auto; height: auto; } }
|
|
||||||
</style>
|
|
||||||
<!-- Yandex.Metrika counter -->
|
<!-- Yandex.Metrika counter -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
(function(m,e,t,r,i,k,a){
|
(function(m,e,t,r,i,k,a){
|
||||||
@@ -57,60 +53,251 @@
|
|||||||
</script>
|
</script>
|
||||||
<noscript><div><img src="https://mc.yandex.ru/watch/109119977" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
<noscript><div><img src="https://mc.yandex.ru/watch/109119977" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||||
<!-- /Yandex.Metrika counter -->
|
<!-- /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-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>
|
</head>
|
||||||
<body>
|
<body class="login-page">
|
||||||
<div class="login-wrap">
|
<div class="lg2-wrap">
|
||||||
<aside class="login-left">
|
<aside class="lg2-pitch">
|
||||||
<div class="login-left-glow login-left-glow-top"></div>
|
<div class="lg2-brand">
|
||||||
<div class="login-left-glow login-left-glow-bottom"></div>
|
<a href="#" onclick="window.open(location.hostname==='stand.mont.ru'?'https://www.mont.ru':'https://4mont.ru','_blank');return false;">
|
||||||
<div class="login-left-inner">
|
<img src="/static/logo-color.png?v=1" alt="MONT" />
|
||||||
<a href="#" onclick="window.open(location.hostname==='stand.mont.ru'?'https://www.mont.ru':'https://4mont.ru','_blank');return false;"><img src="/static/logo.png?v=4" alt="MONT" class="login-corner-logo" /></a>
|
</a>
|
||||||
<h1 class="login-left-title">Инфраструктурный<br>полигон MONT</h1>
|
<span class="lg2-brand-mark">Инфраструктурный полигон</span>
|
||||||
<p class="login-left-desc">Платформа для демонстрации и пилотного тестирования российского ПО. Партнеры MONT и их заказчики получают браузерный доступ к рабочим стендам с отечественными ОС, платформами виртуализации, СРК и другими решениями — без установки и настройки.</p>
|
|
||||||
<ul class="login-features">
|
|
||||||
<li class="login-feature">
|
|
||||||
<span class="login-feature-icon">🖥</span>
|
|
||||||
<span>Доступ к рабочим столам ОС</span>
|
|
||||||
</li>
|
|
||||||
<li class="login-feature">
|
|
||||||
<span class="login-feature-icon">🌐</span>
|
|
||||||
<span>Веб-интерфейсы сервисов</span>
|
|
||||||
</li>
|
|
||||||
<li class="login-feature">
|
|
||||||
<span class="login-feature-icon">⚡</span>
|
|
||||||
<span>Доступ в один клик</span>
|
|
||||||
</li>
|
|
||||||
<li class="login-feature">
|
|
||||||
<span class="login-feature-icon">🔒</span>
|
|
||||||
<span>Защищённый контур</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<button type="button" class="login-distrib-btn" onclick="window.open(location.hostname==='stand.mont.ru'?'https://maps.mont.ru':'https://maps.4mont.ru','_blank')">Продукты нашей дистрибуции</button>
|
|
||||||
</div>
|
</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 %}
|
||||||
|
|
||||||
|
<div class="lg2-pitch-footer">MONT PROVING GROUND</div>
|
||||||
</aside>
|
</aside>
|
||||||
<main class="login-right">
|
|
||||||
<div class="login-right-inner">
|
<main class="lg2-auth">
|
||||||
<div class="login-form-title">Вход в систему</div>
|
<div class="lg2-auth-card">
|
||||||
<div class="login-form-subtitle">Инфраструктурный полигон</div>
|
<div class="lg2-auth-title">Вход в систему</div>
|
||||||
{% if session_notice %}<div class="session-notice lp-session-notice">{{ session_notice }}</div>{% endif %}
|
<div class="lg2-auth-sub">Инфраструктурный полигон</div>
|
||||||
{% if login_error %}<div class="auth-error lp-auth-error">{{ login_error }}</div>{% endif %}
|
<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>
|
||||||
<form method="post" action="/login" class="login-form">
|
{% 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 }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||||
<div class="login-field">
|
<div class="lg2-field">
|
||||||
<label>Логин</label>
|
<label>Логин</label>
|
||||||
<input type="text" name="username" placeholder="Введите логин" required autocomplete="username" />
|
<input type="text" name="username" placeholder="Введите логин" required autocomplete="username" />
|
||||||
</div>
|
</div>
|
||||||
<div class="login-field">
|
<div class="lg2-field">
|
||||||
<label>Пароль</label>
|
<label>Пароль</label>
|
||||||
<input type="password" name="password" placeholder="Введите пароль" required autocomplete="current-password" />
|
<input type="password" name="password" placeholder="Введите пароль" required autocomplete="current-password" />
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="login-submit">Войти</button>
|
<button type="submit" class="lg2-submit">Войти</button>
|
||||||
</form>
|
</form>
|
||||||
<button type="button" class="login-request-btn" id="btn-request-access" data-open-access-modal="1">Запросить доступ</button>
|
<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>
|
</div>
|
||||||
<footer class="login-footer">
|
|
||||||
<a href="mailto:ruslan@ipcom.su" class="login-footer-link">Made by Galyaviev</a>
|
|
||||||
</footer>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -356,179 +543,12 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Contact Ruslan Modal -->
|
|
||||||
<div id="contact-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" id="cm-body">
|
|
||||||
<div class="access-field">
|
|
||||||
<label>Ваше имя <span class="req">*</span></label>
|
|
||||||
<input id="cm-name" type="text" placeholder="Иван Иванов" />
|
|
||||||
</div>
|
|
||||||
<div class="access-field">
|
|
||||||
<label>Email <span class="req">*</span></label>
|
|
||||||
<input id="cm-email" type="email" placeholder="ivan@company.ru" />
|
|
||||||
</div>
|
|
||||||
<div class="access-field">
|
|
||||||
<label>Телефон <span class="req">*</span></label>
|
|
||||||
<input id="cm-phone" type="tel" placeholder="+7 (999) 000-00-00" />
|
|
||||||
</div>
|
|
||||||
<div class="access-field">
|
|
||||||
<label>Сообщение <span class="req">*</span></label>
|
|
||||||
<textarea id="cm-text" class="access-textarea" placeholder="Ваш вопрос или предложение..." rows="4"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="access-consent-field">
|
|
||||||
<label class="access-consent-label">
|
|
||||||
<input type="checkbox" id="cm-consent" />
|
|
||||||
<span>Согласен на <a href="/privacy" target="_blank" class="access-consent-link">обработку персональных данных</a></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div id="cm-error" class="access-modal-error" style="display:none"></div>
|
|
||||||
</div>
|
|
||||||
<div class="access-modal-footer" id="cm-footer">
|
|
||||||
<button type="button" class="access-btn-cancel" id="cm-cancel">Отмена</button>
|
|
||||||
<button type="button" class="access-btn-submit" id="cm-submit">Отправить</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
const overlay = document.getElementById('contact-modal');
|
|
||||||
const btnCancel = document.getElementById('cm-cancel');
|
|
||||||
const btnSubmit = document.getElementById('cm-submit');
|
|
||||||
const errEl = document.getElementById('cm-error');
|
|
||||||
|
|
||||||
function resetContactForm() {
|
|
||||||
if (!document.getElementById('cm-name')) {
|
|
||||||
document.getElementById('cm-body').innerHTML = `
|
|
||||||
<div class="access-field"><label>Ваше имя <span class="req">*</span></label><input id="cm-name" type="text" placeholder="Иван Иванов" /></div>
|
|
||||||
<div class="access-field"><label>Email <span class="req">*</span></label><input id="cm-email" type="email" placeholder="ivan@company.ru" /></div>
|
|
||||||
<div class="access-field"><label>Телефон <span class="req">*</span></label><input id="cm-phone" type="tel" placeholder="+7 (999) 000-00-00" /></div>
|
|
||||||
<div class="access-field"><label>Сообщение <span class="req">*</span></label><textarea id="cm-text" class="access-textarea" placeholder="Ваш вопрос или предложение..." rows="4"></textarea></div>
|
|
||||||
<div class="access-consent-field"><label class="access-consent-label"><input type="checkbox" id="cm-consent" /><span>Согласен на <a href="/privacy" target="_blank" class="access-consent-link">обработку персональных данных</a></span></label></div>
|
|
||||||
<div id="cm-error" class="access-modal-error" style="display:none"></div>`;
|
|
||||||
document.getElementById('cm-footer').innerHTML = `<button type="button" class="access-btn-cancel" id="cm-cancel">Отмена</button><button type="button" class="access-btn-submit" id="cm-submit">Отправить</button>`;
|
|
||||||
document.getElementById('cm-cancel').addEventListener('click', closeContact);
|
|
||||||
document.getElementById('cm-submit').addEventListener('click', submitContact);
|
|
||||||
document.querySelectorAll('#cm-name,#cm-email,#cm-phone,#cm-text').forEach(function(el){ el.addEventListener('input', function(){ el.classList.remove('am-invalid'); }); });
|
|
||||||
} else {
|
|
||||||
['cm-name','cm-email','cm-phone','cm-text'].forEach(function(id){ var el=document.getElementById(id); if(el){el.value='';el.classList.remove('am-invalid');} });
|
|
||||||
var err=document.getElementById('cm-error'); if(err) err.style.display='none';
|
|
||||||
var btn=document.getElementById('cm-submit'); if(btn){btn.disabled=false;btn.textContent='Отправить';}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openContact() {
|
|
||||||
resetContactForm();
|
|
||||||
overlay.style.display = 'flex';
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeContact() {
|
|
||||||
overlay.style.display = 'none';
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
errEl.style.display = 'none';
|
|
||||||
document.querySelectorAll('#contact-modal .am-invalid').forEach(el => el.classList.remove('am-invalid'));
|
|
||||||
}
|
|
||||||
window._closeContactModal = closeContact;
|
|
||||||
|
|
||||||
async function submitContact() {
|
|
||||||
const nameEl = document.getElementById('cm-name');
|
|
||||||
const emailEl = document.getElementById('cm-email');
|
|
||||||
const phoneEl = document.getElementById('cm-phone');
|
|
||||||
const textEl = document.getElementById('cm-text');
|
|
||||||
const submitBtn = document.getElementById('cm-submit');
|
|
||||||
const errorEl = document.getElementById('cm-error');
|
|
||||||
|
|
||||||
const name = nameEl ? nameEl.value.trim() : '';
|
|
||||||
const email = emailEl ? emailEl.value.trim() : '';
|
|
||||||
const phone = phoneEl ? phoneEl.value.trim() : '';
|
|
||||||
const text = textEl ? textEl.value.trim() : '';
|
|
||||||
|
|
||||||
const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
||||||
const phoneRe = /^[\+\d][\d\s\-\(\)]{6,18}$/;
|
|
||||||
|
|
||||||
const consentEl = document.getElementById('cm-consent');
|
|
||||||
const fields = [
|
|
||||||
{ el: nameEl, check: () => !!name, msg: 'Введите имя' },
|
|
||||||
{ el: emailEl, check: () => emailRe.test(email), msg: 'Введите корректный email' },
|
|
||||||
{ el: phoneEl, check: () => phoneRe.test(phone), msg: 'Введите корректный номер телефона' },
|
|
||||||
{ el: textEl, check: () => !!text, 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('#contact-modal .access-consent-field');
|
|
||||||
if (cf) cf.classList.add('am-invalid-consent');
|
|
||||||
} else {
|
|
||||||
const cf = document.querySelector('#contact-modal .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/contact', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({name, email, phone, text}),
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
const d = await res.json().catch(() => ({}));
|
|
||||||
throw new Error(d.detail || 'Ошибка отправки');
|
|
||||||
}
|
|
||||||
document.getElementById('cm-body').innerHTML =
|
|
||||||
'<div class="am-success-msg">' +
|
|
||||||
'<div class="am-success-icon">✓</div>' +
|
|
||||||
'<div class="am-success-title">Отправлено</div>' +
|
|
||||||
'<div class="am-success-sub">Постараюсь ответить в ближайшее время</div>' +
|
|
||||||
'</div>';
|
|
||||||
document.getElementById('cm-footer').innerHTML =
|
|
||||||
'<button type="button" class="access-btn-cancel" onclick="window._closeContactModal()">Закрыть</button>';
|
|
||||||
} catch(e) {
|
|
||||||
const eb = document.getElementById('cm-error');
|
|
||||||
if (eb) { eb.textContent = e.message || 'Ошибка отправки, попробуйте позже'; eb.style.display = 'block'; }
|
|
||||||
const sb = document.getElementById('cm-submit');
|
|
||||||
if (sb) { sb.disabled = false; sb.textContent = 'Отправить'; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll('#cm-name,#cm-email,#cm-phone,#cm-text').forEach(el => {
|
|
||||||
el.addEventListener('input', () => el.classList.remove('am-invalid'));
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btn-contact-ruslan').addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
openContact();
|
|
||||||
});
|
|
||||||
btnCancel.addEventListener('click', closeContact);
|
|
||||||
btnSubmit.addEventListener('click', submitContact);
|
|
||||||
overlay.addEventListener('click', function(e) { if (e.target === overlay) closeContact(); });
|
|
||||||
document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && overlay.style.display !== 'none') closeContact(); });
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Cookie Banner -->
|
<!-- 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;">
|
<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;">
|
<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:#5b9bd5;text-decoration:underline;">Политикой конфиденциальности</a>.
|
Мы используем файлы cookie, чтобы сделать работу с сайтом удобнее. Нажмите «Принять», чтобы согласиться с использованием файлов cookie в соответствии с <a href="https://www.mont.ru/ru-ru/confidential" target="_blank" style="color:#ff9d3d;text-decoration:underline;">Политикой конфиденциальности</a>.
|
||||||
</p>
|
</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,#1a5db5,#2d8cf0);color:#fff;border:none;border-radius:8px;font-size:0.88rem;font-weight:600;cursor:pointer;white-space:nowrap;">Принять</button>
|
<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>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
if (!localStorage.getItem('cookie_accepted')) {
|
if (!localStorage.getItem('cookie_accepted')) {
|
||||||
|
|||||||
@@ -0,0 +1,600 @@
|
|||||||
|
{% set _scheme = request.headers.get('x-forwarded-proto', request.url.scheme) %}
|
||||||
|
{% set base_url = _scheme + '://' + request.url.netloc + '/' %}
|
||||||
|
{% set cat_names = categories|map(attribute='name')|list %}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>{{ service.name }} — тестовый доступ | Инфраструктурный полигон MONT</title>
|
||||||
|
<meta name="description" content="{{ meta_description }}" />
|
||||||
|
<meta name="robots" content="index, follow" />
|
||||||
|
<link rel="canonical" href="{{ canonical_url }}" />
|
||||||
|
<meta property="og:type" content="product" />
|
||||||
|
<meta property="og:url" content="{{ canonical_url }}" />
|
||||||
|
<meta property="og:title" content="{{ service.name }} — Инфраструктурный полигон MONT" />
|
||||||
|
<meta property="og:description" content="{{ meta_description }}" />
|
||||||
|
<meta property="og:image" content="{{ base_url[:-1] }}{{ service.icon_path or '/static/service-placeholder.svg' }}" />
|
||||||
|
<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="{{ service.name }}" />
|
||||||
|
<meta name="twitter:description" content="{{ meta_description }}" />
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "SoftwareApplication",
|
||||||
|
"name": {{ service.name|tojson }},
|
||||||
|
"url": {{ canonical_url|tojson }},
|
||||||
|
"description": {{ meta_description|tojson }},
|
||||||
|
"applicationCategory": {{ (cat_names[0] if cat_names else "BusinessApplication")|tojson }},
|
||||||
|
"offers": {
|
||||||
|
"@type": "Offer",
|
||||||
|
"price": "0",
|
||||||
|
"priceCurrency": "RUB",
|
||||||
|
"description": "Бесплатный тестовый доступ на инфраструктурном полигоне MONT"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"@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:#0a0e14; --lg2-panel:#12181f; --lg2-raised:#1b232d;
|
||||||
|
--lg2-line:#232b36; --lg2-line-soft:#1a212a;
|
||||||
|
--lg2-fg:#e7ecf2; --lg2-fg-dim:#8391a3; --lg2-fg-faint:#56626f;
|
||||||
|
--lg2-accent:#ff9d3d; --lg2-accent-dim:rgba(255,157,61,.14); --lg2-accent-ink:#180d02;
|
||||||
|
--lg2-bad:#ff6b6b; --lg2-bad-dim:rgba(255,107,107,.12);
|
||||||
|
--lg2-info:#7fc7ff; --lg2-info-dim:rgba(127,199,255,.10);
|
||||||
|
--lg2-good:#5fd68a;
|
||||||
|
}
|
||||||
|
:root[data-theme="light"]{
|
||||||
|
--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;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme:light){
|
||||||
|
:root:not([data-theme="dark"]){
|
||||||
|
--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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*{ box-sizing:border-box; }
|
||||||
|
html{ background:var(--lg2-bg); }
|
||||||
|
body.product-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;
|
||||||
|
}
|
||||||
|
a{ color:inherit; }
|
||||||
|
|
||||||
|
.pg-header{
|
||||||
|
display:flex; align-items:center; justify-content:space-between; gap:16px;
|
||||||
|
padding:18px clamp(20px,4vw,40px); border-bottom:1px solid var(--lg2-line); background:var(--lg2-panel);
|
||||||
|
flex-wrap:wrap;
|
||||||
|
}
|
||||||
|
.pg-brand{ display:flex; align-items:center; gap:10px; text-decoration:none; }
|
||||||
|
.pg-brand img{ height:28px; display:block; }
|
||||||
|
.pg-brand span{ font:700 12px/1 "Ubuntu Mono",monospace; color:var(--lg2-fg-dim); letter-spacing:.06em; text-transform:uppercase; }
|
||||||
|
.pg-header-actions{ display:flex; gap:10px; }
|
||||||
|
.pg-btn{
|
||||||
|
display:inline-flex; align-items:center; gap:8px; border-radius:8px; text-decoration:none; cursor:pointer;
|
||||||
|
font:600 13px/1 "Ubuntu",sans-serif; padding:10px 18px; border:1px solid var(--lg2-line); white-space:nowrap;
|
||||||
|
}
|
||||||
|
.pg-btn-ghost{ background:transparent; color:var(--lg2-fg-dim); }
|
||||||
|
.pg-btn-ghost:hover{ border-color:var(--lg2-fg-faint); color:var(--lg2-fg); }
|
||||||
|
.pg-btn-accent{ background:var(--lg2-accent); color:var(--lg2-accent-ink); border-color:var(--lg2-accent); font-weight:700; }
|
||||||
|
.pg-btn-accent:hover{ filter:brightness(1.06); }
|
||||||
|
|
||||||
|
.pg-crumbs{
|
||||||
|
max-width:1180px; margin:22px auto 0; padding:0 clamp(20px,4vw,40px);
|
||||||
|
font:600 11px/1 "Ubuntu Mono",monospace; letter-spacing:.08em; text-transform:uppercase;
|
||||||
|
color:var(--lg2-fg-faint); display:flex; gap:8px; align-items:center; flex-wrap:wrap;
|
||||||
|
}
|
||||||
|
.pg-crumbs a{ text-decoration:none; color:var(--lg2-fg-faint); }
|
||||||
|
.pg-crumbs a:hover{ color:var(--lg2-accent); }
|
||||||
|
.pg-crumbs .sep{ opacity:.5; }
|
||||||
|
.pg-crumbs .current{ color:var(--lg2-fg-dim); }
|
||||||
|
|
||||||
|
.pg-hero{
|
||||||
|
max-width:1180px; margin:20px auto 0; padding:0 clamp(20px,4vw,40px);
|
||||||
|
display:grid; grid-template-columns:112px minmax(0,1fr); gap:26px; align-items:start;
|
||||||
|
}
|
||||||
|
@media (max-width:640px){ .pg-hero{ grid-template-columns:1fr; } }
|
||||||
|
.pg-plate{
|
||||||
|
width:112px; height:112px; border-radius:18px; background:var(--lg2-raised);
|
||||||
|
border:1px solid var(--lg2-line); display:flex; align-items:center; justify-content:center; flex:none;
|
||||||
|
}
|
||||||
|
.pg-plate img{ width:74px; height:74px; object-fit:contain; }
|
||||||
|
.pg-eyebrow{
|
||||||
|
font:700 11px/1 "Ubuntu Mono",monospace; letter-spacing:.1em; text-transform:uppercase;
|
||||||
|
color:var(--lg2-accent); margin:0 0 10px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;
|
||||||
|
}
|
||||||
|
.pg-eyebrow::before{ content:""; width:6px; height:6px; border-radius:50%; background:var(--lg2-good); box-shadow:0 0 0 3px var(--lg2-accent-dim); flex:none; }
|
||||||
|
.pg-hero h1{ font:800 clamp(24px,3.4vw,36px)/1.15 "Ubuntu",sans-serif; margin:0 0 12px; text-wrap:balance; overflow-wrap:break-word; }
|
||||||
|
.pg-lede{ font:400 16px/1.6 "Ubuntu",sans-serif; color:var(--lg2-fg-dim); max-width:64ch; margin:0 0 20px; }
|
||||||
|
.pg-actions{ display:flex; gap:12px; flex-wrap:wrap; align-items:center; }
|
||||||
|
.pg-note{ font:400 12.5px/1.4 "Ubuntu",sans-serif; color:var(--lg2-fg-faint); }
|
||||||
|
.pg-prompt{
|
||||||
|
margin-top:22px; background:var(--lg2-panel); border:1px solid var(--lg2-line); border-radius:9px;
|
||||||
|
padding:12px 16px; font:400 13px/1.5 "Ubuntu Mono",monospace; color:var(--lg2-fg-dim); max-width:560px;
|
||||||
|
overflow-wrap:break-word;
|
||||||
|
}
|
||||||
|
.pg-prompt .p1{ color:var(--lg2-accent); }
|
||||||
|
.pg-prompt .p2{ color:var(--lg2-fg-faint); }
|
||||||
|
.pg-cursor{ display:inline-block; width:7px; height:14px; background:var(--lg2-accent); margin-left:2px; vertical-align:-2px; animation:pg-blink 1.1s step-end infinite; }
|
||||||
|
@keyframes pg-blink{ 50%{ opacity:0; } }
|
||||||
|
@media (prefers-reduced-motion: reduce){ .pg-cursor{ animation:none; } }
|
||||||
|
|
||||||
|
.pg-layout{
|
||||||
|
max-width:1180px; margin:44px auto 0; padding:0 clamp(20px,4vw,40px) 60px;
|
||||||
|
display:grid; grid-template-columns:minmax(0,1fr) 280px; gap:28px;
|
||||||
|
}
|
||||||
|
@media (max-width:900px){ .pg-layout{ grid-template-columns:minmax(0,1fr); } }
|
||||||
|
|
||||||
|
.pg-card{
|
||||||
|
background:var(--lg2-panel); border:1px solid var(--lg2-line); border-radius:12px;
|
||||||
|
padding:26px 28px; margin-bottom:20px;
|
||||||
|
}
|
||||||
|
.pg-card > h2:first-child{ margin-top:0; }
|
||||||
|
.pg-desc h2{
|
||||||
|
font:700 13px/1 "Ubuntu Mono",monospace; letter-spacing:.08em; text-transform:uppercase;
|
||||||
|
color:var(--lg2-fg-faint); margin:28px 0 14px; padding-top:20px; border-top:1px solid var(--lg2-line-soft);
|
||||||
|
}
|
||||||
|
.pg-desc h2:first-child{ margin-top:0; padding-top:0; border-top:none; }
|
||||||
|
.pg-desc p{ margin:0 0 14px; color:var(--lg2-fg-dim); font-size:15px; line-height:1.65; }
|
||||||
|
.pg-desc p:last-child{ margin-bottom:0; }
|
||||||
|
.pg-desc strong{ color:var(--lg2-fg); }
|
||||||
|
.pg-desc ul{ list-style:none; margin:0 0 14px; padding:0; display:grid; gap:9px; }
|
||||||
|
.pg-desc ul li{ display:flex; gap:10px; align-items:flex-start; font-size:14.5px; color:var(--lg2-fg-dim); line-height:1.55; }
|
||||||
|
.pg-desc ul li::before{
|
||||||
|
content:""; flex:none; width:7px; height:7px; border-radius:2px; background:var(--lg2-accent);
|
||||||
|
margin-top:7px; transform:rotate(45deg);
|
||||||
|
}
|
||||||
|
.pg-desc hr{ border:none; border-top:1px solid var(--lg2-line-soft); margin:22px 0; }
|
||||||
|
|
||||||
|
.pg-side{
|
||||||
|
background:var(--lg2-panel); border:1px solid var(--lg2-line); border-radius:12px;
|
||||||
|
padding:22px; position:sticky; top:20px; align-self:start;
|
||||||
|
}
|
||||||
|
.pg-side h3{
|
||||||
|
font:700 11px/1 "Ubuntu Mono",monospace; letter-spacing:.08em; text-transform:uppercase;
|
||||||
|
color:var(--lg2-fg-faint); margin:0 0 14px;
|
||||||
|
}
|
||||||
|
.pg-side-row{ display:flex; justify-content:space-between; gap:10px; padding:9px 0; border-bottom:1px solid var(--lg2-line-soft); font-size:13.5px; }
|
||||||
|
.pg-side-row:last-of-type{ border-bottom:none; }
|
||||||
|
.pg-side-row span:first-child{ color:var(--lg2-fg-faint); }
|
||||||
|
.pg-side-row span:last-child{ color:var(--lg2-fg); font-weight:600; text-align:right; }
|
||||||
|
.pg-side-cta{ width:100%; margin-top:16px; justify-content:center; }
|
||||||
|
|
||||||
|
.pg-catalog{ max-width:1180px; margin:0 auto 60px; padding:0 clamp(20px,4vw,40px); }
|
||||||
|
.pg-catalog-head{ display:flex; align-items:baseline; justify-content:space-between; margin-bottom:16px; flex-wrap:wrap; gap:8px; }
|
||||||
|
.pg-catalog-head h2{ font:700 18px/1 "Ubuntu",sans-serif; margin:0; color:var(--lg2-fg); }
|
||||||
|
.pg-catalog-head a{ font:600 13px/1 "Ubuntu",sans-serif; color:var(--lg2-accent); text-decoration:none; }
|
||||||
|
.pg-catalog-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(210px,1fr)); gap:10px; }
|
||||||
|
.pg-chip{
|
||||||
|
display:flex; align-items:center; gap:10px; background:var(--lg2-panel); border:1px solid var(--lg2-line);
|
||||||
|
border-radius:9px; padding:10px 12px; text-decoration:none; color:var(--lg2-fg-dim);
|
||||||
|
font:600 13.5px/1.25 "Ubuntu",sans-serif; transition:border-color .12s;
|
||||||
|
}
|
||||||
|
.pg-chip:hover{ border-color:var(--lg2-accent); color:var(--lg2-fg); }
|
||||||
|
.pg-chip img{ width:22px; height:22px; object-fit:contain; flex:none; }
|
||||||
|
|
||||||
|
.pg-footer{
|
||||||
|
border-top:1px solid var(--lg2-line); padding:20px clamp(20px,4vw,40px); text-align:center;
|
||||||
|
font:400 12.5px/1 "Ubuntu",sans-serif; color:var(--lg2-fg-faint);
|
||||||
|
}
|
||||||
|
.pg-footer a{ color:var(--lg2-fg-faint); text-decoration:none; }
|
||||||
|
|
||||||
|
/* Re-skin the shared access-modal component (same as login.html). */
|
||||||
|
.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; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="product-page">
|
||||||
|
|
||||||
|
<header class="pg-header">
|
||||||
|
<a class="pg-brand" href="/">
|
||||||
|
<img src="/static/logo-color.png" alt="MONT" />
|
||||||
|
<span>Инфраструктурный полигон</span>
|
||||||
|
</a>
|
||||||
|
<div class="pg-header-actions">
|
||||||
|
<a class="pg-btn pg-btn-ghost" href="/#products">Все продукты</a>
|
||||||
|
<a class="pg-btn pg-btn-accent" href="/">Войти в полигон</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="pg-crumbs">
|
||||||
|
<a href="/">Полигон</a><span class="sep">/</span>
|
||||||
|
<span class="current">{{ service.name }}</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="pg-hero">
|
||||||
|
<div class="pg-plate">
|
||||||
|
<img src="{{ service.icon_path or '/static/service-placeholder.svg' }}" alt="{{ service.name }}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="pg-eyebrow">Доступен на полигоне{% if cat_names %} · {{ cat_names|join(', ') }}{% endif %}</div>
|
||||||
|
<h1>{{ service.name }}</h1>
|
||||||
|
<p class="pg-lede">{{ meta_description }}</p>
|
||||||
|
<div class="pg-actions">
|
||||||
|
<button class="pg-btn pg-btn-accent" type="button" data-open-access-modal="1" data-preselect="{{ service.name }}">Запросить тестовый доступ →</button>
|
||||||
|
<span class="pg-note">Доступ откроется в течение рабочего дня, бесплатно</span>
|
||||||
|
</div>
|
||||||
|
<div class="pg-prompt">
|
||||||
|
<span class="p2">mont@polygon:~/catalog$</span> <span class="p1">./access.sh --product {{ service.slug }}</span><span class="pg-cursor"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pg-layout">
|
||||||
|
<div>
|
||||||
|
<div class="pg-card pg-desc">
|
||||||
|
{% if description_html %}
|
||||||
|
{{ description_html }}
|
||||||
|
{% else %}
|
||||||
|
<p>{{ service.name }} доступен для тестирования на инфраструктурном полигоне MONT — оставьте заявку, и мы откроем доступ к рабочему стенду.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside>
|
||||||
|
<div class="pg-side">
|
||||||
|
<h3>Карточка доступа</h3>
|
||||||
|
{% if cat_names %}
|
||||||
|
<div class="pg-side-row"><span>Категория</span><span>{{ cat_names|join(', ') }}</span></div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="pg-side-row"><span>Тип сессии</span><span>{{ service.type.value }}</span></div>
|
||||||
|
<button class="pg-btn pg-btn-accent pg-side-cta" type="button" data-open-access-modal="1" data-preselect="{{ service.name }}">Запросить доступ</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if other_services %}
|
||||||
|
<div class="pg-catalog">
|
||||||
|
<div class="pg-catalog-head">
|
||||||
|
<h2>Другие продукты полигона</h2>
|
||||||
|
<a href="/#products">Смотреть весь каталог →</a>
|
||||||
|
</div>
|
||||||
|
<div class="pg-catalog-grid">
|
||||||
|
{% for svc in other_services %}
|
||||||
|
<a class="pg-chip" href="/product/{{ svc.slug }}">
|
||||||
|
<img src="{{ svc.icon_path or '/static/service-placeholder.svg' }}" alt="" />
|
||||||
|
{{ svc.name }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<footer class="pg-footer">
|
||||||
|
<a href="mailto:ruslan@ipcom.su">Made by Galyaviev</a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<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;
|
||||||
|
let pendingPreselect = '';
|
||||||
|
|
||||||
|
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(preselect) {
|
||||||
|
pendingPreselect = preselect || '';
|
||||||
|
resetAccessForm();
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
if (!productsLoaded) loadProducts();
|
||||||
|
else applyPreselect();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPreselect() {
|
||||||
|
if (!pendingPreselect) return;
|
||||||
|
const cb = document.querySelector('#am-products input[type=checkbox][value="' + pendingPreselect.replace(/"/g,'\\"') + '"]');
|
||||||
|
if (cb) cb.checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
applyPreselect();
|
||||||
|
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; });
|
||||||
|
});
|
||||||
|
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 = 'Запросить доступ'; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('#am-name,#am-company,#am-email,#am-phone').forEach(el => {
|
||||||
|
el.addEventListener('input', () => el.classList.remove('am-invalid'));
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-open-access-modal]').forEach(el => {
|
||||||
|
el.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
openModal(el.getAttribute('data-preselect') || '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import contextvars
|
import contextvars
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -86,6 +87,26 @@ def format_service_comment(raw_text: str) -> Markup:
|
|||||||
return Markup(_md(raw))
|
return Markup(_md(raw))
|
||||||
|
|
||||||
|
|
||||||
|
def format_seo_description(raw_text: str) -> Markup:
|
||||||
|
"""Render a service's long-form SEO description (public /product/{slug}
|
||||||
|
page) to HTML. Unlike format_service_comment() (short dashboard card
|
||||||
|
text), this source text may carry its own leading '# Product Name'
|
||||||
|
title line - the page template already renders an <h1> with the name,
|
||||||
|
so a duplicate <h1> here would hurt SEO. We drop that leading title
|
||||||
|
line if present, and downgrade any other single-# headings (section
|
||||||
|
headers like '# Возможности') to ## so the page never emits more than
|
||||||
|
one <h1>."""
|
||||||
|
raw = (raw_text or "").replace("\r\n", "\n").replace("\r", "\n").strip()
|
||||||
|
if not raw:
|
||||||
|
return Markup("")
|
||||||
|
lines = raw.split("\n")
|
||||||
|
if lines and re.match(r"^#\s+", lines[0]):
|
||||||
|
lines = lines[1:]
|
||||||
|
body = "\n".join(lines).strip()
|
||||||
|
body = re.sub(r"(?m)^#\s+", "## ", body)
|
||||||
|
return Markup(_md(body))
|
||||||
|
|
||||||
|
|
||||||
def parse_rdp_target(target: str) -> dict:
|
def parse_rdp_target(target: str) -> dict:
|
||||||
raw = (target or "").strip()
|
raw = (target or "").strip()
|
||||||
if not raw:
|
if not raw:
|
||||||
|
|||||||
@@ -180,8 +180,8 @@ cat > /opt/portal/index.html <<HTML
|
|||||||
|
|
||||||
const enableHeartbeat = "${ENABLE_HEARTBEAT}" === "1";
|
const enableHeartbeat = "${ENABLE_HEARTBEAT}" === "1";
|
||||||
const sid = new URLSearchParams(location.search).get('sid');
|
const sid = new URLSearchParams(location.search).get('sid');
|
||||||
const TOUCH_PATH = sid ? `/api/sessions/${sid}/touch` : '${TOUCH_PATH}';
|
const TOUCH_PATH = sid ? \`/api/sessions/\${sid}/touch\` : '${TOUCH_PATH}';
|
||||||
const CLOSE_PATH = sid ? `/api/sessions/${sid}/close` : TOUCH_PATH.replace(/\/touch$/, '/close');
|
const CLOSE_PATH = sid ? \`/api/sessions/\${sid}/close\` : TOUCH_PATH.replace(/\/touch$/, '/close');
|
||||||
const SESSION_CLOSED_URL = '/?session_closed=idle';
|
const SESSION_CLOSED_URL = '/?session_closed=idle';
|
||||||
|
|
||||||
function goSessionClosed(reason) {
|
function goSessionClosed(reason) {
|
||||||
|
|||||||
Reference in New Issue
Block a user