01О проекте
Карта тревог показывает, где сейчас воздушная тревога: по областям и по громадам, с движением целей на карте, линией фронта, термоточками со спутников и лентой мониторинговых каналов внизу. Её читают те, кому ответ нужен за секунды, и читают с телефона, ночью, на плохом мобильном интернете.
При этом страница — только часть продукта. Те же данные уходят наружу открытым JSON-API с ключами и лимитами и виджетом, который другие сайты ставят к себе в рамку. Мы спроектировали и сделали всё: сборщики, хранение, API, карту и виджет.
02Контекст и проблема
Данных, которые отвечают на вопрос «есть ли тревога здесь», не существует в одном месте. Официальные тревоги приходят по областям. Локальные тревоги громад — из другого сервиса. Где цели, словами пишут мониторинговые каналы. Линию фронта публикует картографический проект. Пожары видят спутники. Пять источников, пять форматов, пять разных представлений о том, что такое «сейчас».
Вторая проблема — молчание. Любой из источников может встать: канал замолчал, API начал отваливаться по таймауту, — и честное поведение здесь сказать об этом, а не продолжать рисовать вчерашнюю картину. Карта, которая выглядит спокойной, потому что у неё умер источник, хуже, чем отсутствие карты.
03Задачи проекта
- одна картина, собранная из пяти независимых источников, у каждого свои часы обновления;
- ответ за секунды с телефона, на слабой связи, ночью;
- данные, которые могут уходить с сайта: открытый API с ключами, лимитами и тарифами, и виджет для встраивания;
- три языка, потому что аудитория не только местная;
- замолчавший источник должен быть виден как замолчавший, а не как спокойствие;
- адрес самого сервера не должен доходить до источников, которые он читает.
04Пять источников — одна картина
- Официальные тревоги по областям — основа карты;
- локальные тревоги громад и районов, из отдельного сервиса;
- мониторинговые каналы в Telegram — читаются и через публичные превью, и, для закрытых каналов, по самому протоколу мессенджера;
- линия фронта — от картографического проекта, который её публикует;
- термоточки со спутников NASA — пожары, видимые с орбиты.
Сборщики работают двумя долгоживущими воркерами под системным супервизором, каждый перезапускается сам по себе, и пишут в локальное хранилище. Сайт никогда не ходит к источнику напрямую: он читает то, что сборщики уже записали, — поэтому медленный источник делает карту более старой, а не недоступной.
05Четыре файла вместо одной базы
Данные лежат в четырёх отдельных файлах SQLite: журнал тревог, подписки на пуши и их состояние по регионам, события карты с лентой, ключи API со счётчиками лимитов. Общей базы нет намеренно — отказ одного файла не должен уносить остальные. Недоступны подписки — карта всё равно рисуется; занят склад ключей — страница всё равно отвечает.
В те же файлы пишет общая панель управления группы проектов — выпуск ключа, сброс состояния тревоги, — поэтому каждое соединение открывается с ожиданием занятости. По умолчанию SQLite на занятой базе сдаётся мгновенно, и это «мгновенно» здесь означало бы страницу с ошибкой в момент, когда воркер всего лишь дописывал строку.
06Трое часов на одной странице
Разные данные стареют с разной скоростью — и обновляются с разной: тревоги раз в 15 секунд, движущиеся цели раз в 10, лента раз в 20. Один общий интервал был бы неправильным сразу в обе стороны: слишком медленным для целей, расточительным для ленты и лишним запросом каждые несколько секунд для тысяч открытых вкладок.
У каждого блока есть и своё «обновлено». Страница, которая молча перерисовывается, приучает доверять ей ровно тогда, когда доверять не стоит; остановившаяся отметка времени — самый быстрый способ сказать «эта часть устарела».
07Открытый API с ключами и тарифами
Карта публикует то, что знает: текущие тревоги, события, аналитику с тепловой картой «область × корзина времени», ритм суток, типы целей и направления. Аноним получает скромный лимит в минуту; ключ поднимает его, а тарифы растут ступенями для тех, кто на этом строит.
Ключи выпускаются из общей панели управления и никогда не набираются руками, а расход считается по ключу и по дням. Одна точка намеренно закрыта для всех, кроме владельца ключа, — та, что рассказывает ключу его тариф, лимит и расход. Остальное открыто, потому что общественный сервис, прячущий свои данные, — общественный только наполовину.
08Виджет, который живёт на чужих сайтах
Виджет — та часть продукта, которая уходит из дома. Два фиксированных размера — полоса состояния и квадратная панель, три языка, две темы и политика вложения в рамку, написанная специально так, чтобы встраивание было разрешено здесь и нигде больше на сайте: карту и виджет в рамку брать можно, кабинет и страницы поддержки — нельзя.
Раз он живёт в рамке, у виджета нет своих кук и он не выбирает поколение интерфейса: выглядит одинаково у всех. Это решение, а не ограничение — виджет, который рисуется по-разному в зависимости от прежних выборов посетителя, невозможно было бы поддерживать на чужой странице.
09Пуши, которые знают ваш регион
Подписка привязана к региону, а состояние каждого региона хранится рядом с подписками. Именно это делает уведомление событием на границе — когда тревога в вашей области началась или закончилась, — а не на каждое обновление источника. В этой разнице весь продукт: одно — сигнал, другое — шум, который отключают в первый же день.
Поверх тех же данных работает нативное приложение, и ходит оно в своё app-API, а не по страницам сайта. Отдельный адрес живости отвечает реестру приложений: проверка живости не должна стоить дороже той работы, которую проверяет.
10Три языка, включая виджет
Карта говорит по-украински, по-английски и по-русски — и так же говорят справочник API, страницы кабинета и виджет. Новая строка интерфейса заводится сразу на три языка, а проверка считает недостающие: их должно быть ноль. Наполовину переведённые интерфейсы не сообщают о себе — они тихо показывают один язык внутри другого и выглядят нормально для того, кто их писал.
Названия регионов — отдельная история: они приходят из источников в своём написании, поэтому карта держит собственный словарь, а не доверяет тому, что приехало в ленте.
11Читать источники, не показывая свой адрес
Всё, что забирают сборщики, уходит через SOCKS5-прокси — включая протокол мессенджера для закрытых каналов, где проксирование сделано кодом, без лишней зависимости. Адрес самого сервера не доходит до источника, а источник, решивший кого-то заблокировать, блокирует прокси, а не машину, которая отдаёт карту.
Ограничитель частоты стоит перед API по той же причине, только наоборот: публичная точка с живыми данными притягивает скрейперов, и лимит на ключ в минуту — это то, что оставляет общественный сервис пригодным для тех, ради кого он написан.
12Технологии
- PHP 8.3 без фреймворка: маленький роутер, точки API и рендеры;
- воркеры на Python под системным супервизором — один только на стандартной библиотеке, второй говорит по протоколу мессенджера;
- SQLite, четыре отдельных файла, у каждого своя причина существовать;
- Leaflet для карты, ассеты с хешем в имени, виджет фиксированных размеров;
- веб-пуши с состоянием по регионам, нативное приложение на тех же данных;
- SOCKS5 наружу для каждого исходящего запроса, лимиты на ключ перед API.
13Результат
Карта, которая отвечает за секунды и продолжает отвечать, когда один из пяти источников встал. Данные, которые можно забрать к себе: открытый API с ключами и тарифами и виджет на трёх языках в двух темах. Уведомления, срабатывающие на границе события, а не на каждый опрос. Нативное приложение на тех же данных и панель, из которой выпускают ключи и сбрасывают состояния.
14Выводы
Три вещи, которые мы унесём в любой проект с живыми данными. Отделять сборщик от страницы: сайт, который идёт к источнику, пока посетитель ждёт, — это сайт, падающий вместе с источником. Давать каждому блоку свои часы и свою отметку времени: один интервал неправильный сразу в обе стороны, а молчаливая перерисовка прячет устаревание. Говорить, когда не знаешь: пустой блок, который в этом признаётся, стоит дороже спокойной на вид карты со вчерашними данными.
И один продуктовый вывод: как только данные можно встроить к себе, продуктом становится виджет. Ему нужны фиксированные размеры, своя политика рамки и переводы — та же забота, что и главной странице, а не остаток от неё.
Если у вас есть живые данные и нужны карта, API и виджет, который смогут взять другие сайты, — это работа, которую мы делаем целиком.



