Save servaki.online content, generator and release markup, agent=codex
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
# Local credentials, runtime dependencies and backups
|
||||
.deps/
|
||||
__pycache__/
|
||||
release-backups/
|
||||
*.zip
|
||||
*.key
|
||||
*.pem
|
||||
*.crt
|
||||
*.env
|
||||
*.secret
|
||||
*.secret.*
|
||||
*id_rsa*
|
||||
verification.json
|
||||
live-verification.json
|
||||
reference/live-*
|
||||
artwork/photos/
|
||||
artwork/**/contact-sheet.jpg
|
||||
gitea_auth_local.py
|
||||
prepare_gitea_tree.py
|
||||
@@ -1,5 +1,17 @@
|
||||
# servaki.online
|
||||
|
||||
## Версия сайта от 28 сентября 2026
|
||||
|
||||
Здесь сохранены исходники, оригиналы изображений и готовый каталог `public/`,
|
||||
который опубликован на Beget. Главная связывает 12 карточек с профильными статьями;
|
||||
есть 3 каталога, адаптивные изображения, лёгкие схемы работы оборудования и SEO-разметка.
|
||||
Состав сборки, проверки, доступ к хостингу без секретов и порядок отката — в [SITE-BUILD.md](SITE-BUILD.md).
|
||||
|
||||
Актуальные тарифы: «Старт» — 9 000 ₽/мес., «Бизнес» — 21 000 ₽/мес.,
|
||||
«Премиум» — 45 000 ₽/мес. Публикация на Beget выполняется отдельно от Git.
|
||||
|
||||
## Сведения о прежней версии
|
||||
|
||||
**Сайт:** https://servaki.online
|
||||
**Назначение:** IT-поддержка для бизнеса на Юге России — главный хаб компании Сервакисервисы
|
||||
**Стек:** Статический HTML + Nginx (Beget shared hosting)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# servaki.online — расширение лендинга
|
||||
|
||||
Подготовлено и опубликовано 28 сентября 2026 на существующем Beget-хостинге `infogypa`. Сертификат Let's Encrypt заказан повторно и ожидает выпуска со стороны Beget.
|
||||
|
||||
## Состав
|
||||
|
||||
- Главная с 12 тематическими иллюстрациями под описаниями карточек и ссылками на профильные страницы.
|
||||
- 12 статей-лендингов: 3 подхода, 6 услуг, 3 отрасли. 3525 слов основного содержания с FAQ.
|
||||
- Каталоги `/approach/`, `/services/`, `/industries/`.
|
||||
- Автор, дата, хлебные крошки, оглавления, связанные материалы, контакты.
|
||||
- Уникальные title/description/canonical, Open Graph, Article/Service/FAQPage/BreadcrumbList, sitemap, robots.
|
||||
- 12 тематических AI-иллюстраций для карточек главной и 12 тёплых фотореалистичных AI-изображений для внутренних страниц, с WebP-версиями 480/960/1536 px. У каждой статьи своё изображение и подпись о том, что оно создано ИИ без данных и фотографий клиентов.
|
||||
- На страницах услуг — конкретные пояснения о согласовании доступа, ограничении видимости данных и бережной работе в действующем офисе или на объекте. Это описание подхода, а не обещание абсолютной безопасности.
|
||||
- В тексте каждой из 12 статей — тематическая схема работы оборудования: индикаторы приборов и движение сигнала между ними. Схемы состоят из компактных SVG и CSS, не загружают GIF или видео. При настройке «уменьшить движение» индикаторы и путь остаются статичными.
|
||||
- Описания под всеми 12 карточками главной переписаны: задача клиента, способ работы и результат; условия отклика указаны без обещания устранить любой сбой за 15 минут. У трёх каталогов и главной — отдельные поисковые описания.
|
||||
|
||||
## Просмотр и сборка
|
||||
|
||||
Текущий предпросмотр: http://127.0.0.1:5186/ . Использовать HTTP-сервер, не открывать index.html как file://: пути начинаются от корня сайта.
|
||||
|
||||
Python 3.12+, зависимости в `requirements.txt`: Pillow для экспорта изображений, Paramiko для SFTP и cryptography для проверки сертификата. HTML генерируется стандартной библиотекой, Node и npm для этой сборки не нужны.
|
||||
|
||||
```
|
||||
python -m pip install -r requirements.txt
|
||||
python prepare_images.py
|
||||
python prepare_photos.py
|
||||
python build.py
|
||||
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` — сохранённая исходная главная.
|
||||
|
||||
## Фактическая основа
|
||||
|
||||
Внутренние заметки используются только как подтверждение исторического инженерного опыта, без имён заказчиков, адресов, паролей и топологии:
|
||||
|
||||
- fleet-vault/daily/2026-09-28-antosha-fleet-traffic-audit.md — диагностика туннелей по реальному трафику, сохранение настроек и проверка зависимостей.
|
||||
- fleet-vault/memory/vesovaya-bitarov-most-remzona-2026-07-21.md — обследование связи весовой и ремонтной зоны, сопоставление доступности и uptime.
|
||||
- fleet-vault/memory/mpo-inform-site-2026-07-01.md — работа со схемами камер и СКУД, необходимость отделять условные модели от точных монтажных данных.
|
||||
- fleet-vault/projects/controllers-energy-digest-bot.md — Wiren Board/MQTT, показатели энергии, существующие сценарии и отдельно запланированные функции.
|
||||
- fleet-vault/inventory/fleet-hosts.md — опыт серверной виртуализации и контейнеров. Наличие каждого старого сервиса сейчас не заявляется.
|
||||
|
||||
Для HoReCa описана применимость сетевого и серверного опыта, не придуманы гостиничные кейсы, отзывы, гарантированные результаты или количество клиентов. 28 сентября 2026 по прямому указанию владельца цены трёх тарифов увеличены втрое: «Старт» — 9 000 ₽/мес., «Бизнес» — 21 000 ₽/мес., «Премиум» — 45 000 ₽/мес. Состав тарифов не менялся; отклик и срок устранения разделены.
|
||||
|
||||
## SEO и ИИ-поиск
|
||||
|
||||
Основа: доступный HTML-текст, нормальная внутренняя навигация, точные описания, вопросы и ответы, изображения и соответствующая видимому тексту разметка. Специальный файл llms.txt не нужен для показа в AI-функциях Google и не представлен как условие индексации или гарантия цитирования.
|
||||
|
||||
Источники: https://developers.google.com/search/docs/appearance/ai-features и https://developers.google.com/search/docs/appearance/snippet . Попадание в индекс и ответы ИИ не гарантируется. FAQ-разметка описывает видимый FAQ, не обещает расширенный сниппет.
|
||||
|
||||
## Проверки
|
||||
|
||||
`verification.json`: 16 HTML-страниц локально HTTP 200, побайтная сверка, уникальные title и description, H1, JSON-LD, все внутренние ссылки/якоря и изображения, 16 адресов sitemap. На 12 статьях проверены схемы с тремя приборами и двумя путями сигнала; мобильные фото весят не более 38,7 КБ, общий CSS — 12,6 КБ. Отрицательный контроль: намеренно сломанная ссылка на изображение обнаруживается.
|
||||
|
||||
`live-verification.json`: 21 публичный URL проверен по HTTPS с временным отключением проверки старого сертификата; все отдали 200 и байт-в-байт совпали со сборкой. Публичный сертификат на момент проверки ещё истёк (17.09.2026); заявка на бесплатный перевыпуск принята панелью Beget 28.09.2026. До выпуска сертификата браузеры по-прежнему могут показывать предупреждение.
|
||||
|
||||
Браузер: главная и статья на обычном экране и 390×844; горизонтального переполнения нет, responsive images загружаются, оглавление и FAQ работают. Анимированная схема сети визуально проверена на 1280 px и в мобильном окне 390 px: устройства выстраиваются вертикально, путь сигнала сохраняется. Исходная форма на главной остаётся демонстрационной; все новые CTA ведут прямо в Telegram или на телефон, сообщения автоматически не отправлялись.
|
||||
|
||||
## Публикация и откат
|
||||
|
||||
- Боевой каталог подтверждён по SFTP: `/home/i/infogypa/servaki.online/public_html`. Перед публикацией его `index.html` побайтно совпал с `reference/original.html`; новые каталоги `approach`, `services`, `industries`, `assets/editorial` отсутствовали. Семь прежних изображений в `img/` совпали с исходными и не перезаписывались.
|
||||
- `deploy_beget.py audit` сохранил изменяемые исходные файлы в `release-backups/2026-09-28-before-landing/`; дубликат главной размещён на Beget вне публичного каталога: `/home/i/infogypa/servaki.online/backup-2026-09-28-before-landing/`.
|
||||
- `deploy_beget.py deploy` загрузил и сверил SHA-256 всех 98 файлов. Сначала были загружены графика и внутренние страницы, главная заменена последней. Скрипт фиксирует SSH-отпечаток хостинга и не перезаписывает посторонние файлы.
|
||||
- `deploy_beget.py reprice` меняет только главную: перед загрузкой проверяет, что шесть изменений касаются исключительно трёх видимых цен и трёх цен в JSON-LD. Предыдущая главная сохраняется локально в `release-backups/2026-09-28-before-tripled-prices/` и на Beget вне публичного каталога в `backup-2026-09-28-before-tripled-prices/`.
|
||||
- `.htaccess`, DNS, почта и другие сайты не менялись. Исходный репозиторий сайта — `german/site-servaki-online` в Gitea; в `servaki-monitor` есть чужие незакоммиченные изменения, они не затрагивались. Публикация выполнялась напрямую на существующий Beget-хостинг, без автоматического деплоя из Gitea.
|
||||
- Для отката вернуть из резервной копии прежний `index.html`; у нового релиза были добавлены `robots.txt`, `sitemap.xml` и каталоги `approach`, `services`, `industries`, `assets/editorial`. Удалять общие каталоги `img` и другие сайты нельзя.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,158 @@
|
||||
"""Static HTML build; no JavaScript required for article content or navigation."""
|
||||
from pathlib import Path
|
||||
import json, html, re, shutil, math
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
OUT=ROOT/'public'
|
||||
BASE='https://servaki.online'
|
||||
DATE='2026-09-28'
|
||||
PAGES=json.loads((ROOT/'content/pages.json').read_text(encoding='utf-8'))
|
||||
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'))
|
||||
CARD_COPY=json.loads((ROOT/'content/card-copy.json').read_text(encoding='utf-8'))
|
||||
GROUPS={'Подход':('approach','Как мы работаем','Поддержка с понятной ответственностью'),'Услуги':('services','IT-услуги для бизнеса','От рабочего места до инфраструктуры'),'Отрасли':('industries','IT-решения для отраслей','Учитываем, как работает ваш бизнес')}
|
||||
HUB_DESCRIPTIONS={
|
||||
'Подход':'Как servaki.online принимает обращения, поддерживает удалённые объекты и выстраивает абонентское IT-обслуживание бизнеса на КМВ и Юге России.',
|
||||
'Услуги':'Удалённая помощь, сети и VPN, мониторинг, видеонаблюдение, серверы и автоматизация для бизнеса на КМВ и Юге России. Состав работ и порядок поддержки.',
|
||||
'Отрасли':'IT-поддержка гостиниц и кафе, сельского хозяйства и строительных объектов: связь, рабочие системы, камеры и удалённый контроль на Юге России.'
|
||||
}
|
||||
ICON_PATHS={
|
||||
'ticket':'<path d="M4 6h16v5a2 2 0 0 0 0 4v5H4v-5a2 2 0 0 0 0-4V6Z"/><path d="M12 7v2m0 3v2m0 3v2"/>',
|
||||
'laptop':'<rect x="4" y="4" width="16" height="12" rx="2"/><path d="M2 20h20M8 20l1-4m7 4-1-4"/>',
|
||||
'check':'<circle cx="12" cy="12" r="9"/><path d="m8 12 2.5 2.5L16 9"/>',
|
||||
'sensor':'<rect x="7" y="3" width="10" height="18" rx="3"/><circle cx="12" cy="11" r="2"/><path d="M10 17h4"/>',
|
||||
'router':'<rect x="3" y="10" width="18" height="9" rx="2"/><path d="M7 10V5m10 5V5M6 15h.01M10 15h.01M14 15h4"/>',
|
||||
'settings':'<circle cx="12" cy="12" r="3"/><path d="M12 2v3m0 14v3M2 12h3m14 0h3M4.9 4.9 7 7m10 10 2.1 2.1M19.1 4.9 17 7M7 17l-2.1 2.1"/>',
|
||||
'shield':'<path d="M12 2 4 5v6c0 5 3 8 8 11 5-3 8-6 8-11V5l-8-3Z"/><path d="m9 12 2 2 4-4"/>',
|
||||
'user':'<circle cx="12" cy="8" r="4"/><path d="M4 21c0-4 3-7 8-7s8 3 8 7"/>',
|
||||
'server':'<rect x="4" y="3" width="16" height="8" rx="2"/><rect x="4" y="13" width="16" height="8" rx="2"/><path d="M8 7h.01M8 17h.01M12 7h5M12 17h5"/>',
|
||||
'bell':'<path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9Z"/><path d="M10 21h4"/>',
|
||||
'camera':'<rect x="3" y="6" width="14" height="12" rx="2"/><circle cx="10" cy="12" r="3"/><path d="m17 10 4-2v8l-4-2"/>',
|
||||
'database':'<ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 2 4 3 8 3s8-1 8-3V5M4 12c0 2 4 3 8 3s8-1 8-3"/>',
|
||||
'phone':'<rect x="6" y="2" width="12" height="20" rx="3"/><path d="M10 5h4m-3 14h2"/>'
|
||||
}
|
||||
def esc(t): return html.escape(str(t),quote=True)
|
||||
def write(rel,text):
|
||||
p=OUT/rel;p.parent.mkdir(parents=True,exist_ok=True);p.write_text(text,encoding='utf-8')
|
||||
def url(p):return '/'+p['path']+'/'
|
||||
def image(p,lazy=True,small=False):
|
||||
slug=p['slug']; sizes='(max-width: 600px) 100vw, 380px' if small else '(max-width: 1248px) 100vw, 1200px'
|
||||
return f'<img src="/assets/editorial/{slug}-960.webp" srcset="/assets/editorial/{slug}-480.webp 480w, /assets/editorial/{slug}-960.webp 960w, /assets/editorial/{slug}-1536.webp 1536w" sizes="{sizes}" width="1536" height="1024" alt="{esc(p["alt"])}" {"loading=\"lazy\"" if lazy else "fetchpriority=\"high\""} decoding="async">'
|
||||
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 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>'''
|
||||
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>'
|
||||
def flow_diagram(p):
|
||||
scene=FLOWS[p['slug']]
|
||||
devices=[]
|
||||
for i,(icon,label) in enumerate(scene['nodes']):
|
||||
if i:devices.append('<div class="flow-link" aria-hidden="true"><span></span></div>')
|
||||
glyph=f'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{ICON_PATHS[icon]}</svg>'
|
||||
devices.append(f'<div class="flow-device"><div class="flow-device-top"><span class="flow-icon">{glyph}</span><span class="flow-led" aria-hidden="true"></span></div><strong>{esc(label)}</strong><span class="flow-device-index">0{i+1}</span></div>')
|
||||
return f'<figure class="system-flow" id="equipment-flow"><figcaption><span class="flow-kicker">ПРИНЦИП РАБОТЫ</span><strong>{esc(scene["title"])}</strong><small>Условная схема движения сигнала, без реальных данных и настроек клиентов.</small></figcaption><div class="flow-track">{"".join(devices)}</div></figure>'
|
||||
def article(p):
|
||||
canonical=BASE+url(p);img=BASE+'/assets/editorial/'+p['slug']+'-1536.webp';g=GROUPS[p['group']]
|
||||
article_id=canonical+'#article'
|
||||
graph=[ORG,{'@type':'WebPage','@id':canonical,'url':canonical,'name':p['title'],'description':p['description'],'inLanguage':'ru-RU','isPartOf':{'@id':BASE+'/#website'},'mainEntity':{'@id':article_id}}, {'@type':'Article','@id':article_id,'headline':p['title'],'description':p['description'],'image':[img],'author':{'@id':BASE+'/#organization'},'publisher':{'@id':BASE+'/#organization'},'datePublished':DATE,'dateModified':DATE,'inLanguage':'ru-RU','mainEntityOfPage':{'@id':canonical}}, {'@type':'BreadcrumbList','itemListElement':[{'@type':'ListItem','position':i+1,'name':n,'item':BASE+u} for i,(n,u) in enumerate([('Главная','/'),(g[1],'/'+g[0]+'/'),(p['card'],url(p))])]}, {'@type':'FAQPage','@id':canonical+'#faq','mainEntity':[{'@type':'Question','name':q,'acceptedAnswer':{'@type':'Answer','text':a}} for q,a in p['faq']]}]
|
||||
if p['group'] in ['Услуги','Отрасли']:
|
||||
graph.append({'@type':'Service','@id':canonical+'#service','name':p['title'],'description':p['lead'],'provider':{'@id':BASE+'/#organization'},'areaServed':['Ставропольский край','Кавказские Минеральные Воды','Юг России'],'url':canonical})
|
||||
toc=''.join(f'<li><a href="#part-{i}">{esc(h)}</a></li>'+('<li><a href="#equipment-flow">Схема работы оборудования</a></li>' if i==1 else '') for i,(h,_) in enumerate(p['sections'],1))
|
||||
sections=''.join(f'<section id="part-{i}"><h2>{esc(h)}</h2><p>{esc(t)}</p></section>'+ (flow_diagram(p) if i==1 else '') for i,(h,t) in enumerate(p['sections'],1))
|
||||
strengths=''.join(f'<div class="strength"><span>0{i} / НАШ ПОДХОД</span>{esc(t)}</div>' for i,t in enumerate(p['strengths'],1))
|
||||
faq=''.join(f'<details><summary>{esc(q)}</summary><p>{esc(a)}</p></details>' for q,a in p['faq'])
|
||||
minutes=max(3,math.ceil(len(' '.join(t for _,t in p['sections']).split())/170))
|
||||
return head(p['title'],p['description'],canonical,img,graph)+NAV+f'''<main id="content" class="wrap"><article><header class="intro">{crumbs([('Главная','/'),(g[1],'/'+g[0]+'/'),(p['card'],None)])}<span class="eyebrow">{esc(p['group'])} / {esc(p['label'])}</span><h1>{esc(p['title'])}</h1><p class="lead">{esc(p['lead'])}</p><div class="actions"><a class="button" href="https://t.me/servaki_online">Обсудить задачу ↗</a><a class="button secondary" href="#part-1">Как это работает ↓</a></div><p class="meta">Команда servaki.online · <time datetime="{DATE}">28 сентября 2026</time> · {minutes} мин чтения</p></header><figure class="hero-figure">{image(p,lazy=False)}<figcaption>{esc(p['label'])} · Тематическая AI-иллюстрация</figcaption></figure><div class="strengths">{strengths}</div><div class="reading-grid"><aside class="toc"><strong>На этой странице</strong><ol>{toc}<li><a href="#faq">Вопросы и ответы</a></li><li><a href="#discuss">Обсудить задачу</a></li></ol></aside><div class="article-body">{sections}<section class="faq" id="faq"><h2>Вопросы и ответы</h2>{faq}</section></div></div></article><section class="consult" id="discuss"><span class="eyebrow">Следующий шаг</span><h2>Расскажите, что должно работать</h2><p>Укажите ваш объект, существующее оборудование и задачу. Разберём исходные условия, уточним необходимые доступы и предложим порядок работ. Стоимость и сроки согласуем до начала.</p><div class="actions"><a class="button" href="https://t.me/servaki_online">Написать в Telegram ↗</a><a class="button secondary" href="tel:+79620151414">+7 962 015-14-14</a><a class="button secondary" href="/#pricing">Тарифы поддержки</a></div></section><section class="related"><h2>Связанные решения</h2><div class="related-grid">{''.join(related_card(BY_ID[s]) for s in p['related'])}</div></section></main>'''+FOOT
|
||||
def home(original):
|
||||
result=original
|
||||
for p in PAGES:
|
||||
pattern=r'<article\b[^>]*>\s*(?:(?!</article>).)*<h3>'+re.escape(p['card'])+r'</h3>(?:(?!</article>).)*</article>'
|
||||
match=re.search(pattern,result,re.S)
|
||||
if not match:raise RuntimeError('Missing homepage card: '+p['card'])
|
||||
block=match.group()
|
||||
heading_end=block.find('</h3>')+len('</h3>')
|
||||
before,after=block[:heading_end],block[heading_end:]
|
||||
after,count=re.subn(r'(<p\b[^>]*>).*?(</p>)',lambda m:m.group(1)+esc(CARD_COPY[p['slug']])+m.group(2),after,count=1,flags=re.S)
|
||||
if count!=1:raise RuntimeError('Missing homepage description: '+p['card'])
|
||||
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>'
|
||||
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=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)
|
||||
result=re.sub(r'(<meta (?:name|property)="(?:og:title|twitter:title)" content=")[^"]*(")',lambda m:m.group(1)+'IT-поддержка бизнеса на КМВ и Юге России — servaki.online'+m.group(2),result)
|
||||
result=re.sub(r'(<meta (?:name|property)="twitter:description" content=")[^"]*(")',lambda m:m.group(1)+'Рабочие места, сети и VPN, серверы, видеонаблюдение и удалённые объекты. Услуги и тарифы servaki.online.'+m.group(2),result)
|
||||
result=result.replace('https://servaki.online/og-cover.png',BASE+'/assets/editorial/remote-sites-1536.webp')
|
||||
# Make essential content crawlable and visible even if the original reveal script fails.
|
||||
result=result.replace('<li><a href="#services">Услуги</a></li>','<li><a href="/services/">Все услуги</a></li>')
|
||||
result=result.replace('<li><a href="#industries">Отрасли</a></li>','<li><a href="/industries/">Решения для отраслей</a></li>')
|
||||
result=result.replace('<li><a href="#about">О нас</a></li>','<li><a href="/approach/">Наш подход</a></li>')
|
||||
# Connect process/pricing context to detailed pages.
|
||||
result=result.replace('Простой и прозрачный процесс — от вашего сообщения до постоянного мониторинга.','Простой и прозрачный процесс — от вашего сообщения до постоянного мониторинга. <a class="editorial-entry" href="/approach/rapid-response/">Как устроена поддержка →</a>')
|
||||
result=result.replace('<h2 class="sec-title reveal d1" id="pricing-h">Прозрачные подписки</h2>','<h2 class="sec-title reveal d1" id="pricing-h">Прозрачные подписки</h2><p><a class="editorial-entry" href="/approach/it-subscription/">Что входит в обслуживание и как выбрать тариф →</a></p>')
|
||||
for old in (3000, 7000, 15000):
|
||||
structured=f'"price": "{old}"'
|
||||
visible=f'<span class="amount">{old}</span>'
|
||||
if result.count(structured)!=1 or result.count(visible)!=1:
|
||||
raise RuntimeError(f'Expected one visible and one structured price for {old}')
|
||||
new=old*3
|
||||
result=result.replace(structured,f'"price": "{new}"')
|
||||
result=result.replace(visible,f'<span class="amount">{new:,}</span>'.replace(',', ' '))
|
||||
return result
|
||||
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')
|
||||
for p in PAGES:
|
||||
for width in [480,960,1536]:
|
||||
img=ROOT/'assets'/f'{p["slug"]}-{width}.webp'
|
||||
if img.exists():shutil.copy2(img,dest/img.name)
|
||||
photo=ROOT/'assets'/f'photo-{p["slug"]}-{width}.webp'
|
||||
if not photo.exists():raise RuntimeError('Missing warm photograph: '+str(photo))
|
||||
shutil.copy2(photo,dest/photo.name)
|
||||
page=article(p)
|
||||
page=page.replace(image(p,lazy=False),photo_image(p),1)
|
||||
page=page.replace(f'/assets/editorial/{p["slug"]}-1536.webp',f'/assets/editorial/photo-{p["slug"]}-1536.webp')
|
||||
page=page.replace(' · Тематическая AI-иллюстрация',' · Изображение создано ИИ, без данных и фотографий клиентов',1)
|
||||
page=page.replace('<article><header class="intro">','<article><div class="article-top"><header class="intro">',1)
|
||||
page=page.replace('</figure><div class="strengths">','</figure></div><div class="strengths">',1)
|
||||
note=f'<aside class="privacy-note" aria-label="Бережный доступ"><span>БЕРЕЖНО К ДАННЫМ</span><p>{esc(PRIVACY[p["slug"]])}</p></aside>'
|
||||
page=page.replace('</figure></div><div class="strengths">','</figure></div>'+note+'<div class="strengths">',1)
|
||||
write(p['path']+'/index.html',page)
|
||||
original=(ROOT/'reference/original.html').read_text(encoding='utf-8')
|
||||
write('index.html',home(original))
|
||||
for group,(slug,title,label) in GROUPS.items():
|
||||
desc=HUB_DESCRIPTIONS[group]
|
||||
write(slug+'/index.html',head(title,desc,BASE+'/'+slug+'/',BASE+'/assets/editorial/remote-sites-1536.webp',[ORG,{'@type':'CollectionPage','name':title,'url':BASE+'/'+slug+'/','inLanguage':'ru-RU'}])+NAV+f'<main class="wrap" id="content"><header class="intro hub-intro">{crumbs([("Главная","/"),(title,None)])}<span class="eyebrow">{label}</span><h1>{title}</h1><p class="lead">{esc(desc)}</p></header><div class="hub-grid">'+''.join(related_card(p) for p in PAGES if p['group']==group)+'</div></main>'+FOOT)
|
||||
# Preserve existing URLs when a previous sitemap is available.
|
||||
locations={BASE+'/',*[BASE+url(p) for p in PAGES],*[BASE+'/'+v[0]+'/' for v in GROUPS.values()]}
|
||||
previous=ROOT/'reference/sitemap.xml'
|
||||
if previous.exists():
|
||||
try:locations.update(e.text for e in ET.fromstring(previous.read_bytes()).iter() if e.tag.endswith('}loc') and e.text.startswith(BASE+'/'))
|
||||
except ET.ParseError:pass
|
||||
write('sitemap.xml','<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'+''.join(f'<url><loc>{esc(u)}</loc></url>' for u in sorted(locations))+'</urlset>')
|
||||
robots=ROOT/'reference/robots.txt'
|
||||
r=robots.read_text(encoding='utf-8') if robots.exists() else 'User-agent: *\nAllow: /\n'
|
||||
if '<html' in r.lower():raise RuntimeError('robots reference is HTML')
|
||||
if 'Sitemap: '+BASE+'/sitemap.xml' not in r:r+='\nSitemap: '+BASE+'/sitemap.xml\n'
|
||||
write('robots.txt',r)
|
||||
print(f'Built {len(PAGES)} articles, 3 hubs, updated homepage, sitemap and robots.')
|
||||
if __name__=='__main__':build()
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"rapid-response": "При сбое связи, компьютера или сервиса принимаем заявку, оцениваем влияние на работу и начинаем диагностику. Отклик от 15 минут зависит от тарифа; срок устранения определяем после выяснения причины.",
|
||||
"remote-sites": "Связываем фермы, весовые, склады и горные объекты с офисом. Проверяем канал, питание и доступ к нужным системам, чтобы удалённая площадка оставалась управляемой.",
|
||||
"it-subscription": "Обслуживание рабочих мест и инфраструктуры по понятному ежемесячному плану: состав работ, ответственные и время отклика согласованы заранее. Помогаем не копить сбои до остановки работы.",
|
||||
"remote-desktop": "Помогаем сотруднику с компьютером, программой или принтером через согласованный сеанс. Сначала уточняем задачу, затем проверяем результат вместе с пользователем без лишнего доступа к данным.",
|
||||
"networks-vpn": "Строим связь между офисом, складом и удалённым объектом через защищённые соединения. Настраиваем маршруты и права доступа, проверяем прохождение трафика до рабочих сервисов.",
|
||||
"monitoring-security": "Следим за доступностью важных сервисов и сообщаем ответственным о сбоях. Состав мониторинга, резервное копирование и уведомления подбираем под инфраструктуру и порядок работы команды.",
|
||||
"video-surveillance": "Подбираем камеры, запись и удалённый просмотр под задачи объекта. Учитываем зоны обзора, питание, хранение архива и права доступа, чтобы система помогала в работе.",
|
||||
"servers-hosting": "Размещаем и сопровождаем серверы для рабочих приложений, чатов и внутренних сервисов. Контролируем доступы, обновления и резервные копии, согласуем перенос до начала работ.",
|
||||
"automation": "Соединяем датчики, учётные системы и уведомления в понятный рабочий процесс. Боты сообщают о событиях и собирают данные; управляющие команды доступны согласованным сотрудникам.",
|
||||
"horeca": "Для гостиниц и кафе проектируем разделение гостевого Wi-Fi и рабочих систем, помогаем с кассами и служебными сервисами. Доступ к данным гостей определяется правилами заведения.",
|
||||
"agriculture": "На фермах и складах связываем датчики, весовые, камеры и удалённые площадки. Проверяем связь и питание, чтобы ответственные получали нужные показатели и понимали причину сбоя.",
|
||||
"construction": "Организуем временные сети, рабочие места и камеры на стройплощадке. Учитываем перенос оборудования по этапам работ, питание, схему доступа и передачу настроек."
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"rapid-response": {"title": "От обращения до проверки результата", "nodes": [["ticket", "Заявка"], ["laptop", "Диагностика"], ["check", "Проверка"]]},
|
||||
"remote-sites": {"title": "Связь с удалённой площадкой", "nodes": [["sensor", "Объект"], ["router", "Канал связи"], ["laptop", "Инженер"]]},
|
||||
"it-subscription": {"title": "Как работает сопровождение", "nodes": [["laptop", "Рабочее место"], ["settings", "Поддержка"], ["check", "Отчёт"]]},
|
||||
"remote-desktop": {"title": "Помощь с рабочего места", "nodes": [["laptop", "Компьютер"], ["shield", "Согласованный сеанс"], ["user", "Специалист"]]},
|
||||
"networks-vpn": {"title": "Как проходит трафик между площадками", "nodes": [["router", "Роутер объекта"], ["shield", "Защищённый канал"], ["server", "Ресурс компании"]]},
|
||||
"monitoring-security": {"title": "От показателя до уведомления", "nodes": [["sensor", "Показатель"], ["server", "Мониторинг"], ["bell", "Уведомление"]]},
|
||||
"video-surveillance": {"title": "Как работает видеонаблюдение", "nodes": [["camera", "Камера"], ["server", "Локальная запись"], ["laptop", "Просмотр"]]},
|
||||
"servers-hosting": {"title": "Работа и восстановление сервера", "nodes": [["server", "Сервер"], ["database", "Резервная копия"], ["check", "Восстановление"]]},
|
||||
"automation": {"title": "От прибора до нужного действия", "nodes": [["sensor", "Прибор"], ["settings", "Контроллер"], ["phone", "Уведомление"]]},
|
||||
"horeca": {"title": "Связь в гостевом доме или кафе", "nodes": [["router", "Гостевая сеть"], ["shield", "Разделение"], ["server", "Рабочий сервис"]]},
|
||||
"agriculture": {"title": "Показания с удалённого объекта", "nodes": [["sensor", "Датчик"], ["router", "Связь"], ["phone", "Ответственный"]]},
|
||||
"construction": {"title": "Система на строительной площадке", "nodes": [["camera", "Камера"], ["router", "Сеть объекта"], ["laptop", "Штаб"]]}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
[
|
||||
{
|
||||
"slug": "rapid-response",
|
||||
"path": "artwork/rapid-response.png"
|
||||
},
|
||||
{
|
||||
"slug": "remote-sites",
|
||||
"path": "artwork/remote-sites.png"
|
||||
},
|
||||
{
|
||||
"slug": "it-subscription",
|
||||
"path": "artwork/it-subscription.png"
|
||||
},
|
||||
{
|
||||
"slug": "remote-desktop",
|
||||
"path": "artwork/remote-desktop.png"
|
||||
},
|
||||
{
|
||||
"slug": "networks-vpn",
|
||||
"path": "artwork/networks-vpn.png"
|
||||
},
|
||||
{
|
||||
"slug": "monitoring-security",
|
||||
"path": "artwork/monitoring-security.png"
|
||||
},
|
||||
{
|
||||
"slug": "video-surveillance",
|
||||
"path": "artwork/video-surveillance.png"
|
||||
},
|
||||
{
|
||||
"slug": "servers-hosting",
|
||||
"path": "artwork/servers-hosting.png"
|
||||
},
|
||||
{
|
||||
"slug": "automation",
|
||||
"path": "artwork/automation.png"
|
||||
},
|
||||
{
|
||||
"slug": "horeca",
|
||||
"path": "artwork/horeca.png"
|
||||
},
|
||||
{
|
||||
"slug": "agriculture",
|
||||
"path": "artwork/agriculture.png"
|
||||
},
|
||||
{
|
||||
"slug": "construction",
|
||||
"path": "artwork/construction.png"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"mode": "built-in image_gen",
|
||||
"style": "Create a polished wide 1536x1024 editorial 3D illustration for servaki.online, an IT support company in southern Russia. Unified visual style: near-black navy #05070f, midnight blue #0c1326, restrained luminous cyan #22d3ee, blue #3b82f6 and violet #8b5cf6, subtle starfield and orbital curves, physically convincing equipment and architectural miniature details, cinematic soft light, premium sober B2B feel, clear recognizable subject, generous dark margins. No text, no letters, no logos, no watermark, no split panels. This is conceptual service illustration, not a photograph of a real client. Subject: ",
|
||||
"subjects": [
|
||||
[
|
||||
"rapid-response",
|
||||
"An IT support operations desk with a single luminous circular time dial and a laptop showing abstract incident signals; a cyan connection arc arriving at a distant small office, calm speed and help."
|
||||
],
|
||||
[
|
||||
"remote-sites",
|
||||
"A Caucasus mountain valley miniature with a remote farm, warehouse and mountain lodge, connected by thin cyan network arcs to one central router. Emphasize geographical distance and reliable connection."
|
||||
],
|
||||
[
|
||||
"it-subscription",
|
||||
"An orderly modern small-business IT workspace, laptop, compact server and router arranged on three matching modular platforms along a calm orbital ring. Visual concept predictable managed support, no money symbols or text."
|
||||
],
|
||||
[
|
||||
"remote-desktop",
|
||||
"An elegant office computer monitor, keyboard and small desktop printer, with a small distant support console joined by a cyan arc. Recognizable business desktop assistance."
|
||||
],
|
||||
[
|
||||
"networks-vpn",
|
||||
"A detailed router and network switch at the heart of three architectural miniature offices on separate platforms linked by luminous cyan cables and restrained orbital arcs."
|
||||
],
|
||||
[
|
||||
"monitoring-security",
|
||||
"A compact server, secure backup storage and a floating abstract monitoring glass panel with subtle graphs, small status lights and a single elegant protective shield shape, no text."
|
||||
],
|
||||
[
|
||||
"video-surveillance",
|
||||
"A realistic bullet IP security camera above a miniature warehouse yard and loading gate, a neat video recorder in the foreground, restrained translucent viewing cone showing protected area, night scene."
|
||||
],
|
||||
[
|
||||
"servers-hosting",
|
||||
"A detailed compact server rack with restrained cyan status lights, removable storage units and a beautiful thin orbital backup path around it. Equipment is physically plausible, premium dark studio scene."
|
||||
],
|
||||
[
|
||||
"automation",
|
||||
"A DIN rail industrial automation controller, a small energy measurement sensor and a smartphone showing only abstract colored event cards linked by precise cyan light trails. Realistic technical components, no words."
|
||||
],
|
||||
[
|
||||
"horeca",
|
||||
"A warm elegant restaurant in a Caucasus mountain boutique hotel, reception counter, tables and a discreet ceiling Wi-Fi access point, subtle cyan connection curves. Warm interior lighting balanced by midnight blue mountains outside."
|
||||
],
|
||||
[
|
||||
"agriculture",
|
||||
"A Caucasus agricultural farm miniature at blue hour: grain silos, a truck weighbridge, a barn and crops; tiny sensors and restrained cyan network lines linking the facilities. Concrete agricultural operations, no futuristic fantasy."
|
||||
],
|
||||
[
|
||||
"construction",
|
||||
"A carefully detailed construction site miniature: one tower crane, a partially completed building, a modular site office, materials storage and a security camera on a pole. Mountain silhouettes, blue hour, cyan network arc and a few warm work lights."
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
[
|
||||
{
|
||||
"slug":"rapid-response","group":"Подход","path":"approach/rapid-response","card":"Реакция от 15 минут","title":"Оперативная IT-поддержка бизнеса: как устроен отклик","description":"Как servaki.online принимает IT-заявки, определяет приоритет и подключается к решению. Условия отклика, диагностика, отчёт и поддержка бизнеса на КМВ.","lead":"Когда остановилась работа, важен понятный следующий шаг. Принимаем обращение, оцениваем влияние сбоя на бизнес и согласуем порядок восстановления. Время отклика закрепляем в выбранном тарифе.","label":"Помощь начинается с понимания задачи","alt":"Рабочее место IT-поддержки и световая шкала времени в тёмно-синей палитре",
|
||||
"sections":[
|
||||
["Что означает реакция от 15 минут","Это время начала обработки обращения для соответствующего уровня обслуживания, а не обещание устранить любую неисправность за четверть часа. На главной представлены разные условия: у тарифа «Старт» отклик до двух часов, у «Бизнес» — 30 минут, у «Премиум» — 15 минут. Рабочее время поддержки, каналы связи и порядок аварийной эскалации фиксируем при подключении."],
|
||||
["Сначала восстанавливаем работу","В заявке полезно указать, что перестало работать, когда начался сбой и скольких сотрудников он затронул. Остановка связи на объекте и настройка нового принтера требуют разного приоритета. Проверяем доступность, последние изменения и сообщения об ошибках. Затем предлагаем действие: восстановить соединение, вернуть рабочую конфигурацию, переключить резерв или подготовить выезд."],
|
||||
["Наш опыт: проверка всей цепочки","В работе с распределёнными объектами мы сталкиваемся с ситуациями, когда роутер доступен, а нужный сервис не открывается. Поэтому проверяем не только индикатор подключения, но и прохождение данных, DNS и доступ к целевому приложению. Такой подход помогает отличить проблему провайдера от неисправности локальной сети или самого сервиса."],
|
||||
["Результат, который можно проверить","После работ объясняем причину, перечисляем изменения и вместе с сотрудником проверяем исходную задачу: открылся ли документ, печатает ли принтер, доступна ли камера. Если требуется наблюдение, назначаем конкретную проверку. Для повторяющихся сбоев предлагаем план устранения причины, а не бесконечную серию перезапусков."],
|
||||
["Когда понадобится человек на месте","Удалённое подключение не заменяет ремонт питания, кабеля или неисправного оборудования. В таких случаях готовим понятное задание для местного сотрудника или выезда: что проверить, какое устройство заменить, какие признаки зафиксировать. Это позволяет использовать время на объекте предметно."]
|
||||
],
|
||||
"strengths":["Приоритет по влиянию на работу бизнеса","Проверка результата на реальной задаче сотрудника","Документирование изменений и дальнейших действий"],
|
||||
"faq":[["За 15 минут будет устранён любой сбой?","Нет. Отклик и срок решения — разные показатели. Время восстановления зависит от причины, доступа, оборудования и сторонних сервисов."],["Что отправить в первой заявке?","Название объекта, краткое описание проблемы, время её появления и контакт сотрудника на месте. Пароли в обычное сообщение отправлять не нужно."],["Работает ли поддержка круглосуточно?","Круглосуточный автоматический мониторинг и дежурство инженера — разные услуги. Часы поддержки и аварийный порядок согласуются отдельно."]],"related":["remote-desktop","monitoring-security","it-subscription"]
|
||||
},
|
||||
{
|
||||
"slug":"remote-sites","group":"Подход","path":"approach/remote-sites","card":"Работаем с удалёнными точками","title":"IT-поддержка удалённых объектов: фермы, склады и базы","description":"Связь и IT-поддержка удалённых объектов на Юге России: фермы, весовые, склады и базы. Диагностика каналов, резервирование и контролируемый доступ.","lead":"Между офисом и объектом могут быть десятки километров и горный перевал. Строим поддержку вокруг доступности связи, удалённого управления и понятных действий для человека на месте.","label":"Расстояние учтено в архитектуре","alt":"Ферма, склад и горная база связаны голубыми линиями сети в кавказской долине",
|
||||
"sections":[["Почему удалённой точке нужен свой подход","За пределами города на работу IT влияют питание, радиомосты, качество мобильной связи и ограниченная возможность быстро заменить оборудование. Обычная схема офисной поддержки здесь недостаточна. Начинаем с карты зависимостей: откуда приходит интернет, что запитано от одного блока, где заканчивается ответственность провайдера и кто может помочь на площадке."],["Соединяем объект с офисом","Настраиваем защищённый доступ к нужным ресурсам: учётной системе, камерам, контроллеру или рабочему месту. Разделяем служебные устройства и гостевые подключения. Для резервного канала проверяем реальное переключение и возвращение на основной, чтобы запасное соединение существовало не только в настройках."],["Практика: весовая и удалённая ремонтная зона","В нашей практике есть диагностика связи между весовой и ремонтной зоной через радиомост. Для поиска причины наблюдали доступность устройств по обе стороны моста и время работы маршрутизатора. Так можно отделить перезагрузку оборудования от потери связи на конкретном участке и подготовить адресную проверку питания или радиолинии."],["Наблюдаем за полезной работой","Сам факт подключения VPN не доказывает, что сотрудник откроет приложение. Проверяем обмен данными, доступность нужного сервиса и изменения после переключения канала. Для камер отдельно проверяем живое видео и архив; для контроллеров — свежесть измерений. Порог и получателей уведомлений выбираем под задачу объекта."],["Что получит ответственный за площадку","Фиксируем состав оборудования, назначение каналов, порядок перезапуска и контакты для эскалации. Готовим короткую инструкцию для типовых ситуаций. Если восстановление требует физического вмешательства, человек на месте получает последовательность действий, а не просьбу наугад отключить всё питание."]],
|
||||
"strengths":["Опыт диагностики радиомостов и удалённых площадок","Контроль реального обмена данными","План действий при потере связи и питания"],"faq":[["Можно ли подключить объект с мобильным интернетом?","Часто да. Сначала оцениваем покрытие, ограничения оператора и входящий доступ. Конкретную схему выбираем после проверки связи на площадке."],["Нужен ли постоянный специалист на месте?","Для повседневной поддержки обычно достаточно ответственного сотрудника. Монтаж, замена оборудования и физические неисправности требуют присутствия человека."],["Можно ли сохранить имеющийся роутер?","Да, если он поддерживает нужные функции и работает стабильно. Замены предлагаем по результатам обследования."]],"related":["networks-vpn","agriculture","monitoring-security"]
|
||||
},
|
||||
{
|
||||
"slug":"it-subscription","group":"Подход","path":"approach/it-subscription","card":"Доступные подписки","title":"Абонентское IT-обслуживание бизнеса: состав и условия","description":"IT-обслуживание по подписке на КМВ и Юге России. Что входит в поддержку, как выбрать тариф и согласовать отклик, выезды и дополнительные работы.","lead":"Подписка помогает планировать IT-расходы и закрепить ответственность за поддержку. Сначала описываем вашу инфраструктуру и нужный уровень обслуживания, затем согласуем состав работ.","label":"Понятный объём. Понятная ответственность.","alt":"Компьютер, сервер и роутер на упорядоченных модульных платформах",
|
||||
"sections":[["Что стоит за ежемесячной оплатой","Абонентское обслуживание — это согласованный набор задач: помощь сотрудникам, сопровождение оборудования, профилактика и отчётность. Фиксированная стоимость имеет смысл только вместе с понятными границами. До старта определяем количество рабочих мест, перечень серверов и объектов, часы поддержки и приоритет критичных сервисов."],["Как выбрать подходящий уровень","На главной опубликованы тарифы «Старт», «Бизнес» и «Премиум». Сопоставлять их лучше с задачами, а не только с числом компьютеров. Три рабочих места с критичной учётной системой могут требовать больше внимания, чем десять обычных офисных ПК. Несколько филиалов, камеры и резервные серверы учитываем при обсуждении состава обслуживания."],["Сильная сторона: видим связи между системами","В нашей практике сети, серверы, видеонаблюдение и контроллеры работают совместно. Неисправность питания или одного маршрутизатора может проявиться сразу в нескольких сервисах. Поэтому при подключении описываем зависимости и общие точки отказа. Это даёт основу для профилактики и осмысленного плана развития."],["Что нужно согласовать отдельно","Закупка оборудования, лицензии, монтаж, перенос сервисов и крупные изменения могут выходить за рамки регулярной поддержки. Такие задачи предварительно оцениваем. Порядок выездов, аварийные обращения вне рабочего времени и ограничения тарифа отражаем в договорённостях, чтобы у обеих сторон было одинаковое понимание услуги."],["Как начинается обслуживание","Собираем перечень устройств, проверяем доступы и резервные копии, фиксируем известные проблемы. Приоритет отдаём рискам остановки бизнеса. Затем согласуем канал обращений и форму отчёта. По итогам сопровождения показываем выполненные работы и рекомендации, которые можно включить в следующий бюджет."]],
|
||||
"strengths":["Состав обслуживания согласован до старта","Единая картина сети, серверов и рабочих мест","Дополнительные работы оцениваются заранее"],"faq":[["Какая цена будет у нашей компании?","Ориентиры опубликованы на главной в разделе тарифов. Итог зависит от состава оборудования, числа объектов, часов поддержки и необходимых выездов."],["Можно ли начать с небольшого объёма?","Да. Можно согласовать поддержку выбранных рабочих мест или сервисов и затем расширить состав обслуживания."],["Лицензии и оборудование включены?","Это нужно проверить в предложении и договоре. Не следует считать закупки и сторонние подписки включёнными по умолчанию."]],"related":["rapid-response","servers-hosting","remote-desktop"]
|
||||
},
|
||||
{
|
||||
"slug":"remote-desktop","group":"Услуги","path":"services/remote-desktop","card":"Удалённый рабочий стол","title":"Удалённая компьютерная помощь для бизнеса на КМВ","description":"Настройка компьютеров, программ, принтеров и доступа сотрудников. Удалённая IT-помощь бизнесу на КМВ с проверкой результата и понятным отчётом.","lead":"Помогаем сотруднику вернуться к работе: разобраться с программой, восстановить доступ, настроить принтер или подготовить рабочее место. Подключение и объём действий согласуем заранее.","label":"Рабочее место снова работает","alt":"Офисный компьютер и компактный принтер с защищённым каналом удалённой помощи",
|
||||
"sections":[["Какие задачи решаем удалённо","Диагностируем ошибки операционной системы и приложений, настраиваем сетевые принтеры, доступ к общим папкам и рабочим сервисам. Помогаем подключить новое рабочее место к существующей инфраструктуре. Перед изменениями уточняем, какие данные и программы критичны для сотрудника и что уже предпринималось для решения проблемы."],["Как проходит сеанс помощи","Сотрудник описывает симптом и подтверждает подключение. Проверяем состояние компьютера, сообщения об ошибках, сетевые настройки и доступность зависимых сервисов. При необходимости согласуем перезапуск или временное закрытие программы. Завершаем сеанс проверкой конкретной операции, которая не работала, и кратким объяснением результата."],["Наш подход: компьютер — часть инфраструктуры","Ошибка на экране не всегда возникает на самом ПК. Приложение может зависеть от VPN, файлового сервера или службы авторизации. Опыт сопровождения сетей и серверов помогает проверять цепочку целиком. Это особенно важно для удалённых сотрудников и объектов, где рабочее место выходит в корпоративные ресурсы через несколько участков связи."],["Безопасность доступа","Выбираем согласованный инструмент удалённой помощи и определяем, кто вправе подключаться. Постоянный доступ настраиваем только при необходимости, с ограничением прав. Учётные записи закрепляем за ответственными, а доступ уволенных сотрудников отзываем. Перед действиями, способными затронуть данные, проверяем наличие подходящей резервной копии."],["Границы удалённой помощи","Неисправный накопитель, повреждённый кабель и механическая поломка принтера требуют работ на месте. Если диагностика указывает на аппаратную причину, объясняем, что именно нужно проверить или заменить. Для специфического прикладного ПО при необходимости подключаем его поставщика: сетевую доступность и работу самой программы важно разделять."]],
|
||||
"strengths":["Диагностика от приложения до сети и сервера","Согласованные действия с рабочими данными","Проверка результата вместе с сотрудником"],"faq":[["Можно ли помочь сотруднику вне офиса?","Да, если есть рабочий интернет и возможность согласованного удалённого подключения. Доступ к корпоративным ресурсам проверяем отдельно."],["Нужно ли сообщать пароль от всех сервисов?","Нет. Согласуем только необходимый доступ и подходящий способ его предоставления. Лишние права для диагностики не нужны."],["Настраиваете ли сетевые принтеры?","Да: проверяем сетевое подключение, очередь печати и драйвер. Механический ремонт и расходные материалы относятся к отдельным работам."]],"related":["rapid-response","networks-vpn","it-subscription"]
|
||||
},
|
||||
{
|
||||
"slug":"networks-vpn","group":"Услуги","path":"services/networks-vpn","card":"Сети и VPN","title":"Сети и VPN для бизнеса: офисы, склады и удалённые точки","description":"Объединение офисов и объектов в защищённую сеть. Keenetic, MikroTik, WireGuard и ZeroTier: маршрутизация, резервирование и диагностика связи.","lead":"Связываем офисы, склады и удалённые объекты так, чтобы сотрудники получали доступ к нужным ресурсам, а управление сетью оставалось понятным и контролируемым.","label":"Одна команда. Несколько площадок.","alt":"Маршрутизатор и сетевой коммутатор соединяют несколько офисных миниатюр световыми линиями",
|
||||
"sections":[["Начинаем с задач доступа","Уточняем, кому и к чему нужна связь: сотрудникам — к файлам и приложениям, ответственному — к камерам, инженеру — к оборудованию. Проверяем адресацию, ограничения провайдеров и существующие маршруты. Это помогает заранее обнаружить пересекающиеся подсети и избежать ситуации, когда подключение VPN лишает объект обычного интернета."],["Подбираем схему под площадку","Работаем с маршрутизаторами Keenetic и MikroTik, туннелями WireGuard и решениями на базе ZeroTier. Выбор зависит от оборудования, числа точек и требований к доступу. Разделяем служебный и гостевой трафик, задаём необходимые правила доступа и предусматриваем способ восстановления управления после изменения конфигурации."],["Наш опыт: соединение проверяется трафиком","В распределённых сетях встречается ситуация: туннель показывает активное состояние, но полезные данные не проходят. В нашей практике такие случаи разбираются по маршрутам в обе стороны, счётчикам обмена и доступности целевого сервиса. Отдельно проверяем DNS и сохранение настроек. Это позволяет искать причину точнее, чем по одному зелёному индикатору."],["Резервирование без ложной уверенности","Два подключения не всегда означают два независимых канала: они могут зависеть от одного питания, линии или промежуточного узла. Фиксируем общие зависимости и проверяем поведение при отказе. Согласуем, какие сервисы должны пережить переключение и допустимо ли краткое прерывание работы."],["Что передаём после настройки","Описываем назначение сегментов и соединений, сохраняем резервную конфигурацию и порядок отката. Проверяем доступ с рабочих мест и удалённых площадок. При сопровождении наблюдаем за нужными каналами и сервисами; новые правила вводим с учётом уже работающих устройств."]],
|
||||
"strengths":["Практика Keenetic, MikroTik и распределённых VPN","Проверка прямого и обратного маршрута","Учёт общих зависимостей резервных каналов"],"faq":[["Для чего бизнесу VPN?","Для защищённого соединения площадок и доступа к корпоративным ресурсам. Состав доступных систем и права пользователей задаются отдельно."],["Можно ли объединить офис и склад у разных провайдеров?","Да, если связь и оборудование позволяют построить выбранную схему. Проверяем ограничения, адресацию и качество канала."],["VPN сам обеспечивает резервный интернет?","Нет. Для резерва нужен дополнительный рабочий канал и настроенное переключение. VPN решает задачу связи поверх доступного соединения."]],"related":["remote-sites","servers-hosting","monitoring-security"]
|
||||
},
|
||||
{
|
||||
"slug":"monitoring-security","group":"Услуги","path":"services/monitoring-security","card":"Мониторинг и безопасность","title":"Мониторинг IT-инфраструктуры и резервных копий бизнеса","description":"Контроль серверов, связи, сервисов и резервных копий. Настройка уведомлений и диагностика сбоев для бизнеса на КМВ и Юге России.","lead":"Настраиваем наблюдение за тем, от чего зависит работа бизнеса. Полезное уведомление должно отвечать на три вопроса: что произошло, где искать причину и кому действовать.","label":"Знать о проблеме и понимать действие","alt":"Панель наблюдения за серверами, сигналы доступности и защищённое резервное хранилище",
|
||||
"sections":[["Что включаем в контроль","Выбираем показатели под ваши сервисы: доступность сайта или приложения, состояние канала, свободное место, свежесть резервной копии и поступление данных от оборудования. Проверка одного сетевого адреса не заменяет проверку пользовательской операции. Для важных систем определяем, какой именно результат означает исправную работу."],["Уведомления без постоянного шума","Согласуем пороги, повторные проверки и получателей. Кратковременный сетевой сбой и длительная недоступность требуют разных действий. Уведомление дополняем названием объекта, наблюдаемым симптомом и следующей проверкой. Telegram может быть одним из каналов доставки; его работоспособность также нужно проверять."],["Сильная сторона: проверяем причину и восстановление","В нашей практике мониторинг используется при поиске обрывов связи на удалённых площадках, наблюдении за серверами и сборе данных контроллеров. Сопоставляем доступность устройств и сервисов, чтобы локализовать участок отказа. После изменения проверяем исходный симптом; после настройки оповещений проводим согласованный пробный сценарий."],["Резервная копия должна восстанавливаться","Само наличие файла ещё не доказывает пригодность резервной копии. Определяем, какие данные сохранять, как долго их хранить и где разместить копии. Согласуем допустимую потерю последних изменений и время восстановления. Для выбранных систем проводим пробное восстановление в отдельном месте, не затрагивая рабочие данные."],["Безопасность как регулярная работа","Проверяем внешние точки входа, права учётных записей и актуальность обновлений. Изменения планируем с резервной копией и возможностью отката. Мониторинг не гарантирует отсутствие инцидентов: он помогает обнаруживать отклонения и действовать по согласованному порядку. Круглосуточный сбор сигналов и круглосуточное дежурство инженера оговариваются отдельно."]],
|
||||
"strengths":["Контроль полезного сервиса, а не только адреса","Проверка доставки уведомлений","Резервирование с планом восстановления"],"faq":[["Что означает мониторинг 24/7?","Автоматические проверки выполняются круглосуточно. Время реакции инженера, дежурство и эскалация определяются условиями обслуживания."],["Можно ли получать уведомления в Telegram?","Да, если согласованы бот, получатели и правила доставки. После подключения проверяем отправку и получение тестового уведомления."],["Резервные копии исключают потерю данных?","Нет. Результат зависит от периодичности, хранения и пригодности копий. Поэтому заранее определяем допустимые потери и проверяем восстановление."]],"related":["servers-hosting","automation","rapid-response"]
|
||||
},
|
||||
{
|
||||
"slug":"video-surveillance","group":"Услуги","path":"services/video-surveillance","card":"Видеонаблюдение","title":"Видеонаблюдение для бизнеса: камеры, архив и удалённый доступ","description":"Проектирование и сопровождение IP-видеонаблюдения на КМВ: камеры, регистраторы, сеть, питание и проверка архива для офисов и удалённых объектов.","lead":"Помогаем видеть нужную зону и находить запись нужного события. Рассматриваем видеонаблюдение как единую систему: обзор, освещение, сеть, питание, регистратор и доступ ответственных.","label":"Нужный ракурс. Доступная запись.","alt":"IP-камера над складской площадкой и видеорегистратор в тёмной технологической композиции",
|
||||
"sections":[["Начинаем с того, что нужно увидеть","Для въезда, кассовой зоны и общего обзора территории нужны разные ракурсы. Уточняем задачу каждой камеры, расстояние до объекта наблюдения и условия освещения. При проектировании учитываем высоту установки, препятствия, кабельные трассы и возможность обслуживания. Количество камер само по себе не определяет качество системы."],["Сеть и питание — часть видеонаблюдения","Пропадание изображения может быть связано с камерой, PoE-коммутатором, радиомостом или перегрузкой канала. Проверяем цепочку целиком. Для удалённых площадок отдельно оцениваем пропускную способность и доступ к локальному архиву, чтобы просмотр через интернет не мешал основной работе объекта."],["Наш опыт: от трасс до удалённой диагностики","В практике команды есть работа со схемами камер и кабельных трасс на строительных объектах, а также диагностика регистраторов и связи между удалёнными площадками. Используем планы и наглядные схемы для обсуждения зон обзора. Условную визуализацию всегда отделяем от точной монтажной документации и проверяем привязки по исходным материалам."],["Архив проверяем отдельно от живого видео","Открывающаяся камера ещё не означает, что запись сохраняется. Проверяем дату и время, режим записи, состояние накопителей и поиск фрагмента. Срок хранения зависит от числа камер, разрешения, интенсивности записи и ёмкости дисков. Рассчитываем его под задачу и подтверждаем на выбранной конфигурации."],["Доступ для владельца и ответственных","Разделяем права просмотра и администрирования, согласуем устройства для удалённого доступа. После настройки проверяем соединение из нужной сети и просмотр архива. Изменения паролей и состава пользователей учитываем в сопровождении. Для расширения системы заранее оцениваем запас по портам, питанию и хранилищу."]],
|
||||
"strengths":["Опыт с сетевой частью и удалёнными регистраторами","Проверка архива вместе с живым изображением","Наглядное обсуждение ракурсов и трасс"],"faq":[["Можно ли оставить существующие камеры?","Да, если совместимость, состояние и качество изображения подходят задаче. Сначала обследуем систему и выявляем ограничения."],["Будет ли запись при пропадании интернета?","При локальном регистраторе запись может продолжаться, если питание и локальная сеть исправны. Это нужно проверить на конкретной системе."],["Можно ли смотреть камеры с телефона?","Да, для совместимого оборудования настраиваем согласованный способ доступа и права. Работу проверяем через сеть, которой пользуется ответственный."]],"related":["construction","agriculture","networks-vpn"]
|
||||
},
|
||||
{
|
||||
"slug":"servers-hosting","group":"Услуги","path":"services/servers-hosting","card":"Серверы и хостинг","title":"Серверы и хостинг для бизнеса: Proxmox, Docker и сопровождение","description":"Развёртывание и сопровождение серверов, виртуальных машин и приложений. Proxmox, Docker, резервные копии и план обновлений для бизнеса.","lead":"Размещаем рабочие сервисы там, где ими удобно управлять и восстанавливать их после сбоя. Выбор между локальным сервером и хостингом начинаем с требований к данным и доступности.","label":"Основа для ежедневной работы","alt":"Компактная серверная стойка с синим и бирюзовым светом и орбитальной линией резервирования",
|
||||
"sections":[["От задачи бизнеса к размещению","Уточняем состав приложений, пользователей и данных: сайт, CRM, файловое хранилище, внутренняя система или другое рабочее ПО. Проверяем требования поставщиков и лицензирования. Оцениваем ресурсы, зависимость от интернета и возможный простой. Это помогает подобрать размещение без лишней сложности и без недооценки нагрузки."],["Виртуализация и контейнеры","Используем Proxmox для разделения серверных задач на виртуальные машины и контейнеры, Docker — для подходящих приложений. Разделение упрощает обслуживание, но само по себе не создаёт отказоустойчивость. Планируем хранение данных, доступ к управлению, обновления и резервное копирование каждого значимого компонента."],["Практический опыт сопровождения","В нашей инфраструктурной практике есть виртуальные машины, контейнерные приложения, базы данных и веб-сервисы. Поэтому рассматриваем не только запуск программы, но и её зависимости: домен, сертификат, база, хранилище, почта и фоновые задачи. При переносе важно проверить пользовательский сценарий и сохранность данных, а не только состояние процесса."],["Обновления с возможностью вернуться назад","Перед изменением определяем состав резервной копии и порядок отката. Для важных обновлений используем отдельную проверку или тестовый контур, если он предусмотрен. После работ проверяем ключевые функции приложения и журналы ошибок. Документируем версии и особенности конфигурации, которые понадобятся при следующем обслуживании."],["Что входит в понятное сопровождение","Фиксируем ответственных за хостинг, приложение, лицензии и резервные копии. Настраиваем контроль ресурсов и важных сроков, включая сертификаты при включении этой задачи в обслуживание. Отдельно согласуем перенос данных, администрирование прикладной системы и доработку её кода: это разные виды работ."]],
|
||||
"strengths":["Практика Proxmox, Docker и серверных приложений","Учёт данных и зависимостей при переносе","Обновления с проверкой и планом отката"],"faq":[["Что лучше: сервер в офисе или хостинг?","Зависит от приложений, связи, требований к данным и допустимого простоя. Сначала сравниваем эти условия и стоимость сопровождения."],["Делаете ли перенос существующего сервиса?","Да, после обследования и согласования окна работ. План включает копирование данных, проверку, переключение и возможность отката."],["Нужны ли копии, если есть виртуализация?","Да. Виртуализация не заменяет независимые резервные копии. Снимок на том же хранилище не защищает от его отказа."]],"related":["monitoring-security","networks-vpn","it-subscription"]
|
||||
},
|
||||
{
|
||||
"slug":"automation","group":"Услуги","path":"services/automation","card":"Автоматизация","title":"Автоматизация бизнеса: Telegram-боты, интеграции и контроллеры","description":"Telegram-боты, уведомления, отчёты и интеграции с контроллерами. Автоматизация повторяющихся операций с проверкой доставки и прав доступа.","lead":"Связываем источники данных и рабочие действия: получить показание, отправить уведомление, собрать отчёт или выполнить согласованную команду. Начинаем с одного полезного сценария.","label":"Меньше ручных повторений","alt":"Промышленный контроллер, датчик и телефон соединены световыми потоками событий",
|
||||
"sections":[["Сначала описываем рабочий процесс","Разбираем, кто сейчас выполняет операцию, откуда берёт данные и кому передаёт результат. Выбираем измеримый сценарий: ежедневная сводка, уведомление об отклонении, сбор заявки или изменение статуса. Уточняем, что делать при недоступности источника, повторном событии и ошибке отправки."],["Боты и интеграции","Создаём Telegram-ботов и связки между сервисами, когда у источников есть подходящий интерфейс. Интеграция с кассой, CRM или учётной системой требует проверки API, версии и прав доступа. Не обещаем совместимость со всем оборудованием: сначала подтверждаем, какие данные и действия реально доступны."],["Наш опыт: контроллеры и событийные сообщения","В практике команды есть работа с Wiren Board, MQTT, энергетическими показателями и Telegram-уведомлениями. Измерение напряжения или мощности само по себе мало полезно владельцу; нужно добавить объект, время, понятный порог и объяснение события. Поэтому проектируем сообщение вокруг решения, которое сможет принять получатель."],["Команды требуют отдельной защиты","Просмотр показаний и управление реле имеют разный уровень ответственности. Разделяем роли пользователей и разрешённые действия, предусматриваем подтверждение для чувствительных команд. Учитываем ограничения оборудования и локальные защитные механизмы. Автоматизация не должна обходить штатные блокировки и правила эксплуатации объекта."],["Проверяем весь сценарий","Тестируем получение данных, обработку события, отправку и фактическое получение сообщения. Для отчётов проверяем свежесть данных и поведение при пропусках. Фиксируем зависимости от внешних сервисов и порядок восстановления. Развивать автоматизацию проще поэтапно: сначала надёжный сценарий, затем дополнительные источники и роли."]],
|
||||
"strengths":["Практика Wiren Board, MQTT и Telegram","Проверка сценария от источника до получателя","Разделение наблюдения и управляющих команд"],"faq":[["Можно ли автоматизировать отчёт из нашей системы?","Сначала проверим доступный API, экспорт и права. Если данные можно получать устойчиво, предложим способ формирования и доставки отчёта."],["Можно ли управлять оборудованием из Telegram?","Для совместимого оборудования — да, после согласования прав, подтверждений и защитных ограничений. Критичные блокировки должны оставаться на объекте."],["Что будет при недоступности Telegram?","Это учитывается в проектировании: журнал событий, повторная доставка или резервный канал выбираются под важность сценария."]],"related":["agriculture","monitoring-security","servers-hosting"]
|
||||
},
|
||||
{
|
||||
"slug":"horeca","group":"Отрасли","path":"industries/horeca","card":"HoReCa","title":"IT-поддержка гостиниц, кафе и ресторанов на КМВ","description":"Сети, гостевой Wi-Fi, рабочие места и видеонаблюдение для гостиниц и ресторанов на КМВ. Поддержка с учётом режима работы и зависимостей сервисов.","lead":"Гость должен пользоваться связью, а сотрудники — спокойно работать с заказами и бронированиями. Собираем IT-инфраструктуру заведения вокруг этих повседневных задач.","label":"Технологии поддерживают сервис","alt":"Уютный ресторан при горном отеле со стойкой администратора и деликатными линиями Wi-Fi",
|
||||
"sections":[["Разделяем гостевую и служебную сеть","Посетителям нужен удобный Wi-Fi, персоналу — стабильный доступ к рабочим системам. Предусматриваем разделение сегментов и ограничения доступа между ними. При обследовании учитываем планировку, перекрытия и места высокой нагрузки. Размещение точек доступа выбираем по условиям объекта, а качество связи проверяем в нужных зонах."],["Касса, бронирование и рабочее место","Проверяем сетевую часть подключения кассовых устройств, принтеров, компьютеров администратора и внешних сервисов. Поддержку самого прикладного ПО и фискального оборудования согласуем с их поставщиками. Это помогает быстро определить, кто решает конкретную проблему, вместо пересылки заявки между несколькими подрядчиками."],["Как наш инженерный опыт помогает заведению","Опыт команды в сетях, удалённых объектах, серверных сервисах и видеонаблюдении применим к гостиницам и ресторанам: здесь также важно видеть общие зависимости. Если пропало питание коммутатора, одновременно могут исчезнуть Wi-Fi, камеры и рабочие подключения. Начинаем диагностику с картины целиком, затем проверяем отдельные устройства."],["Обновления с учётом гостей и смен","Согласуем работы на время минимальной нагрузки и предупреждаем, какие операции будут недоступны. Перед изменением сохраняем настройки и определяем порядок возврата. Для сезонных объектов полезно проверить оборудование и доступы до открытия, а после перестановок — заново оценить покрытие Wi-Fi и зоны обзора камер."],["Что обсуждаем перед подключением","Количество зон и рабочих мест, часы работы, текущих поставщиков ПО, необходимость резерва интернета и удалённого просмотра камер. Интеграции с сервисами доставки или бронирования оцениваем по доступным интерфейсам. По результатам формируем понятный состав поддержки и правила обращения для персонала."]],
|
||||
"strengths":["Разделение гостевого и рабочего доступа","Диагностика общих причин сбоев","Работы с учётом режима заведения"],"faq":[["Можно ли улучшить Wi-Fi без полной замены сети?","Возможно: сначала проверяем размещение точек, настройки, кабели и нагрузку. Замена нужна только там, где оборудование или схема ограничивают результат."],["Поддерживаете ли кассовое ПО?","Согласуем перечень систем и границы работ. Настройку сети отделяем от задач, требующих участия поставщика кассового или фискального оборудования."],["Работаете ли с сезонными базами отдыха?","Такой объект можно включить в сопровождение. Состав подготовки к сезону и условия поддержки согласуются по его оборудованию и режиму работы."]],"related":["networks-vpn","remote-desktop","video-surveillance"]
|
||||
},
|
||||
{
|
||||
"slug":"agriculture","group":"Отрасли","path":"industries/agriculture","card":"Агросектор","title":"IT для ферм и агробизнеса: связь, весовые и мониторинг","description":"IT-поддержка ферм, весовых и складов на Юге России: удалённая связь, камеры, контроллеры, мониторинг питания и уведомления ответственным.","lead":"На ферме IT выходит за стены офиса: весовая, склад, ремонтная зона, камеры и датчики могут находиться на разных площадках. Помогаем связать их и видеть состояние важных систем.","label":"Объекты далеко. Информация рядом.","alt":"Ферма у гор, весовая площадка и зерновое хранилище с деликатной визуализацией сети датчиков",
|
||||
"sections":[["Связь между площадками","Проверяем доступные каналы, расстояния и зависимость оборудования от питания. Для офисной части, весовой и камер могут понадобиться разные требования к трафику. Подбираем схему удалённого доступа и резервирования с учётом существующего оборудования. Радиолинию и мобильный интернет оцениваем по измерениям на месте."],["Весовая и рабочие данные","Разделяем доступность сети, работу компьютера и поведение специализированной весовой программы. Уточняем интерфейсы оборудования и ответственность поставщика. Настраиваем согласованный доступ к рабочему месту, контроль связи и резервирование данных, если это входит в задачу. До интеграции с учётной системой проверяем формат обмена."],["Опыт на распределённых хозяйственных объектах","В нашей практике есть диагностика связи весовой с удалённой ремонтной зоной и работа с видеонаблюдением на удалённых площадках. Наблюдение за доступностью оборудования и временем его работы позволяет точнее искать причину: перезапуск, пропадание питания или обрыв участка сети. Такой подход особенно полезен там, где каждый выезд занимает время."],["Показания и уведомления","Работаем с контроллерами Wiren Board и передачей данных по MQTT. Можно организовать наблюдение за совместимыми измерителями, питанием и состоянием оборудования. Для каждого сигнала согласуем порог, период проверки и получателя. Управление исполнительными устройствами рассматриваем отдельно от мониторинга, с учётом локальных защит и условий эксплуатации."],["Результат для ответственного","Стремимся сделать состояние площадки понятным без изучения десятков интерфейсов: какой объект на связи, откуда давно не поступали данные, что проверить первым. Документируем состав системы и действия при типовых отказах. Автоматизация внедряется поэтапно, начиная с самого полезного и проверяемого сценария."]],
|
||||
"strengths":["Практика весовых и связи между площадками","Сочетание сетей, камер и контроллеров","Диагностика с учётом питания и выездов"],"faq":[["Можно ли получать показания датчиков на телефон?","Да, если датчики и контроллер поддерживают доступный интерфейс. Согласуем состав данных, периодичность и способ уведомлений."],["Подключите ли существующую весовую к учёту?","Сначала проверим оборудование, программу и доступный формат обмена. Интеграция зависит от совместимости и документации поставщика."],["Что делать, если на площадке нестабильный интернет?","Обследуем связь и питание, рассмотрим резерв. Критичные локальные функции должны сохраняться без зависимости от постоянного внешнего соединения."]],"related":["remote-sites","automation","video-surveillance"]
|
||||
},
|
||||
{
|
||||
"slug":"construction","group":"Отрасли","path":"industries/construction","card":"Строительство","title":"IT для стройплощадок: временные сети и видеонаблюдение","description":"Связь, рабочие места и камеры на строительных объектах. Временная IT-инфраструктура, схемы трасс, удалённый доступ и сопровождение изменений.","lead":"Стройплощадка меняется по мере работ. Планируем связь и видеонаблюдение так, чтобы оборудование можно было обслуживать, переносить и расширять без потери понимания системы.","label":"Инфраструктура движется вместе с объектом","alt":"Строительная площадка с краном, модульным офисом и камерой на фоне гор в ночной палитре",
|
||||
"sections":[["Интернет и рабочие места на объекте","Организуем сеть для штаба строительства, сотрудников и согласованных рабочих систем. Выбор подключения зависит от доступных операторов, расположения площадки и срока эксплуатации. Проверяем питание, размещение оборудования и защищённый доступ к офисным ресурсам. Для временного решения также нужны документация и ответственный за обслуживание."],["Камеры под этап строительства","Определяем нужные зоны: въезд, склад, периметр или ход работ. При изменении застройки прежний ракурс может стать бесполезным, поэтому предусматриваем пересмотр размещения. Учитываем кабельные трассы, питание, условия среды и сохранение архива. Удалённый просмотр проверяем отдельно от локальной записи."],["Практика работы со схемами","В опыте команды есть работа с планами камер, кабельных трасс и систем доступа для строительного объекта. Наглядная модель помогает обсуждать обзор и расположение устройств, но не заменяет проверенную исполнительную документацию. Если исходные привязки отсутствуют, фиксируем это явно и уточняем их до монтажного решения."],["Изменения без потери управляемости","Перенос бытовок, смена подрядчиков и добавление оборудования меняют сеть. После согласованных работ актуализируем схему и резервные конфигурации. Проверяем, что новые подключения не нарушили существующие камеры и рабочие места. Временные доступы выдаём под задачу и пересматриваем по завершении работ."],["Передача и дальнейшее сопровождение","Составляем перечень устройств, назначение соединений и порядок восстановления после отключения питания. При передаче объекта важно передать не только оборудование, но и актуальные настройки, права и документацию. Заранее согласуем, что остаётся в постоянной эксплуатации, а что демонтируется или переносится на следующую площадку."]],
|
||||
"strengths":["Опыт работы с планами камер и трасс","Учёт временного характера инфраструктуры","Документирование изменений и передачи объекта"],"faq":[["Можно ли начать с мобильного интернета?","Да, если покрытие и нагрузка позволяют. Качество и возможность резерва оцениваем на площадке."],["Можно ли переносить камеры по мере строительства?","Да, но после переноса нужно заново проверить обзор, питание, соединение и запись. План размещения стоит актуализировать."],["Заменяет ли 3D-схема монтажный проект?","Нет. Наглядная схема помогает согласованию; точные привязки и монтажные решения требуют проверенных исходных данных и документации."]],"related":["video-surveillance","networks-vpn","remote-sites"]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"rapid-response": "Специалист помогает владельцу небольшого семейного бизнеса в уютном офисе",
|
||||
"remote-sites": "Инженер обсуждает связь удалённой фермы с её владельцами за столом",
|
||||
"it-subscription": "Владельцы семейного бизнеса и специалист обсуждают IT-поддержку в спокойной обстановке",
|
||||
"remote-desktop": "Специалист объясняет работу компьютера владелице небольшого магазина",
|
||||
"networks-vpn": "Инженер обсуждает офисную сеть с командой семейного склада",
|
||||
"monitoring-security": "Специалист и владелец бизнеса обсуждают наблюдение за системами и резервные копии",
|
||||
"video-surveillance": "Инженер и владельцы гостевого дома оценивают обзор наружной камеры",
|
||||
"servers-hosting": "Инженер проверяет серверное оборудование вместе с владельцем бизнеса",
|
||||
"automation": "Специалист и руководитель хозяйства вместе осматривают датчик и телефон",
|
||||
"horeca": "Владелец семейного отеля и инженер проверяют связь в уютном ресторане",
|
||||
"agriculture": "Семья фермеров и специалист обсуждают датчики и работу хозяйства",
|
||||
"construction": "Руководитель стройплощадки и специалист обсуждают оборудование в рабочем офисе"
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
[
|
||||
{
|
||||
"slug": "rapid-response",
|
||||
"path": "artwork/photos-selected/rapid-response.png"
|
||||
},
|
||||
{
|
||||
"slug": "remote-sites",
|
||||
"path": "artwork/photos-selected/remote-sites.png"
|
||||
},
|
||||
{
|
||||
"slug": "it-subscription",
|
||||
"path": "artwork/photos-selected/it-subscription.png"
|
||||
},
|
||||
{
|
||||
"slug": "remote-desktop",
|
||||
"path": "artwork/photos-selected/remote-desktop.png"
|
||||
},
|
||||
{
|
||||
"slug": "networks-vpn",
|
||||
"path": "artwork/photos-selected/networks-vpn.png"
|
||||
},
|
||||
{
|
||||
"slug": "monitoring-security",
|
||||
"path": "artwork/photos-selected/monitoring-security.png"
|
||||
},
|
||||
{
|
||||
"slug": "video-surveillance",
|
||||
"path": "artwork/photos-selected/video-surveillance.png"
|
||||
},
|
||||
{
|
||||
"slug": "servers-hosting",
|
||||
"path": "artwork/photos-selected/servers-hosting.png"
|
||||
},
|
||||
{
|
||||
"slug": "automation",
|
||||
"path": "artwork/photos-selected/automation.png"
|
||||
},
|
||||
{
|
||||
"slug": "horeca",
|
||||
"path": "artwork/photos-selected/horeca.png"
|
||||
},
|
||||
{
|
||||
"slug": "agriculture",
|
||||
"path": "artwork/photos-selected/agriculture.png"
|
||||
},
|
||||
{
|
||||
"slug": "construction",
|
||||
"path": "artwork/photos-selected/construction.png"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"mode": "built-in image_gen",
|
||||
"style": "Use case: photorealistic-natural. Asset type: wide editorial photograph inside a Russian IT support service article, 3:2 landscape. Warm, candid, intimate yet credible documentary photography with natural adult people, tactile wood and linen, soft amber daylight or lamp light, calm hospitality and family-business warmth. Preserve servaki.online visual identity through subtle deep navy clothing or shadows and a small restrained cyan accent, but the photograph should feel human and welcoming rather than neon or science fiction. Show a careful support specialist earning trust through respectful collaboration and discreet handling of technology. Never show readable text, passwords, private documents, identifiable real clients, logos or brands. No staged handshake, no exaggerated smile, no floating UI, no watermark. Scene: ",
|
||||
"subjects": [
|
||||
[
|
||||
"rapid-response",
|
||||
"In a small warm family-owned office, a female support specialist in her thirties listens attentively to an owner beside a laptop while another employee calmly continues work in the soft-focus background. Focus on reassurance during a disruption; screen angled away and unreadable."
|
||||
],
|
||||
[
|
||||
"remote-sites",
|
||||
"An IT field specialist and two adult family farm operators seated at a wooden table by a window overlooking distant hills, reviewing a small unbranded router and a hand-drawn network sketch with no readable marks. Quiet collaborative warmth."
|
||||
],
|
||||
[
|
||||
"it-subscription",
|
||||
"The owners of a small family business and a trusted IT specialist in a comfortable lived-in office, reviewing an unmarked notebook and a closed laptop together over tea. Daylight, ordinary confidence, no visible financial data."
|
||||
],
|
||||
[
|
||||
"remote-desktop",
|
||||
"A support specialist and an adult woman who runs a family shop seated side by side in a cozy small office, looking at a laptop while the specialist explains with an open hand; small printer nearby, screen blurred and unreadable, considerate personal distance."
|
||||
],
|
||||
[
|
||||
"networks-vpn",
|
||||
"An adult male IT specialist and a family-owned warehouse team at a wooden meeting table, discussing a compact router and cable plan; sunlit southern Russian hills through a window, relaxed serious listening, screens turned away."
|
||||
],
|
||||
[
|
||||
"monitoring-security",
|
||||
"A thoughtful IT specialist showing a small business owner a laptop in a quiet warm home-office style workplace, both focused on trust and backup planning; all display content out of focus, no visible personal information."
|
||||
],
|
||||
[
|
||||
"video-surveillance",
|
||||
"A field specialist and adult proprietors of a small family-run guesthouse looking up together at an unobtrusive outdoor security camera in their sunny enclosed courtyard. The specialist explains sightlines; warm stone and plants; no guests or private footage visible."
|
||||
],
|
||||
[
|
||||
"servers-hosting",
|
||||
"A server specialist in a clean but welcoming office with warm wooden furniture and an adult owner nearby, carefully checking a small rack cabinet; both shown naturally in profile with relaxed confidence, no screen content."
|
||||
],
|
||||
[
|
||||
"automation",
|
||||
"A farm manager and an IT specialist together in a cozy utility-room corner with wood shelves, examining a small unlabeled sensor and a phone held with its screen turned away; gentle daylight, practical collaboration."
|
||||
],
|
||||
[
|
||||
"horeca",
|
||||
"Inside a warm family-owned mountain guesthouse restaurant, an adult proprietor and IT specialist check a small Wi-Fi access point near the reception area while a family member sets a table in the distance. Candid hospitality, no visible reservation or guest data."
|
||||
],
|
||||
[
|
||||
"agriculture",
|
||||
"An adult farming family and an IT specialist gathered around a wooden table at their farm office by a window overlooking fields, discussing a small sensor and closed notebook. Warm late-afternoon light, practical equipment, no legible records."
|
||||
],
|
||||
[
|
||||
"construction",
|
||||
"An adult site manager and an IT specialist in a warm modular construction office with coats hung nearby, reviewing an unmarked site plan and a camera component; sunlit building frame through the window, cordial focus, no detailed blueprints readable."
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"rapid-response": "Перед удалённым подключением уточняем, какая задача требует доступа и какие действия специалист выполнит. Данные рабочего места не используются для публичных демонстраций.",
|
||||
"remote-sites": "Схему удалённого доступа и круг сотрудников с правами согласуем с владельцем объекта. Для диагностики используем необходимые технические сведения без публикации адресов и настроек площадки.",
|
||||
"it-subscription": "В начале обслуживания определяем ответственных за выдачу доступов и способ передачи чувствительных сведений. Состав работ и доступных систем фиксируем вместе с клиентом.",
|
||||
"remote-desktop": "Сотрудник подтверждает сеанс помощи и знает, что специалист собирается проверить. Для обычной диагностики не просим полный доступ ко всем учётным записям компании.",
|
||||
"networks-vpn": "Схема сети и параметры защищённых соединений относятся к рабочим данным клиента. Доступ инженера ограничиваем необходимыми устройствами и задачами.",
|
||||
"monitoring-security": "Для наблюдения выбираем только нужные показатели и получателей уведомлений. Содержимое резервных копий и системные доступы согласуем с владельцем данных.",
|
||||
"video-surveillance": "К просмотру камер и архива допускаются согласованные ответственные. При обсуждении проекта используем схемы и условные изображения без частных записей клиентов.",
|
||||
"servers-hosting": "Права к серверу, базе и резервным копиям предоставляются под конкретные работы. Перед переносом данных согласуем состав копирования и место хранения.",
|
||||
"automation": "У бота и интеграции есть назначение и круг получателей. Отдельно согласуем, какие события передавать в сообщения и кто может запускать управляющие команды.",
|
||||
"horeca": "Гостевая сеть отделяется от рабочих систем. Доступ к бронированиям и другим данным гостей определяется правилами заведения и полномочиями сотрудников.",
|
||||
"agriculture": "Показания, маршруты связи и данные учёта принадлежат хозяйству. Для уведомлений выбираем минимальный набор сведений и согласованный круг получателей.",
|
||||
"construction": "Планы объекта, расположение камер и рабочие доступы обсуждаем с ответственными за площадку. В публичных материалах не показываем реальные схемы и записи заказчика."
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
"""Publish only the generated static site to the existing Beget site over SFTP.
|
||||
|
||||
Credentials stay in the owner's separate access document and are never logged.
|
||||
Run audit before deploy; both phases pin the observed Beget SSH host key.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import posixpath
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(ROOT / '.deps'))
|
||||
import paramiko
|
||||
|
||||
ACCESS_DOC = Path(r'D:\YandexDisk\servaki.online\1 MPO_Inform_AI_Design_System\ops\BEGET-ACCESS.md')
|
||||
HOST = 'infogypa.beget.tech'
|
||||
USER = 'infogypa_0407'
|
||||
HOST_FINGERPRINT = 'SHA256:9vc74idZ9y0Vw8Ai6TS/3PqEn2A8a0VZcAdy5AYlQoc'
|
||||
SITE = '/home/i/infogypa/servaki.online'
|
||||
WEBROOT = SITE + '/public_html'
|
||||
BACKUP = ROOT / 'release-backups' / '2026-09-28-before-landing'
|
||||
REMOTE_BACKUP = SITE + '/backup-2026-09-28-before-landing'
|
||||
CORE = ('index.html', 'sitemap.xml', 'robots.txt')
|
||||
|
||||
|
||||
def digest(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
class PinnedHost(paramiko.MissingHostKeyPolicy):
|
||||
def missing_host_key(self, client, hostname, key):
|
||||
actual = 'SHA256:' + base64.b64encode(hashlib.sha256(key.asbytes()).digest()).decode().rstrip('=')
|
||||
if hostname != HOST or actual != HOST_FINGERPRINT:
|
||||
raise paramiko.SSHException('Beget SSH host key changed; deployment stopped')
|
||||
client.get_host_keys().add(hostname, key.get_name(), key)
|
||||
|
||||
|
||||
def connect():
|
||||
access = ACCESS_DOC.read_text(encoding='utf-8')
|
||||
match = re.search(r'\*\*Пароль:\*\*\s*`([^`]+)`', access)
|
||||
if not match:
|
||||
raise RuntimeError('FTP/SFTP credential entry not found')
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(PinnedHost())
|
||||
ssh.connect(HOST, username=USER, password=match.group(1), look_for_keys=False,
|
||||
allow_agent=False, timeout=20, auth_timeout=20, banner_timeout=20)
|
||||
sftp = ssh.open_sftp()
|
||||
if sftp.normalize(WEBROOT) != WEBROOT:
|
||||
raise RuntimeError('Unexpected Beget document root')
|
||||
return ssh, sftp
|
||||
|
||||
|
||||
def exists(sftp, path):
|
||||
try:
|
||||
return sftp.stat(path)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def remote_bytes(sftp, path):
|
||||
with sftp.open(path, 'rb') as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
def mkdirs(sftp, directory):
|
||||
missing = []
|
||||
while not exists(sftp, directory):
|
||||
missing.append(directory)
|
||||
directory = posixpath.dirname(directory)
|
||||
for item in reversed(missing):
|
||||
sftp.mkdir(item)
|
||||
|
||||
|
||||
def manifest():
|
||||
public = ROOT / 'public'
|
||||
return {p.relative_to(public).as_posix(): digest(p.read_bytes())
|
||||
for p in public.rglob('*') if p.is_file()}
|
||||
|
||||
|
||||
def audit():
|
||||
ssh, sftp = connect()
|
||||
try:
|
||||
before = {'host_fingerprint': HOST_FINGERPRINT, 'webroot': WEBROOT,
|
||||
'files': {}, 'top_entries': sorted(sftp.listdir(WEBROOT)),
|
||||
'release_files': manifest()}
|
||||
live_index = remote_bytes(sftp, WEBROOT + '/index.html')
|
||||
expected = (ROOT / 'reference' / 'original.html').read_bytes()
|
||||
if live_index != expected:
|
||||
raise RuntimeError('Live index differs from the reviewed source; merge before release')
|
||||
for name in CORE:
|
||||
path = WEBROOT + '/' + name
|
||||
data = remote_bytes(sftp, path) if exists(sftp, path) else None
|
||||
before['files'][name] = {'existed': data is not None,
|
||||
'sha256': digest(data) if data is not None else None}
|
||||
if data is not None:
|
||||
BACKUP.mkdir(parents=True, exist_ok=True)
|
||||
target = BACKUP / name
|
||||
if target.exists() and target.read_bytes() != data:
|
||||
raise RuntimeError('Local backup differs from current Beget file: ' + name)
|
||||
target.write_bytes(data)
|
||||
for dirname in ('approach', 'services', 'industries'):
|
||||
if exists(sftp, WEBROOT + '/' + dirname):
|
||||
raise RuntimeError('Existing remote directory needs review: ' + dirname)
|
||||
if exists(sftp, WEBROOT + '/assets/editorial'):
|
||||
raise RuntimeError('Existing editorial assets need review')
|
||||
BACKUP.mkdir(parents=True, exist_ok=True)
|
||||
(BACKUP / 'before.json').write_text(json.dumps(before, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
print('Audit passed: live homepage matches source; new editorial paths are clear.')
|
||||
print('Backed up existing core files:', [k for k, v in before['files'].items() if v['existed']])
|
||||
print('Planned static files:', len(before['release_files']))
|
||||
finally:
|
||||
sftp.close(); ssh.close()
|
||||
|
||||
|
||||
def put_atomic(sftp, local, remote, replace=False):
|
||||
mkdirs(sftp, posixpath.dirname(remote))
|
||||
temp = remote + '.codex-upload-' + uuid.uuid4().hex[:10]
|
||||
sftp.put(str(local), temp, confirm=True)
|
||||
if digest(remote_bytes(sftp, temp)) != digest(local.read_bytes()):
|
||||
raise RuntimeError('Upload verification failed: ' + remote)
|
||||
if replace:
|
||||
try:
|
||||
sftp.posix_rename(temp, remote)
|
||||
except OSError:
|
||||
shifted = REMOTE_BACKUP + '/' + posixpath.basename(remote) + '.shifted'
|
||||
sftp.rename(remote, shifted)
|
||||
try:
|
||||
sftp.rename(temp, remote)
|
||||
except Exception:
|
||||
sftp.rename(shifted, remote)
|
||||
raise
|
||||
else:
|
||||
sftp.rename(temp, remote)
|
||||
|
||||
|
||||
def deploy():
|
||||
before = json.loads((BACKUP / 'before.json').read_text(encoding='utf-8'))
|
||||
current = manifest()
|
||||
if current != before['release_files']:
|
||||
raise RuntimeError('Built site changed after audit; rerun audit')
|
||||
ssh, sftp = connect()
|
||||
try:
|
||||
if digest(remote_bytes(sftp, WEBROOT + '/index.html')) != before['files']['index.html']['sha256']:
|
||||
raise RuntimeError('Live homepage changed after audit; deployment stopped')
|
||||
# Refuse to overwrite any unreviewed route or asset.
|
||||
for rel, expected_hash in current.items():
|
||||
if rel in CORE:
|
||||
continue
|
||||
path = WEBROOT + '/' + rel
|
||||
if exists(sftp, path) and digest(remote_bytes(sftp, path)) != expected_hash:
|
||||
raise RuntimeError('Existing remote file differs: ' + rel)
|
||||
mkdirs(sftp, REMOTE_BACKUP)
|
||||
for name, state in before['files'].items():
|
||||
if state['existed']:
|
||||
target = REMOTE_BACKUP + '/' + name
|
||||
if not exists(sftp, target):
|
||||
put_atomic(sftp, BACKUP / name, target)
|
||||
elif digest(remote_bytes(sftp, target)) != state['sha256']:
|
||||
raise RuntimeError('Remote backup mismatch: ' + name)
|
||||
public = ROOT / 'public'
|
||||
ordered = sorted((rel for rel in current if rel not in CORE),
|
||||
key=lambda rel: (not rel.startswith('assets/'), rel))
|
||||
for rel in ordered:
|
||||
target = WEBROOT + '/' + rel
|
||||
if not exists(sftp, target):
|
||||
put_atomic(sftp, public / rel, target)
|
||||
for rel in ('robots.txt', 'sitemap.xml', 'index.html'):
|
||||
target = WEBROOT + '/' + rel
|
||||
if digest(remote_bytes(sftp, target)) == current[rel] if exists(sftp, target) else False:
|
||||
continue
|
||||
put_atomic(sftp, public / rel, target, replace=bool(exists(sftp, target)))
|
||||
for rel, expected_hash in current.items():
|
||||
if digest(remote_bytes(sftp, WEBROOT + '/' + rel)) != expected_hash:
|
||||
raise RuntimeError('Final remote checksum mismatch: ' + rel)
|
||||
(BACKUP / 'deployed.json').write_text(json.dumps({'files':len(current),'sha256_index':current['index.html']},indent=2),encoding='utf-8')
|
||||
print('Published and verified over SFTP:', len(current), 'files; backup outside public_html:', REMOTE_BACKUP)
|
||||
finally:
|
||||
sftp.close(); ssh.close()
|
||||
|
||||
|
||||
def verify():
|
||||
current = manifest()
|
||||
ssh, sftp = connect()
|
||||
try:
|
||||
failures = [rel for rel, expected in current.items()
|
||||
if not exists(sftp, WEBROOT + '/' + rel)
|
||||
or digest(remote_bytes(sftp, WEBROOT + '/' + rel)) != expected]
|
||||
if failures:
|
||||
raise RuntimeError('Remote file check failed: ' + ', '.join(failures[:5]))
|
||||
print('Remote checksum verification passed:', len(current), 'files')
|
||||
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'))
|
||||
new_index = (ROOT / 'public' / 'index.html').read_bytes()
|
||||
new_hash = digest(new_index)
|
||||
price_backup = ROOT / 'release-backups' / '2026-09-28-before-tripled-prices'
|
||||
remote_price_backup = SITE + '/backup-2026-09-28-before-tripled-prices'
|
||||
ssh, sftp = connect()
|
||||
try:
|
||||
live_index = remote_bytes(sftp, WEBROOT + '/index.html')
|
||||
if digest(live_index) == new_hash:
|
||||
print('Tariff update already published:', new_hash)
|
||||
return
|
||||
if digest(live_index) != before['sha256_index']:
|
||||
raise RuntimeError('Live homepage changed since the previous release; reprice stopped')
|
||||
expected = live_index.decode('utf-8')
|
||||
for old in (3000, 7000, 15000):
|
||||
structured = f'"price": "{old}"'
|
||||
visible = f'<span class="amount">{old}</span>'
|
||||
if expected.count(structured) != 1 or expected.count(visible) != 1:
|
||||
raise RuntimeError(f'Unexpected original tariff markup: {old}')
|
||||
new = old * 3
|
||||
expected = expected.replace(structured, f'"price": "{new}"')
|
||||
expected = expected.replace(visible, f'<span class="amount">{new:,}</span>'.replace(',', ' '))
|
||||
if expected.encode('utf-8') != new_index:
|
||||
raise RuntimeError('Built homepage has changes beyond the six tariff values')
|
||||
price_backup.mkdir(parents=True, exist_ok=True)
|
||||
local_copy = price_backup / 'index.html'
|
||||
if local_copy.exists() and local_copy.read_bytes() != live_index:
|
||||
raise RuntimeError('Existing local tariff backup differs from live homepage')
|
||||
local_copy.write_bytes(live_index)
|
||||
mkdirs(sftp, remote_price_backup)
|
||||
remote_copy = remote_price_backup + '/index.html'
|
||||
if exists(sftp, remote_copy):
|
||||
if digest(remote_bytes(sftp, remote_copy)) != digest(live_index):
|
||||
raise RuntimeError('Existing remote tariff backup differs from live homepage')
|
||||
else:
|
||||
put_atomic(sftp, local_copy, remote_copy)
|
||||
put_atomic(sftp, ROOT / 'public' / 'index.html', WEBROOT + '/index.html', replace=True)
|
||||
if digest(remote_bytes(sftp, WEBROOT + '/index.html')) != new_hash:
|
||||
raise RuntimeError('Published tariff homepage checksum mismatch')
|
||||
(price_backup / 'deployed.json').write_text(json.dumps({
|
||||
'before_sha256': digest(live_index), 'after_sha256': new_hash,
|
||||
'remote_backup': remote_copy, 'prices_rub_month': [9000, 21000, 45000]
|
||||
}, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
print('Published tripled tariffs; homepage checksum:', new_hash)
|
||||
finally:
|
||||
sftp.close(); ssh.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
{'audit': audit, 'deploy': deploy, 'verify': verify, 'reprice': reprice}[sys.argv[1]]()
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Preserve generated originals and export responsive WebP assets."""
|
||||
from pathlib import Path
|
||||
from PIL import Image,ImageOps,ImageDraw
|
||||
import json,shutil
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
manifest=json.loads((ROOT/'content/image-manifest.json').read_text(encoding='utf-8'))
|
||||
originals=ROOT/'artwork';originals.mkdir(exist_ok=True)
|
||||
thumbs=[]
|
||||
for item in manifest:
|
||||
src=ROOT/item['path'];slug=item['slug']
|
||||
dest=originals/(slug+'.png')
|
||||
if src.resolve()!=dest.resolve():shutil.copy2(src,dest)
|
||||
im=Image.open(src).convert('RGB')
|
||||
assert im.size==(1536,1024),(slug,im.size)
|
||||
for width in (480,960,1536):
|
||||
export=im.resize((width,round(width*2/3)),Image.Resampling.LANCZOS)
|
||||
export.save(ROOT/'assets'/f'{slug}-{width}.webp',quality=82,method=6)
|
||||
tile=Image.new('RGB',(384,286),'#05070f');tile.paste(im.resize((384,256)),(0,0));ImageDraw.Draw(tile).text((12,265),slug,fill='#67e8f9');thumbs.append(tile)
|
||||
sheet=Image.new('RGB',(384*3,286*((len(thumbs)+2)//3)),'#05070f')
|
||||
for i,t in enumerate(thumbs):sheet.paste(t,((i%3)*384,(i//3)*286))
|
||||
sheet.save(ROOT/'artwork/contact-sheet.jpg',quality=90)
|
||||
print('Prepared',len(manifest),'illustrations; 3 sizes each')
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Preserve generated editorial photographs and export responsive WebP versions."""
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageDraw, ImageOps
|
||||
import json, shutil
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
manifest=json.loads((ROOT/'content/photo-manifest.json').read_text(encoding='utf-8'))
|
||||
originals=ROOT/'artwork/photos-selected'; originals.mkdir(parents=True,exist_ok=True)
|
||||
tiles=[]
|
||||
for item in manifest:
|
||||
slug=item['slug']; source=ROOT/item['path']
|
||||
dest=originals/(slug+'.png')
|
||||
if source.resolve()!=dest.resolve():shutil.copy2(source,dest)
|
||||
picture=Image.open(source).convert('RGB')
|
||||
picture=ImageOps.fit(picture,(1536,1024),method=Image.Resampling.LANCZOS,centering=(.5,.5))
|
||||
for width in (480,960,1536):
|
||||
picture.resize((width,round(width*2/3)),Image.Resampling.LANCZOS).save(ROOT/'assets'/f'photo-{slug}-{width}.webp',quality=83,method=6)
|
||||
tile=Image.new('RGB',(384,280),'#05070f')
|
||||
tile.paste(picture.resize((384,256)),(0,0))
|
||||
ImageDraw.Draw(tile).text((12,263),slug,fill='#67e8f9')
|
||||
tiles.append(tile)
|
||||
sheet=Image.new('RGB',(1152,280*((len(tiles)+2)//3)),'#05070f')
|
||||
for index,tile in enumerate(tiles):sheet.paste(tile,((index%3)*384,(index//3)*280))
|
||||
sheet.save(originals/'contact-sheet.jpg',quality=88)
|
||||
print('Prepared',len(manifest),'warm editorial photographs in 3 responsive sizes')
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Download only public assets referenced by the existing homepage."""
|
||||
from pathlib import Path
|
||||
from urllib.request import build_opener,ProxyHandler,HTTPSHandler,Request
|
||||
from urllib.parse import urlsplit
|
||||
import ssl,re,json
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
BASE='https://servaki.online'
|
||||
html=(ROOT/'reference/original.html').read_text(encoding='utf-8')
|
||||
normal=build_opener(ProxyHandler({}))
|
||||
try:
|
||||
normal.open(BASE+'/',timeout=20).read(1)
|
||||
print('TLS validation: OK')
|
||||
except Exception as e:
|
||||
print('TLS validation failed:',type(e).__name__)
|
||||
# The original certificate was reported expired. This read-only fallback is
|
||||
# restricted to public site resources; it never carries authentication data.
|
||||
opener=build_opener(ProxyHandler({}),HTTPSHandler(context=ssl._create_unverified_context()))
|
||||
assets=sorted(set(re.findall(r'(?:src|href)="(/[^"?#]+\.(?:png|jpg|jpeg|webp|svg|ico))"',html)))
|
||||
manifest=[]
|
||||
for rel in assets:
|
||||
data=opener.open(BASE+rel,timeout=30).read()
|
||||
dest=ROOT/'public'/rel.lstrip('/');dest.parent.mkdir(parents=True,exist_ok=True);dest.write_bytes(data)
|
||||
manifest.append({'path':rel,'bytes':len(data)})
|
||||
for name in ['robots.txt','sitemap.xml']:
|
||||
try:
|
||||
response=opener.open(BASE+'/'+name,timeout=20);data=response.read()
|
||||
if b'<html' not in data.lower():(ROOT/'reference'/name).write_bytes(data)
|
||||
print(name,response.status,'reference saved' if b'<html' not in data.lower() else 'HTML fallback; not used')
|
||||
except Exception as e:print(name,type(e).__name__)
|
||||
(ROOT/'reference/public-assets.json').write_text(json.dumps(manifest,indent=2),encoding='utf-8')
|
||||
print('Public assets copied:',len(manifest))
|
||||
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
+1676
File diff suppressed because it is too large
Load Diff
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,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://servaki.online/sitemap.xml
|
||||
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
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>https://servaki.online/</loc></url><url><loc>https://servaki.online/approach/</loc></url><url><loc>https://servaki.online/approach/it-subscription/</loc></url><url><loc>https://servaki.online/approach/rapid-response/</loc></url><url><loc>https://servaki.online/approach/remote-sites/</loc></url><url><loc>https://servaki.online/industries/</loc></url><url><loc>https://servaki.online/industries/agriculture/</loc></url><url><loc>https://servaki.online/industries/construction/</loc></url><url><loc>https://servaki.online/industries/horeca/</loc></url><url><loc>https://servaki.online/services/</loc></url><url><loc>https://servaki.online/services/automation/</loc></url><url><loc>https://servaki.online/services/monitoring-security/</loc></url><url><loc>https://servaki.online/services/networks-vpn/</loc></url><url><loc>https://servaki.online/services/remote-desktop/</loc></url><url><loc>https://servaki.online/services/servers-hosting/</loc></url><url><loc>https://servaki.online/services/video-surveillance/</loc></url></urlset>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
[
|
||||
{
|
||||
"path": "/img/construction.png",
|
||||
"bytes": 2133417
|
||||
},
|
||||
{
|
||||
"path": "/img/farms.png",
|
||||
"bytes": 2438466
|
||||
},
|
||||
{
|
||||
"path": "/img/mountains.png",
|
||||
"bytes": 2557280
|
||||
},
|
||||
{
|
||||
"path": "/img/office.png",
|
||||
"bytes": 1980826
|
||||
},
|
||||
{
|
||||
"path": "/img/resorts.png",
|
||||
"bytes": 2456335
|
||||
},
|
||||
{
|
||||
"path": "/img/restaurant.png",
|
||||
"bytes": 2383609
|
||||
},
|
||||
{
|
||||
"path": "/img/servers.png",
|
||||
"bytes": 1939634
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
Pillow==12.3.0
|
||||
paramiko==5.0.0
|
||||
cryptography==50.0.1
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Validate rendered site, links, structured data, images and served responses."""
|
||||
from pathlib import Path
|
||||
from html.parser import HTMLParser
|
||||
from urllib.parse import urlsplit,unquote
|
||||
from urllib.request import build_opener,ProxyHandler
|
||||
from xml.etree import ElementTree as ET
|
||||
import json,re,hashlib
|
||||
ROOT=Path(__file__).resolve().parent;PUBLIC=ROOT/'public'
|
||||
class Page(HTMLParser):
|
||||
def __init__(self,text):
|
||||
super().__init__(convert_charrefs=True)
|
||||
self.h1=0;self.links=[];self.images=[];self.ids=[];self.ld=[];self.title='';self.description='';self.canonical='';self.script=None;self.in_title=False
|
||||
self.feed(text)
|
||||
def handle_starttag(self,tag,attrs):
|
||||
a=dict(attrs)
|
||||
if 'id' in a:self.ids.append(a['id'])
|
||||
if tag=='h1':self.h1+=1
|
||||
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=='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'):
|
||||
self.images.append(a)
|
||||
self.links.append(a['src'])
|
||||
self.links += [part.strip().split()[0] for part in a.get('srcset','').split(',') if part.strip()]
|
||||
if tag=='meta' and a.get('property')=='og:image':self.links.append(a['content'])
|
||||
if tag=='script' and a.get('type')=='application/ld+json':self.script=''
|
||||
def handle_endtag(self,tag):
|
||||
if tag=='title':self.in_title=False
|
||||
if tag=='script' and self.script is not None:
|
||||
self.ld.append(json.loads(self.script));self.script=None
|
||||
def handle_data(self,d):
|
||||
if self.in_title:self.title+=d
|
||||
if self.script is not None:self.script+=d
|
||||
def check(text,rel):
|
||||
p=Page(text)
|
||||
assert p.h1==1,(rel,'expected one H1')
|
||||
assert p.title and p.description and p.canonical,(rel,'missing metadata')
|
||||
assert len(set(p.ids))==len(p.ids),(rel,'duplicate IDs')
|
||||
assert p.canonical=='https://servaki.online/'+('' if rel=='index.html' else rel.removesuffix('index.html')),(rel,'canonical')
|
||||
for link in p.links:
|
||||
u=urlsplit(link)
|
||||
if u.scheme and u.scheme not in ('http','https'):continue
|
||||
if u.netloc and u.netloc!='servaki.online':continue
|
||||
target=PUBLIC/u.path.lstrip('/') if u.path.startswith('/') else PUBLIC/Path(rel).parent/u.path
|
||||
if not u.path:target=PUBLIC/rel
|
||||
if target.is_dir():target=target/'index.html'
|
||||
assert target.is_file(),(rel,'missing link',link)
|
||||
if u.fragment and target.suffix=='.html':
|
||||
assert unquote(u.fragment) in Page(target.read_text(encoding='utf-8')).ids,(rel,'missing fragment',link)
|
||||
for im in p.images:
|
||||
assert 'alt' in im,(rel,'missing image alt')
|
||||
if not im['src'].startswith('https://mc.yandex.ru/'):
|
||||
assert im.get('width') and im.get('height'),(rel,'missing dimensions')
|
||||
return p
|
||||
def main():
|
||||
files=sorted(PUBLIC.rglob('*.html'));titles=[];descriptions=[];checks=[]
|
||||
scenes=json.loads((ROOT/'content/flow-scenes.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(card_copy)==set(scenes),'missing or extra homepage card description'
|
||||
css=(PUBLIC/'assets/editorial/editorial.css').read_text(encoding='utf-8')
|
||||
assert '@media(prefers-reduced-motion:reduce){.flow-link span{animation:none' in css,'missing static motion alternative'
|
||||
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)
|
||||
if rel!='index.html' and rel.count('/')==2:
|
||||
slug=Path(rel).parent.name
|
||||
assert text.count('class="system-flow"')==1,(rel,'equipment diagram')
|
||||
assert text.count('class="flow-device"')==3 and text.count('class="flow-link"')==2,(rel,'device and traffic path')
|
||||
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')
|
||||
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)
|
||||
assert served.status==200 and served.read()==file.read_bytes(),(rel,'HTTP mismatch')
|
||||
checks.append({'path':route,'http':200,'h1':p.h1,'images':len(p.images),'schema_blocks':len(p.ld),'sha256':hashlib.sha256(file.read_bytes()).hexdigest()})
|
||||
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')
|
||||
for slug,description in card_copy.items():
|
||||
assert description in home,(slug,'homepage card description not visible')
|
||||
home_schema=Page(home).ld[0]['@graph']
|
||||
plans=next(item for item in home_schema if item.get('@id')=='https://servaki.online/#plans')
|
||||
expected_prices={'Старт':9000,'Бизнес':21000,'Премиум':45000}
|
||||
actual_prices={offer['name']:int(offer['priceSpecification']['price']) for offer in plans['offers']}
|
||||
assert actual_prices==expected_prices,('structured tariff prices',actual_prices)
|
||||
for price in expected_prices.values():
|
||||
visible=f'<span class="amount">{price:,}</span>'.replace(',', ' ')
|
||||
assert home.count(visible)==1,('visible tariff price',price)
|
||||
sample=PUBLIC/'services/networks-vpn/index.html'
|
||||
broken=sample.read_text(encoding='utf-8').replace('/assets/editorial/photo-networks-vpn-960.webp','/assets/editorial/definitely-missing.webp',1)
|
||||
try:check(broken,'services/networks-vpn/index.html')
|
||||
except AssertionError:negative=True
|
||||
else:raise AssertionError('Negative control did not detect broken image')
|
||||
locations=[e.text for e in ET.parse(PUBLIC/'sitemap.xml').iter() if e.tag.endswith('}loc')]
|
||||
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}
|
||||
(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()
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Check the public Beget release over HTTP and inspect the TLS certificate."""
|
||||
from pathlib import Path
|
||||
from urllib.request import build_opener, ProxyHandler, HTTPSHandler, Request
|
||||
import json
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PUBLIC = ROOT / 'public'
|
||||
sys.path.insert(0, str(ROOT / '.deps'))
|
||||
from cryptography import x509
|
||||
|
||||
BASE = 'https://servaki.online'
|
||||
opener = build_opener(ProxyHandler({}), HTTPSHandler(context=ssl._create_unverified_context()))
|
||||
|
||||
|
||||
def get(path):
|
||||
request = Request(BASE + path, headers={'Cache-Control': 'no-cache', 'Accept-Encoding': 'identity'})
|
||||
with opener.open(request, timeout=20) as response:
|
||||
return response.status, response.read(), response.url
|
||||
|
||||
|
||||
def certificate():
|
||||
with socket.create_connection(('servaki.online', 443), timeout=12) as conn:
|
||||
with ssl._create_unverified_context().wrap_socket(conn, server_hostname='servaki.online') as tls:
|
||||
cert = x509.load_der_x509_certificate(tls.getpeercert(binary_form=True))
|
||||
valid = True
|
||||
try:
|
||||
with socket.create_connection(('servaki.online', 443), timeout=12) as conn:
|
||||
with ssl.create_default_context().wrap_socket(conn, server_hostname='servaki.online'):
|
||||
pass
|
||||
except ssl.SSLError:
|
||||
valid = False
|
||||
return valid, cert.not_valid_after_utc.isoformat()
|
||||
|
||||
|
||||
def main():
|
||||
pages = sorted(PUBLIC.rglob('*.html'))
|
||||
selected = pages + [PUBLIC / 'robots.txt', PUBLIC / 'sitemap.xml']
|
||||
selected += [PUBLIC / 'assets/editorial/editorial.css',
|
||||
PUBLIC / 'assets/editorial/photo-networks-vpn-960.webp',
|
||||
PUBLIC / 'assets/editorial/networks-vpn-480.webp']
|
||||
mismatches = []
|
||||
for file in selected:
|
||||
rel = file.relative_to(PUBLIC).as_posix()
|
||||
path = '/' if rel == 'index.html' else '/' + rel.removesuffix('index.html')
|
||||
try:
|
||||
status, body, final_url = get(path)
|
||||
if status != 200 or body != file.read_bytes() or not final_url.startswith(BASE + '/'):
|
||||
mismatches.append({'path': path, 'status': status, 'bytes': len(body)})
|
||||
except Exception as error:
|
||||
mismatches.append({'path': path, 'error': type(error).__name__ + ': ' + str(error)[:130]})
|
||||
valid, not_after = certificate()
|
||||
report = {'checked': len(selected), 'exact_matches': len(selected) - len(mismatches),
|
||||
'tls_valid': valid, 'tls_not_after': not_after, 'mismatches': mismatches}
|
||||
(ROOT / 'live-verification.json').write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
print(json.dumps(report, ensure_ascii=False))
|
||||
if mismatches or not valid:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user