Снимки status-page без выдуманного downtime
Снимки status-page без выдуманного downtime
Проверено 5 октября 2026 года: 24 офлайн-теста на Python 3.13.5, стандартная библиотека, без сторонних зависимостей. Код рассчитан на Python 3.10+, но другие версии в этой проверке не запускались. Версия формата снимка — schema_version: 1.
Это минимальный рабочий инструмент capture → ручная разметка → review → diff, а не универсальный парсер status-page. Он сохраняет источник и автоматически сравнивает подтверждённые оператором поля. HTML, JavaScript и смысл сообщений провайдера автоматически не интерпретируются.
Исходники: snapshot.py и test_snapshot.py. До слияния добавляющего их PR файлы доступны в его ветке, а не в main.
Зачем нужен отдельный снимок
В журнале Timeweb Cloud есть одинаковые наблюдения статусов msk-1 за 30 сентября и 5 октября. Они не доказывают непрерывный пятидневный простой. В журнале FirstVDS отметка выполнения обслуживания опубликована позднее самого окна; это не время окончания воздействия на каждого клиента.
Инструмент следует методике журнала: дата получения источника, дата публикации, плановое окно и фактическое воздействие хранятся отдельно. Он реализует часть задачи #190 о происхождении данных и воспроизводимости, но не закрывает её для всех рецептов сайта.
Что сохраняется
| Файл | Содержание |
|---|---|
raw.bin | Байты HTTP-ответа, не отрендеренный браузером DOM |
capture.json | Источник, провайдер, checked_at в UTC, SHA-256, версия формата и отдельные HTTP-заголовки |
observations.template.json | Шаблон с тем же источником и хешем, пустой список наблюдений |
observations.json | Созданная оператором рабочая копия шаблона |
reviewed.json | Проверенные по схеме наблюдения, время получения источника и отдельное reviewed_at |
checked_at фиксирует окончание загрузки по системным часам машины; проверьте синхронизацию часов. HTTP Date, Last-Modified, ETag, Age сохраняются отдельно и не становятся датой события. captured_unreviewed в исходном manifest описывает результат загрузки и не заменяется зелёным статусом после HTTP 200. Наличие reviewed.json означает отдельный этап проверки.
SHA-256 позволяет обнаружить изменение сохранённых байтов, но не доказывает достоверность сообщения провайдера или правильность ручной разметки. Это не цифровая подпись провайдера и не независимое измерение доступности.
Проверка кода без сети
Из корня локальной копии репозитория:
python3 --version
python3 -m unittest discover -s scripts/provider-status-snapshot -v
При подготовке рецепта получено:
Ran 24 tests
OK
Фикстуры синтетические и создаются тестами во временных каталогах. Проверены сохранение источника, ошибки HTTP/сети, запрет перезаписи, несовпадение хеша и источника, пустые наблюдения, неизвестный часовой пояс, UTC и дробные секунды, дубли идентификаторов, исчезновение записи, отсутствие семантических изменений и экранирование HTML в отчёте. Тесты не обращаются к провайдерам и не подтверждают работоспособность живых страниц.
1. Получить источник
В текущий фиксированный список URL включены firstvds, firstvds-works, selectel, timeweb-cloud и google-search. Это разрешённые адреса загрузки, не проверенные автоматические адаптеры разметки всех этих сайтов.
Пример для POSIX shell, команды выполняются из корня репозитория:
SNAPSHOT_ROOT="$(mktemp -d)"
printf 'Каталог снимков: %s\n' "$SNAPSHOT_ROOT"
python3 scripts/provider-status-snapshot/snapshot.py capture \
--source firstvds-works --out "$SNAPSHOT_ROOT/first"
Выберите существующий приватный каталог вне Git для длительного хранения вместо временного каталога. --out должен указывать на новый подкаталог: перезаписи нет. При ошибке запроса прежний снимок остаётся неизменным, команда завершается с ненулевым кодом; это ошибка получения источника, не авария провайдера. Текст ошибки сохраните в собственном журнале попыток. Новый reviewed.json в этом случае не создаётся.
Загрузка использует один GET к адресу из списка, обычную проверку HTTPS, без cookies, авторизации, автоматических proxy и переходов по redirect. Максимум ответа — 2 МиБ. Timeout 20 секунд относится к блокирующим сетевым операциям, а не гарантированному общему пределу времени запуска. Пустой ответ, неожиданный HTTP-код, сжатый ответ вопреки запросу identity или превышение лимита отклоняются.
При 403/challenge не отключайте TLS и не обходите защиту сайта. Если пришла HTML-оболочка без нужных данных или страница входа с HTTP 200, оставьте снимок непроверенным. Для JavaScript-only страницы потребуется отдельный проверенный API/браузерный адаптер — его в этой версии нет. Не переносите данные из другого ответа в этот снимок: источник и хеш должны соответствовать именно прочитанным байтам.
2. Разметить прочитанные данные
Откройте raw.bin как текст в редакторе, не исполняйте сохранённый HTML/JavaScript. Скопируйте шаблон:
cp "$SNAPSHOT_ROOT/first/observations.template.json" \
"$SNAPSHOT_ROOT/first/observations.json"
Сохраните сгенерированные schema_version, provider, source_url и source_sha256. Заполните entities только сведениями, реально найденными в этом ответе. Следующий фрагмент — синтетический пример значения entities, не сообщение FirstVDS; не вставляйте его как результат живой проверки:
[
{
"kind": "maintenance",
"id": "local:example:test-1:2026-01-01",
"component": "Тестовый компонент",
"service": "Синтетический сервис",
"location": "test-1",
"status": "scheduled",
"published_at": null,
"started_at": null,
"ended_at": null,
"scheduled_start": "2026-01-01T10:00:00+03:00",
"scheduled_end": "2026-01-01T11:00:00+03:00",
"note": "Синтетический пример; плановое окно не является фактическим простоем"
}
]
Для каждого объекта обязательны kind, id и непустой status. Типы — component, incident, maintenance; monitoring хранится в status соответствующего объекта, а не превращает его в новый инцидент. Дополнительные поля: component, service, location, note и пять времён из примера. Неизвестные поля, в том числе придуманное downtime, отклоняются.
По возможности используйте постоянный ID карточки провайдера. Для компонентного наблюдения без официального ID используйте стабильный локальный ключ, например local:compute:msk-1; он явно не является официальным incident ID. В новом снимке сохраняйте те же ключи и область отбора. Изменение ключа будет выглядеть как исчезновение одного и появление другого объекта. Несколько локаций или сервисов не объединяйте под одним ключом, если их статусы различаются.
provider_timezone — описание подтверждённого часового пояса источника или null. Это поле не подставляет offset автоматически. Все известные временные поля принимаются только в ISO 8601 с явным UTC offset и нормализуются в UTC. Если у служебной отметки часовой пояс неизвестен, оставьте соответствующее поле null, а буквальный текст и ограничение запишите в note. Не подставляйте дату публикации вместо started_at, время закрытия карточки вместо восстановления клиента или scheduled_end вместо ended_at.
3. Проверить разметку и сравнить
python3 scripts/provider-status-snapshot/snapshot.py review \
--capture "$SNAPSHOT_ROOT/first" \
--observations "$SNAPSHOT_ROOT/first/observations.json"
Команда проверяет схему, соответствие источнику и сохранённому SHA-256, непустые записи, уникальность (kind, id), временные форматы и порядок начала/окончания. Это техническая валидация после ручной содержательной проверки, а не автоматическое подтверждение истинности статусов. Готовый reviewed.json не перезаписывается; ошибочную разметку исправляйте в отдельной копии каталога с сохранением исходного артефакта.
При следующей разрешённой проверке повторите capture и ручную разметку в новом каталоге second. Используйте его собственный шаблон и хеш, а не копию старого готового reviewed.json. Затем:
python3 scripts/provider-status-snapshot/snapshot.py review \
--capture "$SNAPSHOT_ROOT/second" \
--observations "$SNAPSHOT_ROOT/second/observations.json"
python3 scripts/provider-status-snapshot/snapshot.py diff \
"$SNAPSHOT_ROOT/first" "$SNAPSHOT_ROOT/second" \
> "$SNAPSHOT_ROOT/diff.md"
Сравнение разрешено только для одного провайдера, URL и версии схемы с неубывающим временем снимков. Изменение HTML-футера, HTTP-заголовков или времени получения само по себе не создаёт семантическое событие. Пустой список не считается доказательством исправности: если нужных данных нет, оставьте снимок непроверенным.
| Результат | Допустимый вывод |
|---|---|
changed | Изменились выбранные поля; сохранены значения до и после |
newly_observed_not_necessarily_new | Запись впервые попала в эту выборку; она не обязательно новая |
not_observed_not_resolved | Запись отсутствует в новой выборке; восстановление не доказано |
| Изменений выбранных полей нет | Наблюдения совпали; непрерывность статуса между ними неизвестна |
Отчёт — Markdown-черновик с URL, временем проверок и хешами; он не вычисляет uptime, длительность аварии или рейтинг провайдера. HTML и обратные кавычки из полей экранируются, но перед публикацией всё равно нужно проверить смысл и отсутствие чувствительных данных. Два разных URL одного провайдера, например общую панель и архив работ, сопоставляются редактором отдельно, а не одним автоматическим diff.
Границы проверки и публикации
При подготовке этого рецепта код проверен офлайн, включая сетевые ошибки через mock. Живой capture из данной рабочей среды не подтверждён; доступ к источникам для новостных заметок выполнялся отдельно браузерным инструментом. Нельзя утверждать, что скрипт успешно загрузил все перечисленные URL. Полная сборка VuePress локально не запускалась из-за недоступности клонирования GitHub; её результат оценивается отдельно в CI PR.
Скрипт не создаёт cron, issues, PR или коммиты и не изменяет ресурсы провайдеров. Не запускайте частый опрос без проверки правил источника. Сырые страницы, логи попыток и рабочие снимки храните вне публичного репозитория с ограниченными правами; не добавляйте токены, приватные кабинеты, клиентские IP и данные заявок. В Git публикуйте только проверенное описание и обезличенные синтетические примеры.
