Animate equipment artwork and publish optimized motion release, agent=codex
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
|
||||
Здесь сохранены исходники, оригиналы изображений и готовый каталог `public/`,
|
||||
который опубликован на Beget. Главная связывает 12 карточек с профильными статьями;
|
||||
есть 3 каталога, адаптивные изображения, лёгкие схемы работы оборудования и SEO-разметка.
|
||||
есть 3 каталога, адаптивные изображения, анимированные маршруты сигнала на карточках,
|
||||
лёгкие схемы работы оборудования и SEO-разметка.
|
||||
Состав сборки, проверки, доступ к хостингу без секретов и порядок отката — в [SITE-BUILD.md](SITE-BUILD.md).
|
||||
|
||||
Актуальные тарифы: «Старт» — 9 000 ₽/мес., «Бизнес» — 21 000 ₽/мес.,
|
||||
@@ -12,6 +13,8 @@
|
||||
|
||||
Исходные генерации, варианты, промпты и готовый выпуск дополнительно сохранены в папке Яндекс Диска `servaki.online/лендинги/servaki.online site/2026-09-28-landing-generations/`. Контрольные суммы 251 файла — в [архивном манифесте](archive-manifests/2026-09-28-generations.json). Для следующих выпусков действует правило в [AGENTS.md](AGENTS.md).
|
||||
|
||||
Выпуск с анимацией изображений сохранён в `servaki.online/лендинги/servaki.online site/2026-09-28-motion-release/`; 256 файлов проверены по [манифесту](archive-manifests/2026-09-28-motion-release.json). Эффекты работают только на видимых карточках и выключаются при настройке «уменьшить движение». Все 23 проверенных адреса опубликованного сайта совпали со сборкой.
|
||||
|
||||
## Сведения о прежней версии
|
||||
|
||||
**Сайт:** https://servaki.online
|
||||
|
||||
+4
-1
@@ -12,6 +12,7 @@
|
||||
- 12 тематических AI-иллюстраций для карточек главной и 12 тёплых фотореалистичных AI-изображений для внутренних страниц, с WebP-версиями 480/960/1536 px. У каждой статьи своё изображение и подпись о том, что оно создано ИИ без данных и фотографий клиентов.
|
||||
- На страницах услуг — конкретные пояснения о согласовании доступа, ограничении видимости данных и бережной работе в действующем офисе или на объекте. Это описание подхода, а не обещание абсолютной безопасности.
|
||||
- В тексте каждой из 12 статей — тематическая схема работы оборудования: индикаторы приборов и движение сигнала между ними. Схемы состоят из компактных SVG и CSS, не загружают GIF или видео. При настройке «уменьшить движение» индикаторы и путь остаются статичными.
|
||||
- На 12 иллюстрациях карточек главной, в каталогах и блоках связанных решений поверх WebP добавлены короткие маршруты сигнала и индикаторы работы оборудования. Они не содержат данных клиентов. Анимация включается только при появлении карточки на экране и выключается при настройке «уменьшить движение»; изображение остаётся доступным без JavaScript. Общий вес новых CSS и JS — 2 404 байта. Самая тяжёлая иллюстрация: 37,1 КБ при 480 px, 116,7 КБ при 960 px и 236,2 КБ при 1536 px.
|
||||
- Описания под всеми 12 карточками главной переписаны: задача клиента, способ работы и результат; условия отклика указаны без обещания устранить любой сбой за 15 минут. У трёх каталогов и главной — отдельные поисковые описания.
|
||||
|
||||
## Просмотр и сборка
|
||||
@@ -29,7 +30,7 @@ python -m http.server 5186 --bind 127.0.0.1 --directory public
|
||||
python verify.py
|
||||
```
|
||||
|
||||
`content/pages.json` — тексты; `content/card-copy.json` — тексты карточек; `content/flow-scenes.json` — подписи и оборудование для анимированных схем; `content/image-prompts.json` и `content/photo-prompts.json` — промпты встроенного image_gen; `content/photo-alt.json` и `content/privacy-notes.json` — описания фото и редакционные пояснения; `artwork/` — оригиналы; `assets/` — CSS и WebP; `public/` — готовый сайт; `reference/original.html` — сохранённая исходная главная.
|
||||
`content/pages.json` — тексты; `content/card-copy.json` — тексты карточек; `content/flow-scenes.json` — подписи и оборудование для анимированных схем; `content/image-motion.json` — иллюстративные маршруты сигнала на карточках; `content/image-prompts.json` и `content/photo-prompts.json` — промпты встроенного image_gen; `content/photo-alt.json` и `content/privacy-notes.json` — описания фото и редакционные пояснения; `artwork/` — оригиналы; `assets/` — CSS, JS и WebP; `public/` — готовый сайт; `reference/original.html` — сохранённая исходная главная.
|
||||
|
||||
## Фактическая основа
|
||||
|
||||
@@ -55,6 +56,8 @@ python verify.py
|
||||
|
||||
`live-verification.json`: 21 публичный URL отдал 200 и байт-в-байт совпал со сборкой. Старый сертификат истёк 17.09.2026, заявка на бесплатный перевыпуск принята панелью Beget 28.09.2026. Повторная проверка в тот же день подтвердила действующий сертификат с окончанием срока 27.12.2026, 12:31 UTC. Диагностический скрипт отдельно проверяет HTTPS стандартной цепочкой доверия.
|
||||
|
||||
Выпуск анимации 28.09.2026: локальная проверка подтвердила 12 анимированных иллюстраций, 16 доступных HTML-страниц, лимиты размеров WebP и неизменные цены. В Chrome проверены каталоги при ширине 1280 и 500 px, а также режим «уменьшить движение». Скрипт `deploy_beget.py update-motion` перед заменой сравнивает все файлы предыдущего выпуска с боевым сайтом и сохраняет изменяемые HTML вне публичного каталога.
|
||||
|
||||
Браузер: главная и статья на обычном экране и 390×844; горизонтального переполнения нет, responsive images загружаются, оглавление и FAQ работают. Анимированная схема сети визуально проверена на 1280 px и в мобильном окне 390 px: устройства выстраиваются вертикально, путь сигнала сохраняется. Исходная форма на главной остаётся демонстрационной; все новые CTA ведут прямо в Telegram или на телефон, сообщения автоматически не отправлялись.
|
||||
|
||||
## Публикация и откат
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
.motion-visual{position:relative;display:block;aspect-ratio:3/2;overflow:hidden;isolation:isolate}
|
||||
.motion-visual>img{display:block;width:100%;height:100%;aspect-ratio:3/2;object-fit:cover}
|
||||
.motion-overlay{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;opacity:0;transition:opacity .45s ease;z-index:1}
|
||||
.motion-visual.is-active .motion-overlay{opacity:1}
|
||||
.motion-path{fill:none;stroke:#67e8f9;stroke-width:2.5;stroke-linecap:round;stroke-dasharray:2 17;opacity:.72;animation:motion-trace 6s linear infinite;animation-play-state:paused}
|
||||
.motion-packet{fill:#f0fdff;stroke:#22d3ee;stroke-width:2;filter:drop-shadow(0 0 7px #22d3ee)}
|
||||
.motion-packet-late{fill:#a5f3fc;opacity:.8}
|
||||
.motion-light{fill:#67e8f9;stroke:#e0faff;stroke-width:2;filter:drop-shadow(0 0 9px #22d3ee);animation:motion-led 2.4s ease-in-out infinite;animation-play-state:paused}
|
||||
.motion-light:nth-of-type(2n){animation-delay:.8s}
|
||||
.motion-visual.is-active .motion-path,.motion-visual.is-active .motion-light{animation-play-state:running}
|
||||
@keyframes motion-trace{to{stroke-dashoffset:-76}}
|
||||
@keyframes motion-led{0%,100%{opacity:.38}50%{opacity:1}}
|
||||
@media(prefers-reduced-motion:reduce){.motion-overlay{display:none!important}.motion-path,.motion-light{animation:none!important}}
|
||||
@@ -0,0 +1,25 @@
|
||||
/* Keep decorative equipment traffic paused when off screen or reduced motion is requested. */
|
||||
(() => {
|
||||
const scenes = [...document.querySelectorAll('.motion-overlay')];
|
||||
if (!scenes.length) return;
|
||||
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
const update = (svg, visible) => {
|
||||
svg.dataset.visible = visible ? '1' : '0';
|
||||
const active = visible && !reduced.matches;
|
||||
svg.parentElement.classList.toggle('is-active', active);
|
||||
if (active) svg.unpauseAnimations();
|
||||
else svg.pauseAnimations();
|
||||
};
|
||||
scenes.forEach(svg => update(svg, false));
|
||||
if ('IntersectionObserver' in window) {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => update(entry.target, entry.isIntersecting));
|
||||
}, {rootMargin: '80px 0px', threshold: 0.01});
|
||||
scenes.forEach(svg => observer.observe(svg));
|
||||
} else {
|
||||
scenes.forEach(svg => update(svg, true));
|
||||
}
|
||||
const refresh = () => scenes.forEach(svg => update(svg, svg.dataset.visible === '1'));
|
||||
if (reduced.addEventListener) reduced.addEventListener('change', refresh);
|
||||
else reduced.addListener(refresh);
|
||||
})();
|
||||
@@ -12,6 +12,7 @@ BY_ID={p['slug']:p for p in PAGES}
|
||||
PRIVACY=json.loads((ROOT/'content/privacy-notes.json').read_text(encoding='utf-8'))
|
||||
PHOTO_ALT=json.loads((ROOT/'content/photo-alt.json').read_text(encoding='utf-8'))
|
||||
FLOWS=json.loads((ROOT/'content/flow-scenes.json').read_text(encoding='utf-8'))
|
||||
IMAGE_MOTION=json.loads((ROOT/'content/image-motion.json').read_text(encoding='utf-8'))
|
||||
CARD_COPY=json.loads((ROOT/'content/card-copy.json').read_text(encoding='utf-8'))
|
||||
GROUPS={'Подход':('approach','Как мы работаем','Поддержка с понятной ответственностью'),'Услуги':('services','IT-услуги для бизнеса','От рабочего места до инфраструктуры'),'Отрасли':('industries','IT-решения для отраслей','Учитываем, как работает ваш бизнес')}
|
||||
HUB_DESCRIPTIONS={
|
||||
@@ -44,16 +45,24 @@ def image(p,lazy=True,small=False):
|
||||
def photo_image(p):
|
||||
slug=p['slug']
|
||||
return image(p,lazy=False).replace(f'/{slug}-',f'/photo-{slug}-').replace(esc(p['alt']),esc(PHOTO_ALT[slug])).replace('(max-width: 1248px) 100vw, 1200px','(max-width: 600px) calc(100vw - 32px), (max-width: 1000px) calc(100vw - 48px), 520px')
|
||||
def motion_image(p,small=True):
|
||||
scene=IMAGE_MOTION[p['slug']]
|
||||
paths=[]
|
||||
for route in scene['routes']:
|
||||
d=esc(route)
|
||||
paths.append(f'<path class="motion-path" d="{d}"/><circle class="motion-packet" r="6"><animateMotion path="{d}" dur="4.8s" repeatCount="indefinite"/></circle><circle class="motion-packet motion-packet-late" r="4"><animateMotion path="{d}" dur="4.8s" begin="2.4s" repeatCount="indefinite"/></circle>')
|
||||
lights=''.join(f'<circle class="motion-light" cx="{x}" cy="{y}" r="7"/>' for x,y in scene['lights'])
|
||||
return f'<div class="motion-visual">{image(p,small=small)}<svg class="motion-overlay" viewBox="0 0 960 640" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false">{"".join(paths)}{lights}</svg></div>'
|
||||
def head(title,description,canonical,img,graph):
|
||||
data=json.dumps({'@context':'https://schema.org','@graph':graph},ensure_ascii=False).replace('<','\\u003c')
|
||||
return f'''<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>{esc(title)} | servaki.online</title><meta name="description" content="{esc(description)}"><meta name="robots" content="index,follow,max-image-preview:large"><meta name="theme-color" content="#05070f"><link rel="canonical" href="{canonical}"><meta property="og:type" content="article"><meta property="og:locale" content="ru_RU"><meta property="og:site_name" content="servaki.online"><meta property="og:title" content="{esc(title)}"><meta property="og:description" content="{esc(description)}"><meta property="og:url" content="{canonical}"><meta property="og:image" content="{img}"><meta property="og:image:width" content="1536"><meta property="og:image:height" content="1024"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="{esc(title)}"><meta name="twitter:description" content="{esc(description)}"><meta name="twitter:image" content="{img}"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"><link rel="stylesheet" href="/assets/editorial/editorial.css"><script type="application/ld+json">{data}</script></head><body>'''
|
||||
return f'''<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>{esc(title)} | servaki.online</title><meta name="description" content="{esc(description)}"><meta name="robots" content="index,follow,max-image-preview:large"><meta name="theme-color" content="#05070f"><link rel="canonical" href="{canonical}"><meta property="og:type" content="article"><meta property="og:locale" content="ru_RU"><meta property="og:site_name" content="servaki.online"><meta property="og:title" content="{esc(title)}"><meta property="og:description" content="{esc(description)}"><meta property="og:url" content="{canonical}"><meta property="og:image" content="{img}"><meta property="og:image:width" content="1536"><meta property="og:image:height" content="1024"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="{esc(title)}"><meta name="twitter:description" content="{esc(description)}"><meta name="twitter:image" content="{img}"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"><link rel="stylesheet" href="/assets/editorial/editorial.css"><link rel="stylesheet" href="/assets/editorial/motion.css"><script src="/assets/editorial/motion.js" defer></script><script type="application/ld+json">{data}</script></head><body>'''
|
||||
NAV='''<a class="skip" href="#content">К содержанию</a><header class="site-nav"><div class="wrap nav-row"><a class="brand" href="/" aria-label="servaki.online — главная"><span class="brand-orbit" aria-hidden="true">S</span><span class="brand-name">servaki.online<small>Ваш IT-отдел на орбите</small></span></a><nav class="nav-links" aria-label="Основная навигация"><a class="desktop-link" href="/services/">Услуги</a><a class="desktop-link" href="/industries/">Отрасли</a><a class="desktop-link" href="/#pricing">Тарифы</a><a class="button" href="https://t.me/servaki_online">Обсудить задачу ↗</a></nav></div></header>'''
|
||||
FOOT='''<footer class="site-footer"><div class="wrap footer-row"><div>© 2026 servaki.online<br>Юг России · Ставропольский край · КМВ</div><nav class="footer-links" aria-label="Ссылки в подвале"><a href="/approach/">Наш подход</a><a href="/services/">Услуги</a><a href="/industries/">Отрасли</a><a href="tel:+79620151414">+7 962 015-14-14</a><a href="https://t.me/servaki_online">Telegram ↗</a></nav></div></footer></body></html>'''
|
||||
ORG={'@type':'Organization','@id':BASE+'/#organization','name':'servaki.online','url':BASE+'/','telephone':'+79620151414','sameAs':['https://t.me/servaki_online'],'areaServed':['Юг России','Ставропольский край','Кавказские Минеральные Воды']}
|
||||
def crumbs(items):
|
||||
return '<nav aria-label="Хлебные крошки"><ol class="crumbs">'+''.join(f'<li><a href="{u}">{esc(t)}</a></li>' if u else f'<li aria-current="page">{esc(t)}</li>' for t,u in items)+'</ol></nav>'
|
||||
def related_card(p):
|
||||
return f'<a class="related-card" href="{url(p)}">{image(p,small=True)}<div class="copy"><h3>{esc(p["card"])}</h3><p>{esc(p["label"])}</p><span class="link-arrow">Подробнее <span aria-hidden="true">↗</span></span></div></a>'
|
||||
return f'<a class="related-card" href="{url(p)}">{motion_image(p)}<div class="copy"><h3>{esc(p["card"])}</h3><p>{esc(p["label"])}</p><span class="link-arrow">Подробнее <span aria-hidden="true">↗</span></span></div></a>'
|
||||
def flow_diagram(p):
|
||||
scene=FLOWS[p['slug']]
|
||||
devices=[]
|
||||
@@ -88,13 +97,13 @@ def home(original):
|
||||
block=before+after
|
||||
# Industry illustrations move below their descriptions, consistent with all cards.
|
||||
block=re.sub(r'<div class="top">.*?</svg></div></div>','',block,flags=re.S)
|
||||
link=f'<a class="card-landing" href="{url(p)}" aria-label="Подробнее: {esc(p["card"])}">{image(p,small=True)}<span>Подробнее: {esc(p["card"])} <b aria-hidden="true">↗</b></span></a>'
|
||||
link=f'<a class="card-landing" href="{url(p)}" aria-label="Подробнее: {esc(p["card"])}">{motion_image(p)}<span>Подробнее: {esc(p["card"])} <b aria-hidden="true">↗</b></span></a>'
|
||||
block=block.replace('</article>',link+'</article>')
|
||||
result=result[:match.start()]+block+result[match.end():]
|
||||
# Scope supplementary styles to homepage components; article reset must not overwrite live home.
|
||||
css='''<style>.card{display:flex;flex-direction:column}.card-landing{display:block;margin-top:22px;border-radius:12px;overflow:hidden;border:1px solid rgba(255,255,255,.12);position:relative;z-index:2}.card-landing img{display:block;width:100%;height:auto;aspect-ratio:3/2;object-fit:cover;transition:transform .25s}.card-landing span{display:block;padding:14px 15px;background:#0c1326;color:#67e8f9;font-size:13px;font-weight:600;line-height:1.5}.card-landing:hover img{transform:scale(1.025)}.card-landing:focus-visible{outline:3px solid #22d3ee;outline-offset:4px}.ind .card-landing{margin:0 24px 24px}.ind .body{padding-top:26px}.card .card-landing{margin-top:auto}.card h3+p{margin-bottom:22px}.card .stack{margin-bottom:22px}body .reveal{opacity:1;transform:none}.editorial-entry{color:#67e8f9!important}@media(prefers-reduced-motion:reduce){.card-landing img{transition:none}}</style>'''
|
||||
css=css.replace('.card-landing span{display:block;', '.card-landing span{display:block;min-height:67px;')
|
||||
result=result.replace('</head>',css+'</head>')
|
||||
result=result.replace('</head>','<link rel="stylesheet" href="/assets/editorial/motion.css"><script src="/assets/editorial/motion.js" defer></script>'+css+'</head>')
|
||||
result=re.sub(r'<title>.*?</title>','<title>IT-поддержка бизнеса на КМВ и Юге России — servaki.online</title>',result,count=1,flags=re.S)
|
||||
result=re.sub(r'(<meta name="description" content=")[^"]*(")',lambda m:m.group(1)+'IT-поддержка бизнеса на КМВ и Юге России: рабочие места, сети и VPN, серверы, видеонаблюдение и удалённые объекты. Услуги, тарифы и порядок обращения.'+m.group(2),result,count=1)
|
||||
result=re.sub(r'(<meta property="og:description" content=")[^"]*(")',lambda m:m.group(1)+'Поддержка рабочих мест, сетей, серверов и удалённых объектов для бизнеса на Юге России.'+m.group(2),result,count=1)
|
||||
@@ -121,6 +130,8 @@ def build():
|
||||
OUT.mkdir(exist_ok=True)
|
||||
dest=OUT/'assets/editorial';dest.mkdir(parents=True,exist_ok=True)
|
||||
shutil.copy2(ROOT/'assets/editorial.css',dest/'editorial.css')
|
||||
shutil.copy2(ROOT/'assets/motion.css',dest/'motion.css')
|
||||
shutil.copy2(ROOT/'assets/motion.js',dest/'motion.js')
|
||||
for p in PAGES:
|
||||
for width in [480,960,1536]:
|
||||
img=ROOT/'assets'/f'{p["slug"]}-{width}.webp'
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"rapid-response": {"routes": ["M245 380 C280 336 291 286 319 243 C409 236 466 207 547 161"], "lights": [[245, 380], [319, 243], [547, 161]]},
|
||||
"remote-sites": {"routes": ["M172 252 C270 128 344 152 480 386 C610 185 719 162 809 271"], "lights": [[172, 252], [480, 386], [809, 271]]},
|
||||
"it-subscription": {"routes": ["M218 371 C344 400 362 287 479 265 C601 208 661 309 800 303"], "lights": [[218, 371], [479, 265], [800, 303]]},
|
||||
"remote-desktop": {"routes": ["M247 286 C344 127 543 150 772 204"], "lights": [[247, 286], [772, 204]]},
|
||||
"networks-vpn": {"routes": ["M146 375 C284 492 389 498 480 413 C589 503 709 491 810 366", "M480 413 C468 269 500 211 477 154"], "lights": [[146, 375], [480, 413], [810, 366], [477, 154]]},
|
||||
"monitoring-security": {"routes": ["M353 412 C481 492 567 444 673 450 C733 407 767 346 770 275"], "lights": [[353, 412], [673, 450], [770, 275]]},
|
||||
"video-surveillance": {"routes": ["M164 169 C287 176 416 287 416 428 C335 463 241 478 183 541"], "lights": [[164, 169], [416, 428], [183, 541]]},
|
||||
"servers-hosting": {"routes": ["M455 418 C317 348 340 190 502 171 C705 139 824 307 737 405 C662 493 549 483 455 418"], "lights": [[455, 418], [502, 171], [737, 405]]},
|
||||
"automation": {"routes": ["M387 333 C501 177 633 180 703 317 C751 375 787 366 820 311"], "lights": [[387, 333], [703, 317], [820, 311]]},
|
||||
"horeca": {"routes": ["M622 138 C502 109 409 160 351 284 C398 372 512 403 622 373"], "lights": [[622, 138], [351, 284], [622, 373]]},
|
||||
"agriculture": {"routes": ["M174 309 C338 177 492 172 609 288 C716 314 790 382 835 450"], "lights": [[174, 309], [609, 288], [835, 450]]},
|
||||
"construction": {"routes": ["M165 232 C282 116 446 119 563 243 C698 294 768 332 828 421"], "lights": [[165, 232], [563, 243], [828, 421]]}
|
||||
}
|
||||
+58
-3
@@ -118,7 +118,7 @@ def audit():
|
||||
sftp.close(); ssh.close()
|
||||
|
||||
|
||||
def put_atomic(sftp, local, remote, replace=False):
|
||||
def put_atomic(sftp, local, remote, replace=False, backup_dir=REMOTE_BACKUP):
|
||||
mkdirs(sftp, posixpath.dirname(remote))
|
||||
temp = remote + '.codex-upload-' + uuid.uuid4().hex[:10]
|
||||
sftp.put(str(local), temp, confirm=True)
|
||||
@@ -128,7 +128,9 @@ def put_atomic(sftp, local, remote, replace=False):
|
||||
try:
|
||||
sftp.posix_rename(temp, remote)
|
||||
except OSError:
|
||||
shifted = REMOTE_BACKUP + '/' + posixpath.basename(remote) + '.shifted'
|
||||
relative = posixpath.relpath(remote, WEBROOT)
|
||||
shifted = backup_dir + '/' + relative + '.shifted'
|
||||
mkdirs(sftp, posixpath.dirname(shifted))
|
||||
sftp.rename(remote, shifted)
|
||||
try:
|
||||
sftp.rename(temp, remote)
|
||||
@@ -198,6 +200,58 @@ def verify():
|
||||
sftp.close(); ssh.close()
|
||||
|
||||
|
||||
def update_motion():
|
||||
"""Publish the illustration motion release only after an exact live baseline check."""
|
||||
previous = ROOT.parent / 'site-servaki-online-git' / 'public'
|
||||
if not previous.is_dir():
|
||||
raise RuntimeError('Previous Gitea release is required for the baseline check')
|
||||
old = {p.relative_to(previous).as_posix(): digest(p.read_bytes())
|
||||
for p in previous.rglob('*') if p.is_file()}
|
||||
new = manifest()
|
||||
removed = set(old) - set(new)
|
||||
changed = {rel for rel in new if new[rel] != old.get(rel)}
|
||||
html = {p.relative_to(ROOT / 'public').as_posix()
|
||||
for p in (ROOT / 'public').rglob('*.html')}
|
||||
allowed = html | {'assets/editorial/motion.css', 'assets/editorial/motion.js'}
|
||||
if removed or changed != allowed:
|
||||
raise RuntimeError(f'Unexpected release scope: removed={sorted(removed)}, changed={sorted(changed ^ allowed)}')
|
||||
local_backup = ROOT / 'release-backups' / '2026-09-28-before-motion'
|
||||
remote_backup = SITE + '/backup-2026-09-28-before-motion'
|
||||
ssh, sftp = connect()
|
||||
try:
|
||||
for rel, expected in old.items():
|
||||
target = WEBROOT + '/' + rel
|
||||
if not exists(sftp, target) or digest(remote_bytes(sftp, target)) != expected:
|
||||
raise RuntimeError('Published baseline changed; stopped before writing: ' + rel)
|
||||
for rel in changed - set(old):
|
||||
if exists(sftp, WEBROOT + '/' + rel):
|
||||
raise RuntimeError('New motion asset already exists: ' + rel)
|
||||
mkdirs(sftp, remote_backup)
|
||||
for rel in sorted(changed & set(old)):
|
||||
data = remote_bytes(sftp, WEBROOT + '/' + rel)
|
||||
destination = local_backup / rel
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if destination.exists() and destination.read_bytes() != data:
|
||||
raise RuntimeError('Local backup conflict: ' + rel)
|
||||
destination.write_bytes(data)
|
||||
remote_copy = remote_backup + '/' + rel
|
||||
if exists(sftp, remote_copy):
|
||||
if digest(remote_bytes(sftp, remote_copy)) != old[rel]:
|
||||
raise RuntimeError('Remote backup conflict: ' + rel)
|
||||
else:
|
||||
put_atomic(sftp, destination, remote_copy)
|
||||
for rel in sorted(changed, key=lambda item: (item == 'index.html', not item.startswith('assets/'), item)):
|
||||
put_atomic(sftp, ROOT / 'public' / rel, WEBROOT + '/' + rel,
|
||||
replace=rel in old, backup_dir=remote_backup)
|
||||
for rel in changed:
|
||||
if digest(remote_bytes(sftp, WEBROOT + '/' + rel)) != new[rel]:
|
||||
raise RuntimeError('Published motion file failed checksum: ' + rel)
|
||||
print('Published animation update; changed files:', len(changed),
|
||||
'verified previous files:', len(old), 'backup:', remote_backup)
|
||||
finally:
|
||||
sftp.close(); ssh.close()
|
||||
|
||||
|
||||
def reprice():
|
||||
"""Replace only the published homepage after verifying the six price tokens."""
|
||||
before = json.loads((BACKUP / 'deployed.json').read_text(encoding='utf-8'))
|
||||
@@ -249,4 +303,5 @@ def reprice():
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
{'audit': audit, 'deploy': deploy, 'verify': verify, 'reprice': reprice}[sys.argv[1]]()
|
||||
{'audit': audit, 'deploy': deploy, 'verify': verify,
|
||||
'reprice': reprice, 'update-motion': update_motion}[sys.argv[1]]()
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
|
||||
.motion-visual{position:relative;display:block;aspect-ratio:3/2;overflow:hidden;isolation:isolate}
|
||||
.motion-visual>img{display:block;width:100%;height:100%;aspect-ratio:3/2;object-fit:cover}
|
||||
.motion-overlay{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;opacity:0;transition:opacity .45s ease;z-index:1}
|
||||
.motion-visual.is-active .motion-overlay{opacity:1}
|
||||
.motion-path{fill:none;stroke:#67e8f9;stroke-width:2.5;stroke-linecap:round;stroke-dasharray:2 17;opacity:.72;animation:motion-trace 6s linear infinite;animation-play-state:paused}
|
||||
.motion-packet{fill:#f0fdff;stroke:#22d3ee;stroke-width:2;filter:drop-shadow(0 0 7px #22d3ee)}
|
||||
.motion-packet-late{fill:#a5f3fc;opacity:.8}
|
||||
.motion-light{fill:#67e8f9;stroke:#e0faff;stroke-width:2;filter:drop-shadow(0 0 9px #22d3ee);animation:motion-led 2.4s ease-in-out infinite;animation-play-state:paused}
|
||||
.motion-light:nth-of-type(2n){animation-delay:.8s}
|
||||
.motion-visual.is-active .motion-path,.motion-visual.is-active .motion-light{animation-play-state:running}
|
||||
@keyframes motion-trace{to{stroke-dashoffset:-76}}
|
||||
@keyframes motion-led{0%,100%{opacity:.38}50%{opacity:1}}
|
||||
@media(prefers-reduced-motion:reduce){.motion-overlay{display:none!important}.motion-path,.motion-light{animation:none!important}}
|
||||
@@ -0,0 +1,25 @@
|
||||
/* Keep decorative equipment traffic paused when off screen or reduced motion is requested. */
|
||||
(() => {
|
||||
const scenes = [...document.querySelectorAll('.motion-overlay')];
|
||||
if (!scenes.length) return;
|
||||
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
const update = (svg, visible) => {
|
||||
svg.dataset.visible = visible ? '1' : '0';
|
||||
const active = visible && !reduced.matches;
|
||||
svg.parentElement.classList.toggle('is-active', active);
|
||||
if (active) svg.unpauseAnimations();
|
||||
else svg.pauseAnimations();
|
||||
};
|
||||
scenes.forEach(svg => update(svg, false));
|
||||
if ('IntersectionObserver' in window) {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => update(entry.target, entry.isIntersecting));
|
||||
}, {rootMargin: '80px 0px', threshold: 0.01});
|
||||
scenes.forEach(svg => observer.observe(svg));
|
||||
} else {
|
||||
scenes.forEach(svg => update(svg, true));
|
||||
}
|
||||
const refresh = () => scenes.forEach(svg => update(svg, svg.dataset.visible === '1'));
|
||||
if (reduced.addEventListener) reduced.addEventListener('change', refresh);
|
||||
else reduced.addListener(refresh);
|
||||
})();
|
||||
+13
-13
@@ -758,7 +758,7 @@
|
||||
</script>
|
||||
<noscript><div><img src="https://mc.yandex.ru/watch/110477494" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||
<!-- /Yandex.Metrika counter -->
|
||||
<style>.card{display:flex;flex-direction:column}.card-landing{display:block;margin-top:22px;border-radius:12px;overflow:hidden;border:1px solid rgba(255,255,255,.12);position:relative;z-index:2}.card-landing img{display:block;width:100%;height:auto;aspect-ratio:3/2;object-fit:cover;transition:transform .25s}.card-landing span{display:block;min-height:67px;padding:14px 15px;background:#0c1326;color:#67e8f9;font-size:13px;font-weight:600;line-height:1.5}.card-landing:hover img{transform:scale(1.025)}.card-landing:focus-visible{outline:3px solid #22d3ee;outline-offset:4px}.ind .card-landing{margin:0 24px 24px}.ind .body{padding-top:26px}.card .card-landing{margin-top:auto}.card h3+p{margin-bottom:22px}.card .stack{margin-bottom:22px}body .reveal{opacity:1;transform:none}.editorial-entry{color:#67e8f9!important}@media(prefers-reduced-motion:reduce){.card-landing img{transition:none}}</style></head>
|
||||
<link rel="stylesheet" href="/assets/editorial/motion.css"><script src="/assets/editorial/motion.js" defer></script><style>.card{display:flex;flex-direction:column}.card-landing{display:block;margin-top:22px;border-radius:12px;overflow:hidden;border:1px solid rgba(255,255,255,.12);position:relative;z-index:2}.card-landing img{display:block;width:100%;height:auto;aspect-ratio:3/2;object-fit:cover;transition:transform .25s}.card-landing span{display:block;min-height:67px;padding:14px 15px;background:#0c1326;color:#67e8f9;font-size:13px;font-weight:600;line-height:1.5}.card-landing:hover img{transform:scale(1.025)}.card-landing:focus-visible{outline:3px solid #22d3ee;outline-offset:4px}.ind .card-landing{margin:0 24px 24px}.ind .body{padding-top:26px}.card .card-landing{margin-top:auto}.card h3+p{margin-bottom:22px}.card .stack{margin-bottom:22px}body .reveal{opacity:1;transform:none}.editorial-entry{color:#67e8f9!important}@media(prefers-reduced-motion:reduce){.card-landing img{transition:none}}</style></head>
|
||||
<body id="top">
|
||||
|
||||
<a class="skip" href="#main">Перейти к содержанию</a>
|
||||
@@ -860,17 +860,17 @@
|
||||
<div class="card-ic" aria-hidden="true"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.8"/><path d="M12 7v5l3 2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<h3>Реакция от 15 минут</h3>
|
||||
<p>При сбое связи, компьютера или сервиса принимаем заявку, оцениваем влияние на работу и начинаем диагностику. Отклик от 15 минут зависит от тарифа; срок устранения определяем после выяснения причины.</p>
|
||||
<a class="card-landing" href="/approach/rapid-response/" aria-label="Подробнее: Реакция от 15 минут"><img src="/assets/editorial/rapid-response-960.webp" srcset="/assets/editorial/rapid-response-480.webp 480w, /assets/editorial/rapid-response-960.webp 960w, /assets/editorial/rapid-response-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Рабочее место IT-поддержки и световая шкала времени в тёмно-синей палитре" loading="lazy" decoding="async"><span>Подробнее: Реакция от 15 минут <b aria-hidden="true">↗</b></span></a></article>
|
||||
<a class="card-landing" href="/approach/rapid-response/" aria-label="Подробнее: Реакция от 15 минут"><div class="motion-visual"><img src="/assets/editorial/rapid-response-960.webp" srcset="/assets/editorial/rapid-response-480.webp 480w, /assets/editorial/rapid-response-960.webp 960w, /assets/editorial/rapid-response-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Рабочее место IT-поддержки и световая шкала времени в тёмно-синей палитре" loading="lazy" decoding="async"><svg class="motion-overlay" viewBox="0 0 960 640" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><path class="motion-path" d="M245 380 C280 336 291 286 319 243 C409 236 466 207 547 161"/><circle class="motion-packet" r="6"><animateMotion path="M245 380 C280 336 291 286 319 243 C409 236 466 207 547 161" dur="4.8s" repeatCount="indefinite"/></circle><circle class="motion-packet motion-packet-late" r="4"><animateMotion path="M245 380 C280 336 291 286 319 243 C409 236 466 207 547 161" dur="4.8s" begin="2.4s" repeatCount="indefinite"/></circle><circle class="motion-light" cx="245" cy="380" r="7"/><circle class="motion-light" cx="319" cy="243" r="7"/><circle class="motion-light" cx="547" cy="161" r="7"/></svg></div><span>Подробнее: Реакция от 15 минут <b aria-hidden="true">↗</b></span></a></article>
|
||||
<article class="card reveal d1">
|
||||
<div class="card-ic" aria-hidden="true"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M3 12h4l3-8 4 16 3-8h4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<h3>Работаем с удалёнными точками</h3>
|
||||
<p>Связываем фермы, весовые, склады и горные объекты с офисом. Проверяем канал, питание и доступ к нужным системам, чтобы удалённая площадка оставалась управляемой.</p>
|
||||
<a class="card-landing" href="/approach/remote-sites/" aria-label="Подробнее: Работаем с удалёнными точками"><img src="/assets/editorial/remote-sites-960.webp" srcset="/assets/editorial/remote-sites-480.webp 480w, /assets/editorial/remote-sites-960.webp 960w, /assets/editorial/remote-sites-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Ферма, склад и горная база связаны голубыми линиями сети в кавказской долине" loading="lazy" decoding="async"><span>Подробнее: Работаем с удалёнными точками <b aria-hidden="true">↗</b></span></a></article>
|
||||
<a class="card-landing" href="/approach/remote-sites/" aria-label="Подробнее: Работаем с удалёнными точками"><div class="motion-visual"><img src="/assets/editorial/remote-sites-960.webp" srcset="/assets/editorial/remote-sites-480.webp 480w, /assets/editorial/remote-sites-960.webp 960w, /assets/editorial/remote-sites-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Ферма, склад и горная база связаны голубыми линиями сети в кавказской долине" loading="lazy" decoding="async"><svg class="motion-overlay" viewBox="0 0 960 640" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><path class="motion-path" d="M172 252 C270 128 344 152 480 386 C610 185 719 162 809 271"/><circle class="motion-packet" r="6"><animateMotion path="M172 252 C270 128 344 152 480 386 C610 185 719 162 809 271" dur="4.8s" repeatCount="indefinite"/></circle><circle class="motion-packet motion-packet-late" r="4"><animateMotion path="M172 252 C270 128 344 152 480 386 C610 185 719 162 809 271" dur="4.8s" begin="2.4s" repeatCount="indefinite"/></circle><circle class="motion-light" cx="172" cy="252" r="7"/><circle class="motion-light" cx="480" cy="386" r="7"/><circle class="motion-light" cx="809" cy="271" r="7"/></svg></div><span>Подробнее: Работаем с удалёнными точками <b aria-hidden="true">↗</b></span></a></article>
|
||||
<article class="card reveal d2">
|
||||
<div class="card-ic" aria-hidden="true"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect x="4" y="5" width="16" height="14" rx="2" stroke="currentColor" stroke-width="1.8"/><path d="M4 10h16" stroke="currentColor" stroke-width="1.8"/><path d="M8 15h4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></div>
|
||||
<h3>Доступные подписки</h3>
|
||||
<p>Обслуживание рабочих мест и инфраструктуры по понятному ежемесячному плану: состав работ, ответственные и время отклика согласованы заранее. Помогаем не копить сбои до остановки работы.</p>
|
||||
<a class="card-landing" href="/approach/it-subscription/" aria-label="Подробнее: Доступные подписки"><img src="/assets/editorial/it-subscription-960.webp" srcset="/assets/editorial/it-subscription-480.webp 480w, /assets/editorial/it-subscription-960.webp 960w, /assets/editorial/it-subscription-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Компьютер, сервер и роутер на упорядоченных модульных платформах" loading="lazy" decoding="async"><span>Подробнее: Доступные подписки <b aria-hidden="true">↗</b></span></a></article>
|
||||
<a class="card-landing" href="/approach/it-subscription/" aria-label="Подробнее: Доступные подписки"><div class="motion-visual"><img src="/assets/editorial/it-subscription-960.webp" srcset="/assets/editorial/it-subscription-480.webp 480w, /assets/editorial/it-subscription-960.webp 960w, /assets/editorial/it-subscription-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Компьютер, сервер и роутер на упорядоченных модульных платформах" loading="lazy" decoding="async"><svg class="motion-overlay" viewBox="0 0 960 640" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><path class="motion-path" d="M218 371 C344 400 362 287 479 265 C601 208 661 309 800 303"/><circle class="motion-packet" r="6"><animateMotion path="M218 371 C344 400 362 287 479 265 C601 208 661 309 800 303" dur="4.8s" repeatCount="indefinite"/></circle><circle class="motion-packet motion-packet-late" r="4"><animateMotion path="M218 371 C344 400 362 287 479 265 C601 208 661 309 800 303" dur="4.8s" begin="2.4s" repeatCount="indefinite"/></circle><circle class="motion-light" cx="218" cy="371" r="7"/><circle class="motion-light" cx="479" cy="265" r="7"/><circle class="motion-light" cx="800" cy="303" r="7"/></svg></div><span>Подробнее: Доступные подписки <b aria-hidden="true">↗</b></span></a></article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -890,39 +890,39 @@
|
||||
<div class="card-ic" aria-hidden="true"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect x="3" y="4" width="18" height="12" rx="2" stroke="currentColor" stroke-width="1.8"/><path d="M8 20h8M12 16v4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></div>
|
||||
<h3>Удалённый рабочий стол</h3>
|
||||
<p>Помогаем сотруднику с компьютером, программой или принтером через согласованный сеанс. Сначала уточняем задачу, затем проверяем результат вместе с пользователем без лишнего доступа к данным.</p>
|
||||
<a class="card-landing" href="/services/remote-desktop/" aria-label="Подробнее: Удалённый рабочий стол"><img src="/assets/editorial/remote-desktop-960.webp" srcset="/assets/editorial/remote-desktop-480.webp 480w, /assets/editorial/remote-desktop-960.webp 960w, /assets/editorial/remote-desktop-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Офисный компьютер и компактный принтер с защищённым каналом удалённой помощи" loading="lazy" decoding="async"><span>Подробнее: Удалённый рабочий стол <b aria-hidden="true">↗</b></span></a></article>
|
||||
<a class="card-landing" href="/services/remote-desktop/" aria-label="Подробнее: Удалённый рабочий стол"><div class="motion-visual"><img src="/assets/editorial/remote-desktop-960.webp" srcset="/assets/editorial/remote-desktop-480.webp 480w, /assets/editorial/remote-desktop-960.webp 960w, /assets/editorial/remote-desktop-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Офисный компьютер и компактный принтер с защищённым каналом удалённой помощи" loading="lazy" decoding="async"><svg class="motion-overlay" viewBox="0 0 960 640" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><path class="motion-path" d="M247 286 C344 127 543 150 772 204"/><circle class="motion-packet" r="6"><animateMotion path="M247 286 C344 127 543 150 772 204" dur="4.8s" repeatCount="indefinite"/></circle><circle class="motion-packet motion-packet-late" r="4"><animateMotion path="M247 286 C344 127 543 150 772 204" dur="4.8s" begin="2.4s" repeatCount="indefinite"/></circle><circle class="motion-light" cx="247" cy="286" r="7"/><circle class="motion-light" cx="772" cy="204" r="7"/></svg></div><span>Подробнее: Удалённый рабочий стол <b aria-hidden="true">↗</b></span></a></article>
|
||||
<article class="card reveal d1">
|
||||
<span class="num-badge" aria-hidden="true">02</span>
|
||||
<div class="card-ic" aria-hidden="true"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><circle cx="6" cy="6" r="2.5" stroke="currentColor" stroke-width="1.8"/><circle cx="18" cy="18" r="2.5" stroke="currentColor" stroke-width="1.8"/><circle cx="18" cy="6" r="2.5" stroke="currentColor" stroke-width="1.8"/><path d="M8 6h7M6 8v8a2 2 0 0 0 2 2h7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></div>
|
||||
<h3>Сети и VPN</h3>
|
||||
<p>Строим связь между офисом, складом и удалённым объектом через защищённые соединения. Настраиваем маршруты и права доступа, проверяем прохождение трафика до рабочих сервисов.</p>
|
||||
<div class="stack"><span>WireGuard</span><span>ZeroTier</span><span>Keenetic</span></div>
|
||||
<a class="card-landing" href="/services/networks-vpn/" aria-label="Подробнее: Сети и VPN"><img src="/assets/editorial/networks-vpn-960.webp" srcset="/assets/editorial/networks-vpn-480.webp 480w, /assets/editorial/networks-vpn-960.webp 960w, /assets/editorial/networks-vpn-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Маршрутизатор и сетевой коммутатор соединяют несколько офисных миниатюр световыми линиями" loading="lazy" decoding="async"><span>Подробнее: Сети и VPN <b aria-hidden="true">↗</b></span></a></article>
|
||||
<a class="card-landing" href="/services/networks-vpn/" aria-label="Подробнее: Сети и VPN"><div class="motion-visual"><img src="/assets/editorial/networks-vpn-960.webp" srcset="/assets/editorial/networks-vpn-480.webp 480w, /assets/editorial/networks-vpn-960.webp 960w, /assets/editorial/networks-vpn-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Маршрутизатор и сетевой коммутатор соединяют несколько офисных миниатюр световыми линиями" loading="lazy" decoding="async"><svg class="motion-overlay" viewBox="0 0 960 640" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><path class="motion-path" d="M146 375 C284 492 389 498 480 413 C589 503 709 491 810 366"/><circle class="motion-packet" r="6"><animateMotion path="M146 375 C284 492 389 498 480 413 C589 503 709 491 810 366" dur="4.8s" repeatCount="indefinite"/></circle><circle class="motion-packet motion-packet-late" r="4"><animateMotion path="M146 375 C284 492 389 498 480 413 C589 503 709 491 810 366" dur="4.8s" begin="2.4s" repeatCount="indefinite"/></circle><path class="motion-path" d="M480 413 C468 269 500 211 477 154"/><circle class="motion-packet" r="6"><animateMotion path="M480 413 C468 269 500 211 477 154" dur="4.8s" repeatCount="indefinite"/></circle><circle class="motion-packet motion-packet-late" r="4"><animateMotion path="M480 413 C468 269 500 211 477 154" dur="4.8s" begin="2.4s" repeatCount="indefinite"/></circle><circle class="motion-light" cx="146" cy="375" r="7"/><circle class="motion-light" cx="480" cy="413" r="7"/><circle class="motion-light" cx="810" cy="366" r="7"/><circle class="motion-light" cx="477" cy="154" r="7"/></svg></div><span>Подробнее: Сети и VPN <b aria-hidden="true">↗</b></span></a></article>
|
||||
<article class="card reveal d2">
|
||||
<span class="num-badge" aria-hidden="true">03</span>
|
||||
<div class="card-ic" aria-hidden="true"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M12 3 4 6v6c0 5 3.4 8.3 8 10 4.6-1.7 8-5 8-10V6l-8-3Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="m9 12 2 2 4-4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<h3>Мониторинг и безопасность</h3>
|
||||
<p>Следим за доступностью важных сервисов и сообщаем ответственным о сбоях. Состав мониторинга, резервное копирование и уведомления подбираем под инфраструктуру и порядок работы команды.</p>
|
||||
<a class="card-landing" href="/services/monitoring-security/" aria-label="Подробнее: Мониторинг и безопасность"><img src="/assets/editorial/monitoring-security-960.webp" srcset="/assets/editorial/monitoring-security-480.webp 480w, /assets/editorial/monitoring-security-960.webp 960w, /assets/editorial/monitoring-security-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Панель наблюдения за серверами, сигналы доступности и защищённое резервное хранилище" loading="lazy" decoding="async"><span>Подробнее: Мониторинг и безопасность <b aria-hidden="true">↗</b></span></a></article>
|
||||
<a class="card-landing" href="/services/monitoring-security/" aria-label="Подробнее: Мониторинг и безопасность"><div class="motion-visual"><img src="/assets/editorial/monitoring-security-960.webp" srcset="/assets/editorial/monitoring-security-480.webp 480w, /assets/editorial/monitoring-security-960.webp 960w, /assets/editorial/monitoring-security-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Панель наблюдения за серверами, сигналы доступности и защищённое резервное хранилище" loading="lazy" decoding="async"><svg class="motion-overlay" viewBox="0 0 960 640" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><path class="motion-path" d="M353 412 C481 492 567 444 673 450 C733 407 767 346 770 275"/><circle class="motion-packet" r="6"><animateMotion path="M353 412 C481 492 567 444 673 450 C733 407 767 346 770 275" dur="4.8s" repeatCount="indefinite"/></circle><circle class="motion-packet motion-packet-late" r="4"><animateMotion path="M353 412 C481 492 567 444 673 450 C733 407 767 346 770 275" dur="4.8s" begin="2.4s" repeatCount="indefinite"/></circle><circle class="motion-light" cx="353" cy="412" r="7"/><circle class="motion-light" cx="673" cy="450" r="7"/><circle class="motion-light" cx="770" cy="275" r="7"/></svg></div><span>Подробнее: Мониторинг и безопасность <b aria-hidden="true">↗</b></span></a></article>
|
||||
<article class="card reveal">
|
||||
<span class="num-badge" aria-hidden="true">04</span>
|
||||
<div class="card-ic" aria-hidden="true"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M3 7a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" stroke="currentColor" stroke-width="1.8"/><path d="m16 10 5-3v10l-5-3" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg></div>
|
||||
<h3>Видеонаблюдение</h3>
|
||||
<p>Подбираем камеры, запись и удалённый просмотр под задачи объекта. Учитываем зоны обзора, питание, хранение архива и права доступа, чтобы система помогала в работе.</p>
|
||||
<a class="card-landing" href="/services/video-surveillance/" aria-label="Подробнее: Видеонаблюдение"><img src="/assets/editorial/video-surveillance-960.webp" srcset="/assets/editorial/video-surveillance-480.webp 480w, /assets/editorial/video-surveillance-960.webp 960w, /assets/editorial/video-surveillance-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="IP-камера над складской площадкой и видеорегистратор в тёмной технологической композиции" loading="lazy" decoding="async"><span>Подробнее: Видеонаблюдение <b aria-hidden="true">↗</b></span></a></article>
|
||||
<a class="card-landing" href="/services/video-surveillance/" aria-label="Подробнее: Видеонаблюдение"><div class="motion-visual"><img src="/assets/editorial/video-surveillance-960.webp" srcset="/assets/editorial/video-surveillance-480.webp 480w, /assets/editorial/video-surveillance-960.webp 960w, /assets/editorial/video-surveillance-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="IP-камера над складской площадкой и видеорегистратор в тёмной технологической композиции" loading="lazy" decoding="async"><svg class="motion-overlay" viewBox="0 0 960 640" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><path class="motion-path" d="M164 169 C287 176 416 287 416 428 C335 463 241 478 183 541"/><circle class="motion-packet" r="6"><animateMotion path="M164 169 C287 176 416 287 416 428 C335 463 241 478 183 541" dur="4.8s" repeatCount="indefinite"/></circle><circle class="motion-packet motion-packet-late" r="4"><animateMotion path="M164 169 C287 176 416 287 416 428 C335 463 241 478 183 541" dur="4.8s" begin="2.4s" repeatCount="indefinite"/></circle><circle class="motion-light" cx="164" cy="169" r="7"/><circle class="motion-light" cx="416" cy="428" r="7"/><circle class="motion-light" cx="183" cy="541" r="7"/></svg></div><span>Подробнее: Видеонаблюдение <b aria-hidden="true">↗</b></span></a></article>
|
||||
<article class="card reveal d1">
|
||||
<span class="num-badge" aria-hidden="true">05</span>
|
||||
<div class="card-ic" aria-hidden="true"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect x="4" y="4" width="16" height="6" rx="1.5" stroke="currentColor" stroke-width="1.8"/><rect x="4" y="14" width="16" height="6" rx="1.5" stroke="currentColor" stroke-width="1.8"/><circle cx="8" cy="7" r="1" fill="currentColor"/><circle cx="8" cy="17" r="1" fill="currentColor"/></svg></div>
|
||||
<h3>Серверы и хостинг</h3>
|
||||
<p>Размещаем и сопровождаем серверы для рабочих приложений, чатов и внутренних сервисов. Контролируем доступы, обновления и резервные копии, согласуем перенос до начала работ.</p>
|
||||
<div class="stack"><span>Proxmox</span><span>Docker</span><span>CRM · 1С</span></div>
|
||||
<a class="card-landing" href="/services/servers-hosting/" aria-label="Подробнее: Серверы и хостинг"><img src="/assets/editorial/servers-hosting-960.webp" srcset="/assets/editorial/servers-hosting-480.webp 480w, /assets/editorial/servers-hosting-960.webp 960w, /assets/editorial/servers-hosting-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Компактная серверная стойка с синим и бирюзовым светом и орбитальной линией резервирования" loading="lazy" decoding="async"><span>Подробнее: Серверы и хостинг <b aria-hidden="true">↗</b></span></a></article>
|
||||
<a class="card-landing" href="/services/servers-hosting/" aria-label="Подробнее: Серверы и хостинг"><div class="motion-visual"><img src="/assets/editorial/servers-hosting-960.webp" srcset="/assets/editorial/servers-hosting-480.webp 480w, /assets/editorial/servers-hosting-960.webp 960w, /assets/editorial/servers-hosting-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Компактная серверная стойка с синим и бирюзовым светом и орбитальной линией резервирования" loading="lazy" decoding="async"><svg class="motion-overlay" viewBox="0 0 960 640" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><path class="motion-path" d="M455 418 C317 348 340 190 502 171 C705 139 824 307 737 405 C662 493 549 483 455 418"/><circle class="motion-packet" r="6"><animateMotion path="M455 418 C317 348 340 190 502 171 C705 139 824 307 737 405 C662 493 549 483 455 418" dur="4.8s" repeatCount="indefinite"/></circle><circle class="motion-packet motion-packet-late" r="4"><animateMotion path="M455 418 C317 348 340 190 502 171 C705 139 824 307 737 405 C662 493 549 483 455 418" dur="4.8s" begin="2.4s" repeatCount="indefinite"/></circle><circle class="motion-light" cx="455" cy="418" r="7"/><circle class="motion-light" cx="502" cy="171" r="7"/><circle class="motion-light" cx="737" cy="405" r="7"/></svg></div><span>Подробнее: Серверы и хостинг <b aria-hidden="true">↗</b></span></a></article>
|
||||
<article class="card reveal d2">
|
||||
<span class="num-badge" aria-hidden="true">06</span>
|
||||
<div class="card-ic" aria-hidden="true"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M12 3v4M12 17v4M5 12H1M23 12h-4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><rect x="7" y="7" width="10" height="10" rx="2.5" stroke="currentColor" stroke-width="1.8"/></svg></div>
|
||||
<h3>Автоматизация</h3>
|
||||
<p>Соединяем датчики, учётные системы и уведомления в понятный рабочий процесс. Боты сообщают о событиях и собирают данные; управляющие команды доступны согласованным сотрудникам.</p>
|
||||
<a class="card-landing" href="/services/automation/" aria-label="Подробнее: Автоматизация"><img src="/assets/editorial/automation-960.webp" srcset="/assets/editorial/automation-480.webp 480w, /assets/editorial/automation-960.webp 960w, /assets/editorial/automation-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Промышленный контроллер, датчик и телефон соединены световыми потоками событий" loading="lazy" decoding="async"><span>Подробнее: Автоматизация <b aria-hidden="true">↗</b></span></a></article>
|
||||
<a class="card-landing" href="/services/automation/" aria-label="Подробнее: Автоматизация"><div class="motion-visual"><img src="/assets/editorial/automation-960.webp" srcset="/assets/editorial/automation-480.webp 480w, /assets/editorial/automation-960.webp 960w, /assets/editorial/automation-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Промышленный контроллер, датчик и телефон соединены световыми потоками событий" loading="lazy" decoding="async"><svg class="motion-overlay" viewBox="0 0 960 640" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><path class="motion-path" d="M387 333 C501 177 633 180 703 317 C751 375 787 366 820 311"/><circle class="motion-packet" r="6"><animateMotion path="M387 333 C501 177 633 180 703 317 C751 375 787 366 820 311" dur="4.8s" repeatCount="indefinite"/></circle><circle class="motion-packet motion-packet-late" r="4"><animateMotion path="M387 333 C501 177 633 180 703 317 C751 375 787 366 820 311" dur="4.8s" begin="2.4s" repeatCount="indefinite"/></circle><circle class="motion-light" cx="387" cy="333" r="7"/><circle class="motion-light" cx="703" cy="317" r="7"/><circle class="motion-light" cx="820" cy="311" r="7"/></svg></div><span>Подробнее: Автоматизация <b aria-hidden="true">↗</b></span></a></article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -943,21 +943,21 @@
|
||||
<h3>HoReCa</h3>
|
||||
<p>Для гостиниц и кафе проектируем разделение гостевого Wi-Fi и рабочих систем, помогаем с кассами и служебными сервисами. Доступ к данным гостей определяется правилами заведения.</p>
|
||||
</div>
|
||||
<a class="card-landing" href="/industries/horeca/" aria-label="Подробнее: HoReCa"><img src="/assets/editorial/horeca-960.webp" srcset="/assets/editorial/horeca-480.webp 480w, /assets/editorial/horeca-960.webp 960w, /assets/editorial/horeca-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Уютный ресторан при горном отеле со стойкой администратора и деликатными линиями Wi-Fi" loading="lazy" decoding="async"><span>Подробнее: HoReCa <b aria-hidden="true">↗</b></span></a></article>
|
||||
<a class="card-landing" href="/industries/horeca/" aria-label="Подробнее: HoReCa"><div class="motion-visual"><img src="/assets/editorial/horeca-960.webp" srcset="/assets/editorial/horeca-480.webp 480w, /assets/editorial/horeca-960.webp 960w, /assets/editorial/horeca-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Уютный ресторан при горном отеле со стойкой администратора и деликатными линиями Wi-Fi" loading="lazy" decoding="async"><svg class="motion-overlay" viewBox="0 0 960 640" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><path class="motion-path" d="M622 138 C502 109 409 160 351 284 C398 372 512 403 622 373"/><circle class="motion-packet" r="6"><animateMotion path="M622 138 C502 109 409 160 351 284 C398 372 512 403 622 373" dur="4.8s" repeatCount="indefinite"/></circle><circle class="motion-packet motion-packet-late" r="4"><animateMotion path="M622 138 C502 109 409 160 351 284 C398 372 512 403 622 373" dur="4.8s" begin="2.4s" repeatCount="indefinite"/></circle><circle class="motion-light" cx="622" cy="138" r="7"/><circle class="motion-light" cx="351" cy="284" r="7"/><circle class="motion-light" cx="622" cy="373" r="7"/></svg></div><span>Подробнее: HoReCa <b aria-hidden="true">↗</b></span></a></article>
|
||||
<article class="ind ind-2 reveal d1">
|
||||
|
||||
<div class="body">
|
||||
<h3>Агросектор</h3>
|
||||
<p>На фермах и складах связываем датчики, весовые, камеры и удалённые площадки. Проверяем связь и питание, чтобы ответственные получали нужные показатели и понимали причину сбоя.</p>
|
||||
</div>
|
||||
<a class="card-landing" href="/industries/agriculture/" aria-label="Подробнее: Агросектор"><img src="/assets/editorial/agriculture-960.webp" srcset="/assets/editorial/agriculture-480.webp 480w, /assets/editorial/agriculture-960.webp 960w, /assets/editorial/agriculture-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Ферма у гор, весовая площадка и зерновое хранилище с деликатной визуализацией сети датчиков" loading="lazy" decoding="async"><span>Подробнее: Агросектор <b aria-hidden="true">↗</b></span></a></article>
|
||||
<a class="card-landing" href="/industries/agriculture/" aria-label="Подробнее: Агросектор"><div class="motion-visual"><img src="/assets/editorial/agriculture-960.webp" srcset="/assets/editorial/agriculture-480.webp 480w, /assets/editorial/agriculture-960.webp 960w, /assets/editorial/agriculture-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Ферма у гор, весовая площадка и зерновое хранилище с деликатной визуализацией сети датчиков" loading="lazy" decoding="async"><svg class="motion-overlay" viewBox="0 0 960 640" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><path class="motion-path" d="M174 309 C338 177 492 172 609 288 C716 314 790 382 835 450"/><circle class="motion-packet" r="6"><animateMotion path="M174 309 C338 177 492 172 609 288 C716 314 790 382 835 450" dur="4.8s" repeatCount="indefinite"/></circle><circle class="motion-packet motion-packet-late" r="4"><animateMotion path="M174 309 C338 177 492 172 609 288 C716 314 790 382 835 450" dur="4.8s" begin="2.4s" repeatCount="indefinite"/></circle><circle class="motion-light" cx="174" cy="309" r="7"/><circle class="motion-light" cx="609" cy="288" r="7"/><circle class="motion-light" cx="835" cy="450" r="7"/></svg></div><span>Подробнее: Агросектор <b aria-hidden="true">↗</b></span></a></article>
|
||||
<article class="ind ind-3 reveal d2">
|
||||
|
||||
<div class="body">
|
||||
<h3>Строительство</h3>
|
||||
<p>Организуем временные сети, рабочие места и камеры на стройплощадке. Учитываем перенос оборудования по этапам работ, питание, схему доступа и передачу настроек.</p>
|
||||
</div>
|
||||
<a class="card-landing" href="/industries/construction/" aria-label="Подробнее: Строительство"><img src="/assets/editorial/construction-960.webp" srcset="/assets/editorial/construction-480.webp 480w, /assets/editorial/construction-960.webp 960w, /assets/editorial/construction-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Строительная площадка с краном, модульным офисом и камерой на фоне гор в ночной палитре" loading="lazy" decoding="async"><span>Подробнее: Строительство <b aria-hidden="true">↗</b></span></a></article>
|
||||
<a class="card-landing" href="/industries/construction/" aria-label="Подробнее: Строительство"><div class="motion-visual"><img src="/assets/editorial/construction-960.webp" srcset="/assets/editorial/construction-480.webp 480w, /assets/editorial/construction-960.webp 960w, /assets/editorial/construction-1536.webp 1536w" sizes="(max-width: 600px) 100vw, 380px" width="1536" height="1024" alt="Строительная площадка с краном, модульным офисом и камерой на фоне гор в ночной палитре" loading="lazy" decoding="async"><svg class="motion-overlay" viewBox="0 0 960 640" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><path class="motion-path" d="M165 232 C282 116 446 119 563 243 C698 294 768 332 828 421"/><circle class="motion-packet" r="6"><animateMotion path="M165 232 C282 116 446 119 563 243 C698 294 768 332 828 421" dur="4.8s" repeatCount="indefinite"/></circle><circle class="motion-packet motion-packet-late" r="4"><animateMotion path="M165 232 C282 116 446 119 563 243 C698 294 768 332 828 421" dur="4.8s" begin="2.4s" repeatCount="indefinite"/></circle><circle class="motion-light" cx="165" cy="232" r="7"/><circle class="motion-light" cx="563" cy="243" r="7"/><circle class="motion-light" cx="828" cy="421" r="7"/></svg></div><span>Подробнее: Строительство <b aria-hidden="true">↗</b></span></a></article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -18,6 +18,7 @@ class Page(HTMLParser):
|
||||
if tag=='title':self.in_title=True
|
||||
if tag=='a' and a.get('href'):self.links.append(a['href'])
|
||||
if tag=='link' and a.get('rel')=='stylesheet':self.links.append(a['href'])
|
||||
if tag=='script' and a.get('src'):self.links.append(a['src'])
|
||||
if tag=='link' and a.get('rel')=='canonical':self.canonical=a['href']
|
||||
if tag=='meta' and a.get('name')=='description':self.description=a.get('content','')
|
||||
if tag=='img' and a.get('src'):
|
||||
@@ -57,12 +58,17 @@ def check(text,rel):
|
||||
def main():
|
||||
files=sorted(PUBLIC.rglob('*.html'));titles=[];descriptions=[];checks=[]
|
||||
scenes=json.loads((ROOT/'content/flow-scenes.json').read_text(encoding='utf-8'))
|
||||
image_motion=json.loads((ROOT/'content/image-motion.json').read_text(encoding='utf-8'))
|
||||
pages=json.loads((ROOT/'content/pages.json').read_text(encoding='utf-8'))
|
||||
card_copy=json.loads((ROOT/'content/card-copy.json').read_text(encoding='utf-8'))
|
||||
assert set(scenes)=={p['slug'] for p in pages},'missing or extra equipment scene'
|
||||
assert set(image_motion)==set(scenes),'missing or extra animated illustration'
|
||||
assert set(card_copy)==set(scenes),'missing or extra homepage card description'
|
||||
css=(PUBLIC/'assets/editorial/editorial.css').read_text(encoding='utf-8')
|
||||
motion_css=(PUBLIC/'assets/editorial/motion.css').read_text(encoding='utf-8')
|
||||
assert '@media(prefers-reduced-motion:reduce){.flow-link span{animation:none' in css,'missing static motion alternative'
|
||||
assert '@media(prefers-reduced-motion:reduce)' in motion_css and '.motion-overlay{display:none' in motion_css,'motion reduction must stop illustration animation'
|
||||
assert (PUBLIC/'assets/editorial/motion.js').stat().st_size<2000,'animation controller too large'
|
||||
opener=build_opener(ProxyHandler({}))
|
||||
for file in files:
|
||||
rel=file.relative_to(PUBLIC).as_posix();text=file.read_text(encoding='utf-8');p=check(text,rel)
|
||||
@@ -73,6 +79,7 @@ def main():
|
||||
assert scenes[slug]['title'] in text,(rel,'wrong diagram')
|
||||
photo=next((im for im in p.images if im['src'].startswith('/assets/editorial/photo-')),None)
|
||||
assert photo and '520px' in photo.get('sizes',''),(rel,'responsive article photograph')
|
||||
assert text.count('class="motion-visual"')==3,(rel,'animated related illustrations')
|
||||
titles.append(p.title);descriptions.append(p.description)
|
||||
route='/'+('' if rel=='index.html' else rel.removesuffix('index.html'))
|
||||
served=opener.open('http://127.0.0.1:5186'+route,timeout=10)
|
||||
@@ -81,6 +88,9 @@ def main():
|
||||
assert len(set(titles))==len(titles),'duplicate titles'
|
||||
assert len(set(descriptions))==len(descriptions),'duplicate descriptions'
|
||||
home=(PUBLIC/'index.html').read_text(encoding='utf-8')
|
||||
assert home.count('class="motion-visual"')==12,'animated homepage illustrations'
|
||||
for group,expected in [('approach',3),('services',6),('industries',3)]:
|
||||
assert (PUBLIC/group/'index.html').read_text(encoding='utf-8').count('class="motion-visual"')==expected,(group,'animated catalogue illustrations')
|
||||
for slug,description in card_copy.items():
|
||||
assert description in home,(slug,'homepage card description not visible')
|
||||
home_schema=Page(home).ld[0]['@graph']
|
||||
@@ -100,7 +110,14 @@ def main():
|
||||
assert len(locations)==16 and len(set(locations))==16,'sitemap pages'
|
||||
small_photos=list((PUBLIC/'assets/editorial').glob('photo-*-480.webp'))
|
||||
assert len(small_photos)==len(pages) and max(p.stat().st_size for p in small_photos)<50000,'oversized mobile photograph'
|
||||
report={'html_pages':len(files),'article_pages':12,'animated_equipment_scenes':len(scenes),'max_mobile_photo_bytes':max(p.stat().st_size for p in small_photos),'animation_css_bytes':len(css.encode('utf-8')),'unique_titles':True,'unique_descriptions':True,'tariff_prices':expected_prices,'links_and_fragments':'pass','negative_control':negative,'sitemap_urls':len(locations),'checks':checks}
|
||||
illustration_budgets={480:50000,960:130000,1536:250000}
|
||||
illustration_max_bytes={}
|
||||
for width,budget in illustration_budgets.items():
|
||||
variants=[PUBLIC/'assets/editorial'/f'{p["slug"]}-{width}.webp' for p in pages]
|
||||
assert all(item.is_file() for item in variants),(width,'missing illustration size')
|
||||
illustration_max_bytes[width]=max(item.stat().st_size for item in variants)
|
||||
assert illustration_max_bytes[width]<budget,(width,'illustration exceeds size budget')
|
||||
report={'html_pages':len(files),'article_pages':12,'animated_equipment_scenes':len(scenes),'animated_illustrations':len(image_motion),'illustration_max_bytes':illustration_max_bytes,'motion_assets_bytes':len(motion_css.encode('utf-8'))+(PUBLIC/'assets/editorial/motion.js').stat().st_size,'max_mobile_photo_bytes':max(p.stat().st_size for p in small_photos),'animation_css_bytes':len(css.encode('utf-8')),'unique_titles':True,'unique_descriptions':True,'tariff_prices':expected_prices,'links_and_fragments':'pass','negative_control':negative,'sitemap_urls':len(locations),'checks':checks}
|
||||
(ROOT/'verification.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8')
|
||||
print(json.dumps({k:v for k,v in report.items() if k!='checks'},ensure_ascii=False))
|
||||
if __name__=='__main__':main()
|
||||
|
||||
@@ -39,6 +39,8 @@ def main():
|
||||
pages = sorted(PUBLIC.rglob('*.html'))
|
||||
selected = pages + [PUBLIC / 'robots.txt', PUBLIC / 'sitemap.xml']
|
||||
selected += [PUBLIC / 'assets/editorial/editorial.css',
|
||||
PUBLIC / 'assets/editorial/motion.css',
|
||||
PUBLIC / 'assets/editorial/motion.js',
|
||||
PUBLIC / 'assets/editorial/photo-networks-vpn-960.webp',
|
||||
PUBLIC / 'assets/editorial/networks-vpn-480.webp']
|
||||
mismatches = []
|
||||
|
||||
Reference in New Issue
Block a user