ХрюХрюКар: как перестать беспокоиться, начать создавать сервисы и обзавестись друзьями

в 8:05, , рубрики: django, timeweb_статьи, vue.js, автомобиль, газон, парковка, ПДД, хрюхрюкар, штрафы
ХрюХрюКар: как перестать беспокоиться, начать создавать сервисы и обзавестись друзьями - 1

Привет!

Мы хотели научиться создавать сервисы от момента возникновения идеи и до этапа эксплуатации, попутно освоив новые технологии.

В итоге получился экспериментальный проект «ХрюХрюКар» — сервис для борьбы с неправильной парковкой, работающий под лозунгом «Хорошие ребята говорят 'Bla-Bla' и не ставят машину на зелёной зоне».

В этой статье мы расскажем, как выбирали проект, на решение каких задач нацелен «ХрюХрюКар», какие технологии мы использовали, какие трудности возникали и что получилось в итоге.

Ну и поделимся всеми исходниками, конечно. Если вам не терпится посмотреть, то вот исходники, а вот что получилось.


В статье мы не будем вставлять блоки кода, который у нас есть в репозитории, а просто будем ссылаться на его редакцию на момент написания статьи. Так каждый сможет посмотреть то, что интересно именно ему, поэтому ссылок будет много. Также будет немного ссылок на публикации в ВК и Telegram, имейте это в виду, переходя по ссылкам.

И да, мы здраво воспринимаем критику и всегда готовы учиться у более опытных коллег, поэтому комментарии приветствуются, как и вопросы в Issues.

❯ Чему мы хотели научиться

Я два года работаю в IT (Fullstack: Django, Go, Vue) и по работе мне приходилось сталкиваться с разными технологиями, но я ни разу не писал сервисы полностью, всегда уже были готовые проекты, в которых нужно было что-то дорабатывать. Также у меня есть брат, который вообще не имел опыта в IT, но хотел научиться программировать (с уклоном в Django).

Чтобы приобрести больше уверенности в вопросах создания сервисов, мы решили сделать проект с нуля. Брату хотелось понять как создавать backend на Django, работать с базами данных, а также разбираться с асинхронными задачами, ну а мне было интересно создать PWA на Vue3, научиться работать с картами, набить шишек на DevOps и в целом разработать сервис полностью.

❯ Как выбирали проект

Хотелось сделать проект, который был бы полезен городу и в то же время интересен для нас.

С выбором направления проблем не возникло: несколько лет назад я участвовал в работе административной комиссии и проблема неправильной парковки меня давно волновала. Были попытки внутренней автоматизации процесса, но они не увенчались успехом из-за полного отсутствия финансирования, нехватки времени и опыта в IT.

В то время все закончилось на том, что мы фотографировали нарушения на OpenCamera, после чего я загружал снимки в QGIS и скриптом извлекал данные из EXIF-тегов, осуществлял обратное геокодирование, заполняя слой. Оставалось разобраться с распознаванием автомобильных номеров и разработать функционал формирования документов (запросов/протоколов/писем), но в связи со сменой места работы, работа над проектом была остановлена.

В июле 2022 года, в рамках эксперимента, мне удалось наладить карту нарушений, но работала она очень плохо, да и выглядела ужасно:

Первая версия карты ХрюХрюКар, все что осталось

Первая версия карты ХрюХрюКар, все что осталось

Это по сути был QGIS-сервер и фронт на базе LizMap. Я все также фотографировал нарушения на OpenCamera, грузил их в QGIS, а также научился формировать заявления в PDF и публиковать карту. Учитывая наличие базовых знаний в C++/Qt я решил написать простейший клиент под Android. Немного помучившись, я понял что не тяну и делаю все не так, поэтому проект был снова спрятан на самую дальнюю полку.

В августе 2023 года я уже год как был занят в коммерческой разработке и понял что самое время осуществить третий подход, но уже с братом. На этом этапе мы выступали в роли граждан, полномочия которых ограничены отправкой заявления в уполномоченный орган, но даже эта задача оказалась не такой простой, как кажется.

Чтобы привлечь нарушителя к ответственности, вам необходимо:

  1. Зафиксировать факт нарушения, время и место его совершения;

  2. Понять кто уполномочен принимать заявления по конкретному типу нарушений на данной территории;

  3. Составить заявление с учетом всех юридических тонкостей;

  4. Отправить заявление в уполномоченный орган и дождаться ответа.

Вроде бы для гражданина все просто, но на практике все оказалось сложнее. Многие считают что все нарушения правил парковки относятся к полномочиям МВД (ГИБДД/ГАИ), но на самом деле это не так.

Нарушения, связанные с размещением автомобиля на территории, занятой зелеными насаждениями или на территории детских/спортивных площадок, рассматриваются в основном административными комиссиями, созданными в муниципалитетах, в рамках возложенных на них полномочий.

Кто-то из таких комиссий принимает заявления по электронной почте, у кого-то работает СЭД и есть форма для обращений на сайте, кто-то использует ПОС Госуслуг, ну а некоторые поддерживают сразу несколько способов обращения, либо вовсе принимают заявления только на бумаге...

Обращение в МВД (ГИБДД/ГАИ) — это вообще отдельная история. При разработке их формы обращения, программисты сделали все для того, чтобы граждане вообще не обращались к ним. Например, в форме обращения, «для борьбы со спамом», отключена возможность использования буфера обмена, а максимальный размер вложений в 30Мб является по сути запретом на отправку видео.

Все это, по факту, от спама защищает чуть больше чем никак, но чинит серьезные препятствия для простых граждан: если вы собираетесь обратиться в МВД, то вам, как правило, текст заявления готовит юрист. В результате, с учетом запрета на использование буфера обмена, вам приходится вручную весь текст набирать в форме.

При отправке обращений через ПОС Госуслуг есть также решения «для борьбы со спамом», о чем напишем чуть ниже.

Ко всему перечисленному добавляется банальный страх деанонимизации. Ведь для отправки заявления, вам нужно указать свои персональные данные, а это значит, что нарушитель, на этапе административного производства, узнает автора обращения и есть риск преследования.

Мы решили создать сервис, который позволит горожанам «в один клик» фиксировать факт правонарушения, а все остальные аспекты взять на себя.

В результате было интересно проанализировать насколько горожане готовы участвовать в жизни города, а также попытаться подискутировать с нарушителями в соцсетях и поработать с их возражениями.

❯ Какую задачу решает проект

В большинстве регионов России проблема неправильной парковки является одной из самых актуальных. Дворы и тротуары забиты личными автомобилями, а водители, нарушающие правила, придумывают себе массу оправданий, которые по сути являются отговорками:

Отговорка

Фактическая ситуация

«Была неотложная ситуация и я впервые тут автомобиль поставил, а так обычно я на стоянке ставлю»

Открывая Google Street View, примерно в 50% случаев, оказывалось, что автомобиль стоит там уже не первый год.

«Все стоянки забиты, а во дворе нет другого места, кроме зеленой зоны»

После нескольких комиссий мне стало интересно, действительно ли все стоянки заняты. Проехав несколько стоянок вокруг района, где были нарушения, я убедился, что это не так.

«Я не знал, что это зеленая зона, там грязь, вы даже там траву никогда не сажали»

В этих случаях зачастую удавалось на Google Street View или Google Earth найти материалы, где отчетливо видно, что территория некогда была зеленой зоной. Пылеватые проплешины среди луговой травы и повреждения бордюра, соответствующие колесной базе автомобиля легко позволяют построить тут причинно-следственную связь.

«Я приехал во двор а там не оказалось свободного места»

Места в сложившейся застройке являются гостевыми, а не личными, владельцам авто следует хранить свои автомобили в гаражах и на стоянках. Это четко указано в местных нормативах градостроительного проектирования, да и положения СанПиН свидетельствуют о том же.

«Вы не тем занимаетесь! Сделайте для начала всем парковки во дворах, прежде чем штрафовать!»

Сделали.

В городе Балаково, по состоянию на 2024 год ориентировочное количество автомобилей составляет 70000 единиц. Сейчас нет возможности точно посчитать сколько из них хранятся в нарушение правил во дворах на зеленых зонах и тротуарах, но по моим наблюдениям это не менее 10% от общего количества.

Очень много автомобилей стоят неделями и месяцами в дворах без движения, что по сути превращает наши дворы в гараж. Большинство некогда зелёных зон превратились в площадки с открытым грунтом, что является одним из главных источников пыли.

В чем причины? Я думаю здесь подействовал целый комплекс факторов. В свое время власти отпустили ситуацию и автомобили постепенно переместились из гаражей и со стоянок во дворы, что превратилось в привычку. Сейчас автомобилисты уже в корне не согласны с тем, что личные авто во дворах сложившейся застройки хранить не получится.

Также мы часто слышим про отсутствие мест на стоянках. Мы решили разобраться с этим доводом и получили контакты представителей всех 30-и стоянок в г. Балаково и начали работу над картой стоянок, работающей на базе ХХК.

На данный момент мы обзвонили не все стоянки, но результат уже достоин внимания: из 17 стоянок по которым мы уже получили данные, только на одной стоянке нет мест, а на остальных — 40-60% свободно! С гаражами ситуация примерно такая же: по данным председателей кооперативов, 30-40% гаражей в городе — брошены и практически не используются.

Причины, которые нам видятся основными:

  • во дворе ставить «выгодней» (от предупреждения до 5000 руб. штрафа раз в 1-2 года против 1200-1600 рублей в месяц за стоянку);

  • до стоянки или гаража нужно ходить, а «мы привыкли от двери до двери на авто, это же удобно, 21-й век!»;

  • низкий шанс получения наказания: за 2023 год, согласно отчетам, на весь город ~300 админ. протоколов (и это не только по парковке);

  • нет общественного порицания: люди либо считают это нормой, либо просто молчат;

  • нехватка кадров в муниципалитете, чтобы фиксировать большое количество нарушений по всему городу. В Балаково это 27 человек, уполномоченных составлять протоколы (помимо основных обязанностей) на тысячи нарушений;

  • отсутствие полноценной информационной работы. Зачастую власти не способны напрямую говорить горькую правду жителям.

В рамках проекта «ХрюХрюКар» мы постарались охватить все описанные выше проблемы.

Посмотреть 12 примеров того, с чем борется ХХК можно под спойлером
Поставил на тротуар, подумав «И так сойдет!». https://xxkap.app/?violation=2429c4af-14ea-45ca-b744-853054ec9bb5

Поставил на тротуар, подумав «И так сойдет!».
https://xxkap.app/?violation=2429c4af-14ea-45ca-b744-853054ec9bb5
Не смог найти стоянку.https://xxkap.app/?violation=c0a180ba-ac71-4e18-bc16-840b9097a8e4

Не смог найти стоянку.
https://xxkap.app/?violation=c0a180ba-ac71-4e18-bc16-840b9097a8e4
Тоже не мог найти стоянку.https://xxkap.app/?violation=07e4abaa-5895-4d9e-81b4-c4e56ee84d33

Тоже не мог найти стоянку.
https://xxkap.app/?violation=07e4abaa-5895-4d9e-81b4-c4e56ee84d33
На фоне зоны, которая была зелёной.https://xxkap.app/?violation=aae24501-0f2a-4caf-913f-12628054035d

На фоне зоны, которая была зелёной.
https://xxkap.app/?violation=aae24501-0f2a-4caf-913f-12628054035d
В десяти метрах левее полупустая стоянка. Ворота за авто —  вход в самый посещаемый парк «Энергетик».https://xxkap.app/?violation=6e2025c6-01d9-4d87-9dac-94ec8067ed6a

В десяти метрах левее полупустая стоянка. Ворота за авто — вход в самый посещаемый парк «Энергетик».
https://xxkap.app/?violation=6e2025c6-01d9-4d87-9dac-94ec8067ed6a
Это тротуар. Саратовское шоссе, 91, Балаково. До полупустой стоянки — 2 минуты пешком.https://xxkap.app/?violation=a21770f8-bb37-43ed-bd78-47a6f0604d2b

Это тротуар. Саратовское шоссе, 91, Балаково. До полупустой стоянки — 2 минуты пешком.
https://xxkap.app/?violation=a21770f8-bb37-43ed-bd78-47a6f0604d2b
Также у входа в парк «Энергетик». На заднем плане «карман», а правее за кадром —  полупустая стоянка.https://xxkap.app/?violation=6ea5829e-c959-480c-990b-3759521eba12

Также у входа в парк «Энергетик». На заднем плане «карман», а правее за кадром — полупустая стоянка.
https://xxkap.app/?violation=6ea5829e-c959-480c-990b-3759521eba12
На фоне стоянки, где масса свободных мест.https://xxkap.app/?violation=eab00b22-a850-41cf-ab3c-55bf22ab6543

На фоне стоянки, где масса свободных мест.
https://xxkap.app/?violation=eab00b22-a850-41cf-ab3c-55bf22ab6543
По словам жителей он стоит так уже не первый год.https://xxkap.app/?violation=91c6e70f-846d-467e-b6f4-88d5bd37b8c4

По словам жителей он стоит так уже не первый год.
https://xxkap.app/?violation=91c6e70f-846d-467e-b6f4-88d5bd37b8c4
Ленина 99А, до полупустой стоянки — 7 минут пешком.https://xxkap.app/?violation=670f9fa6-6c19-4954-8766-6e3e606add93

Ленина 99А, до полупустой стоянки — 7 минут пешком.
https://xxkap.app/?violation=670f9fa6-6c19-4954-8766-6e3e606add93
Срубили бордюры, лишь бы не ходить до стоянки или гаража.https://xxkap.app/?violation=af1e0b53-8b6c-4ee3-9082-705b3c651312https://xxkap.app/?violation=82c45a36-689a-4db9-9144-91e80ea77830

Срубили бордюры, лишь бы не ходить до стоянки или гаража.
https://xxkap.app/?violation=af1e0b53-8b6c-4ee3-9082-705b3c651312
https://xxkap.app/?violation=82c45a36-689a-4db9-9144-91e80ea77830
Кот Вася не одобряет стоянку на тротурае.https://xxkap.app/?violation=e74d6cb4-7c93-4ae2-9d8d-db8f1383ce0d

Кот Вася не одобряет стоянку на тротурае.
https://xxkap.app/?violation=e74d6cb4-7c93-4ae2-9d8d-db8f1383ce0d

❯ Что получилось в результате

Карта нарушений:

При открытии приложения загружается публичная карта нарушений. На карте доступны все нарушения, прошедшие процедуру модерации, то есть те, по которым направлены заявления.

Также на карте есть слой с автомобильными стоянками. Зеленым цветом обозначены стоянки, на которых есть места, а синим — те, куда мы еще не позвонили. Красным цветом отображена стоянка, на которой нет мест. По стоянкам также можно кликать, чтобы посмотреть подробные сведения.

Фиксация нарушений

После авторизации, если вы находитесь на территории, за которой закреплены полномочия по какому-либо типу нарушений, то приложение запустит камеру. В компоненте камеры можно выбрать тип нарушения и сделать снимок.

После отправки снимка, сервер производит распознавание номеров и обратное геокодирование. Вы можете протестировать этот функционал приложения, поставив фиктивное местоположение на своем устройстве где-нибудь на территории города Балаково Саратовской области. Только не забудьте, пожалуйста, сразу после распознавания номеров, удалить снимок, нажав на красную кнопку.

Модерация и отправка заявлений

Пользователю с правами модератора (членство в группе Moderator), а также суперюзеру доступна консоль модерации. Модераторы не получают в очередь модерации фотографии, авторами которых они являются. Суперюзеры — могут модерировать любые материалы.

Модерацию мы сделали в виде степпера, чтобы акцентировать внимание на конкретном в текущем моменте вопросе. Если при распознавании номеров на фотографии обнаружены сразу несколько номеров, то для них создаются отдельные записи в таблице автомобильных номеров и нарушений.

Если отклонить фотографию, то отклоняются все связанные с ней автомобильные номера и, соответственно, нарушения. Также модератор может подтвердить фото (в т.ч. адрес с геолокацией), а затем отклонить часть автомобильных номеров, а часть подтвердить, ну или подтвердить все.

В процессе модерации можно вносить правки в адрес, местоположение снимка, автомобильный номер и тип нарушения. Править ранее подтвержденные данные не получится.

Для удобства модератора, в консоли мы связали карту с Google Street View, чтобы было проще геолоцировать фото.

Если с модератором связан заявитель, то у него доступен последний шаг степпера — генерация и отправка заявления.

Если такой связи нет, то все завершается на шаге подтверждения типа нарушения. В таких случаях, модераторы, с которыми связаны заявители, будут получать уже проверенные материалы и сразу переходить на этап генерации заявления с их данными и подтверждать (отправлять) его или отклонять.

Заявления генерируются в фоне в отдельной celery-task с использованием унифицированного шаблона, либо отдельных шаблонов, которые можно закрепить за полномочиями, используя админ-панель Django.

Рендеринг заявлений производится с использованием встроенного в Django шаблонизатора.

По-хорошему, надо немного подправить код и использовать Jinja2, чтобы рендерить заявления в изолированной песочнице, но пока это не сильно актуально, т.к. создание шаблонов и заполнение данных о заявителях, полномочиях и типах нарушений ведется собственноручно, через админ-панель Django.

Вот как выглядят заявления (3 страницы)
Точное время совершения нарушения видят только адресаты, в приложении мы отдаем пользователям и модераторам только дату.

Точное время совершения нарушения видят только адресаты, в приложении мы отдаем пользователям и модераторам только дату.
ХрюХрюКар: как перестать беспокоиться, начать создавать сервисы и обзавестись друзьями - 16
Панорамы особенно хорошо прикладывать к нарушениям, совершенным зимой. Мы в консоли модератора предусмотрели функцию для подгрузки панорам.

Панорамы особенно хорошо прикладывать к нарушениям, совершенным зимой. Мы в консоли модератора предусмотрели функцию для подгрузки панорам.

После подтверждения заявления, производится его отправка в уполномоченный орган, все также в фоновой задаче celery.

По состоянию на 05/01/2024 у нас реализована автоматическая отправка только по электронной почте, что покрывает ~90% всех нарушений, которые нам присылают через ХрюХрюКар. Отправка в ГИБДД ведется почти в ручном режиме.

У нас есть простейший скрипт на JS, в который мы подставляем текст заявления из панели администратора Django и, перейдя на форму для подачи обращений уполномоченного органа, выполняем этот код в консоли браузера, чтобы заполнить все поля формы. Далее остается прикрепить файл заявления, подтвердить почту и отправить заявление.

Пример кода для заполнения форм МВД
document.querySelector("input[name='post']").value = `Начальнику ОГИБДД МУ МВД России "Балаковское" Саратовской области`;
document.querySelector("input[name='fio']").value = `Корнилову Алексею Ивановичу`;
document.querySelector("input[name='surname']").value = `Иванов`;
document.querySelector("input[name='firstname']").value = `Иван`;
document.querySelector("input[name='patronymic']").value = `Иванович`;
document.querySelector("input[name='email']").value = `ivanovii@xxkapmail.ru`;
document.querySelector("input[name='phone']").value = `+79999999999`;
document.querySelector("textarea[name='message']").value = `Прошу привлечь к административной ответственности, предусмотренной частью 
3 Статьи 12.19. КоАП РФ собственника/водителя транспортного средства, государственный регистрационный знак:  Х578СН197, который 
ХХ.ХХ.2024 в ХХ:ХХ разместил транспортное средство на тротуаре, при отсутствии знака "Парковка (парковочное место)" с одной из табличек 
8.6.2, 8.6.3 и 8.6.6 - 8.6.9., тем самым нарушил пункт 12.2. ПДД РФ, адресный ориентир: напротив 3 подъезда дома 35/2 по улице Степная, 
г. Балаково, Саратовская область, координаты: (52.024486, 47.840909). К настоящему обращению прикладываю заявление за своей подписью 
с материалами фотофиксации правонарушения.`;

Также мы пробовали осуществлять отправку заявлений через ПОС Госуслуг и нас удивили их методы по «борьбе со спамом». Разработчик из непонятных соображений также запретил использование буфера обмена для пользователя и установил следующие ограничения на количество обращений, направляемых через ПОС: не более одного обращения в час и не более двух в сутки.

Данные решения нам показались странными, учитывая тот факт, что для подачи обращения необходимо авторизоваться через аккаунт госуслуг.

С учетом того, что ПОС Госуслуг реализован с использованием реактивных технологий, просто так заполнить форму не получится, ведь необходимо чтобы хранилище пришло к определенному состоянию. В рамках экспериментов мы пришли к такому коду, который на данный момент выполняет свою скромную функцию:

Пример кода для заполнения форм ПОС Госуслуг
const appText = `Прошу привлечь к административной ответственности, предусмотренной частью 15 статьи 8.2 Закона Саратовской 
области от 29 июля 2009 г. № 104-ЗСО «ОБ АДМИНИСТРАТИВНЫХ ПРАВОНАРУШЕНИЯХ НА ТЕРРИТОРИИ САРАТОВСКОЙ ОБЛАСТИ» собственника 
транспортного средства, государственный регистрационный знак:  Е087НМ164, который ХХ.ХХ.2024 в ХХ:ХХ разместил транспортное 
средство на территории, занятой зелеными насаждениями, тем самым нарушил часть 15 статьи 8.2 Закона Саратовской области от 
29 июля 2009 г. № 104-ЗСО «ОБ АДМИНИСТРАТИВНЫХ ПРАВОНАРУШЕНИЯХ НА ТЕРРИТОРИИ САРАТОВСКОЙ ОБЛАСТИ», адресный ориентир: д.8, Степная 
улица, Балаково, Балаковский район, Саратовская область, координаты: (52.016688, 47.828421). К настоящему обращению прикладываю заявление 
за своей подписью с материалами фотофиксации правонарушения.`;

const address = `д.8, Степная улица, Балаково, Балаковский район, Саратовская область`;

const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
const nativeInputCheckedSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'checked').set;

const appealSubjectInput = document.querySelector("#react-select-3-input");
nativeInputValueSetter.call(appealSubjectInput, "Дворы и территории общего пользования");
appealSubjectInput.dispatchEvent(new Event('input', { bubbles: true }));

// Дождемся появления элемента div с id react-select-3-option-0, затем симулируем клик по нему
const observer1 = new MutationObserver((mutationsList) => {
    for (let mutation of mutationsList) {
        if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
            const optionDiv = document.querySelector('#react-select-3-option-0');
            if (optionDiv && optionDiv.textContent.trim() === "Дворы и территории общего пользования") {
                optionDiv.click();
                observer1.disconnect();
                break;
            }
        }
    }
});

observer1.observe(document.body, { childList: true, subtree: true });

const suggest = document.querySelector("#suggest");
nativeInputValueSetter.call(suggest, address);
suggest.dispatchEvent(new Event('input', { bubbles: true }));

const checkboxLabel = document.querySelector('label[for="check-agree"]');
checkboxLabel.click();

const textarea = document.querySelector("textarea");
nativeTextAreaValueSetter.call(textarea, appText);
const event = new Event('input', { bubbles: true });
textarea.dispatchEvent(event);

На данном этапе мы поняли, что заполнять формы мы тоже сможем, но для этого необходимо разработать расширение для браузера, которое будет авторизоваться в ХХК, получать по очереди заявления, открывать необходимые формы и заполнять их.

Почему именно расширение?

Чисто технически, реализовать отправку заявлений напрямую с сервера ХХК, используя эндпоинты уполномоченных органов возможно. Но нам показалось это неправильным с юридической точки зрения. Заявитель все же должен взаимодействовать с формой напрямую, через фронтэнд уполномоченных органов, а мы только можем помочь в её заполнении.

❯ Общие результаты в моменте

За 4,5 месяца мы направили 742 заявления, 28 материалов находится на модерации. Сейчас мы получаем в среднем по 10-20 материалов в день и стараемся около 20-и заявлений в день направлять.

Примерно 90% материалов — нарушения, выражающиеся в размещении автомобиля на зелёной зоне (направляются в администрацию БМР), около 9% — стоянка на тротуаре в нарушение ПДД (полномочия отдела ГИБДД МУ МВД «Балаковское»), 1% делят стоянка на месте для инвалидов, автобусных остановках и пешеходных переходах.

За это время на сайте авторизовалось 277 пользователей, из которых:

  • 62 присылали одно и более нарушений, в том числе;

  • 17 присылали более десяти нарушений, в том числе;

  • 9 присылали более двадцати нарушений, в том числе;

  • 6 присылали более пятидесяти нарушений, в том числе;

  • 4 присылали более ста нарушений.

Следует отметить, что мы неоднократно просили пользователей вкладываться в качество, а не в количество материалов, исходя из ограничений, которые накладываются нюансами бюрократических процедур в рамках административных производств.

На примере некоторых пользователей мы поняли что один человек может зафиксировать сотню нарушений за пару часов прогулки, а мы их можем обработать за 1,5-2 часа. Но мы понимаем, что направление большого количества заявлений в уполномоченный орган по сути является DoS-атакой и вызовет отказ в обслуживании из-за банальной нехватки кадров.

За все время работы к нам зашли 5027 уникальных посетителей ~21000 раз. Основные пики посещаемости после таких вот постов (1, 2) в местной группе автомобильной направленности. Люди находят сайт в Яндексе или нашу группу в ВК и уже от туда приходят на сайт, где ищут свои машины.

Мы оперативно получаем положительные ответы от администрации и не очень оперативно — от МВД. Все ответы стараемся выкладывать в группе, но по мере появления свободного времени. Сейчас на почте ждут обработки 53 ответа от администрации и 3 от ГИБДД, это ~250-350 административных производств (администрация в одном ответе отвечает сразу на несколько наших заявлений).

Минус в том, что мы не получаем входящих номеров от администрации (да и не запрашиваем, если честно), поэтому конкретно связать каждый ответ с конкретным заявлением сейчас не представляется возможным.

Из дополнительных источников метрик следует отметить официальные публикации администрации: 1, 2, 3, 4, 5, 6.

Также нами проводится работа с органами власти всех уровней с целью внедрения в Балаково официального приложения на базе ПАК «Помощник Москвы». Это программное решение имеет статус средства, работающего в автоматическом режиме, что позволяет исключить бюрократию в виде административных производств (штрафы будут приходить через несколько дней прямиком на Госуслуги).

Процесс не быстрый, но мы будем стараться добиться такого внедрения у нас, благо куратор проекта в лице ЦОДД Москвы оказался очень адекватным и на первое же наше письмо ответил приглашением на ВКС, где нам изложили все детали и дали рекомендации для внедрения ПАК ПМ в регионах.

❯ Какие технологии мы использовали

Бэкенд у нас на Django+DRF, база — PostgreSQL с PostGIS. Для очередей задач — Celery с Redis в качестве брокера. Для распознавания номеров используем NomeroffNet, доступный через Flask. Если кому-то пригодится, мы сделали отдельный репозиторий с готовым контейнером для распознавания номеров.

На production/staging используем YandexLockbox для хранения и ротации секретов, Sentry для мониторинга ошибок, Uptime Kuma для мониторинга доступности с отправкой алертов в Telegram, Traefik для проксирования запросов, выпуска сертификатов, балансировки нагрузки и базовой авторизации служебных эндпоинтов.

Для контейнеризации используем Docker, для оркестрации — Docker Compose. Иногда для удобства используем Portainer.

В качестве почтового сервера используем Mailu, хотя при условии работы одного заявителя, можно вполне обойтись почтовым сервером, доступ к которым предоставляют по SMTP Google/Mail/Yandex и другие.

Также используем Nginx: на production/stagingон отдает статику, и проксирует запросы Uptime Kuma на эндпонит хелсчека Nomeroff. На локальной машине используем его для проксирования запросов и шифрования трафика (без SSL браузеры не дают доступ к камере и геолокации).

Фронтенд у нас на Vue3 (Typescript), для PWA — Vite. Роутинг — Vue Router, управление состоянием — Pinia. Для работы с картами используем vue3-openlayers, а для работы с камерой — vueUse. (С камерой до сих пор на некоторых устройствах есть проблемы и нам не помешает помощь более опытных коллег. Об этом чуть ниже.)

Для UI использовали MDB в редакции MDB Vue Pro, о чем сейчас немного жалеем.

Авторизация — через OAuth Google/VK.

Облачные вычисления и сервисы:

  1. Yandex.Cloud — S3-хранилище для фотографий, заявлений и иллюстраций к типам нарушений.

  2. Google Street View API — для просмотра точного местоположения фотографии на панорамах при модерации.

  3. OSM — для обратного геокодирования.

  4. Yandex.Forms — для форм обратной связи: 1, 2, 3, 4.

Деплоим все это скриптами (1, 2, 3). Хотелось бы прикрутить Kubernetes и GitLab CI/CD, но пока не хватает времени и опыта.

❯ Что требует доработки

  1. Камера. На некоторых устройствах (например, на iPhone Pro Max 11) камера не работает. Также наблюдаются проблемы с камерой в следующих браузерах: Яндекс, Samsung Internet, MIUI. Мы понимаем что проблема в некорректном коде компонента камеры, но у нас пока не хватило опыта её исправить. Исправляем работу камеры на одном типе устройств — все ломается на другом. Если есть опыт работы с камерой,будем благодарны за помощь.

  2. Несвободная лицензия MDB Vue в редакции Pro. Из-за отсутствия в команде дизайнера, нам пришлось подбирать UI-библиотеку. На этом этапе была допущена ошибка и из-за невнимательности при ознакомлении с лицензией мы не заметили тот факт, что лицензия MDB Vue Pro запрещает распространять под открытой лицензией проект с исходным кодом самой библиотеки MDB Vue Pro. Поэтому мы опубликовали варианты выхода из этой ситуации. Если вкратце, то можно написать несколько компонентов самостоятельно, удалить ненужные несвободные компоненты или вовсе использовать ХХК без фронтенда (например для написания ТГ-бота такой же направленности, как и ХХК).

  3. Расширение для браузера. Для автоматизации отправки заявлений через формы уполномоченных органов, необходимо разработать простейшее расширение для браузера. Мы готовы оперативно проработать API для взаимодействия с расширением и в целом помочь в разработке, если будут желающие.

  4. DevOps. На данный момент у нас нет CI/CD, Kubernetes, да и в целом есть масса замечаний по деплою. Например, у нас для каждой реплики бэкенда, поднимается свой контейнер с Celery в связке с Redis, что не очень хорошо. Также у нас нет автоматического масштабирования, что в перспективе может привести к проблемам с производительностью. Ну и есть масса замечаний к порядку развертывания. Например, при сборке для staging/production, статика фронта собирается в таком порядке:

    1. Скрипт готовит папки на хосте перед запуском сборки;

    2. Запускается контейнер для сборки статики Django;

    3. Запускается контейнер для сборки статики фронта ХХК и фронта карты стоянок;

    4. Уже после завершения работы трех контейнеров сборки, запускается контейнер с nginx, куда примонтированы директории с собранной статикой.

      По-хорошему, нужно производить сборку backend, frontend, parkings_frontend, а потом уже при сборке Nginx копировать нужные директории/файлы из полученных образов.

      Если есть опыт работы с Kubernetes и GitLab CI/CD, будем рады помощи.

  5. Консоль модератора. Необходимы доработки в консоли модератора, а именно: фильтрация, сортировка, поиск.

  6. Local-first. На данный момент у нас нет возможности работать при плохом канале связи на стороне клиента. Необходимо реализовать возможность работы с ХХК в режиме, близком к offline. Для этого необходимо переработать часть стора, чтобы он работал с IndexedDB, а также реализовать синхронизацию данных с сервером, не забывая про контроль за временем создания снимков (временем на устройстве), чтобы избежать подлогов (нужен «камертон», работающий через веб-сокеты).

  7. Отправка заявлений от имени пользователей без доступа к их данным. На данный момент заявления отправляются от имени нескольких модераторов, что является одним из самых узких мест в проекте. Необходимо реализовать отправку заявлений от имени пользователей, но без доступа к их данным. Тут вариантов масса, но нам кажется идеальным следующее решение: мобильное приложение, которое помогает заполнять формы уполномоченных органов (если отправка ведется через формы), либо готовит черновик письма для отправки на почту уполномоченного органа через стандартный почтовый клиент.

  8. Исправление логики импорта исходных данных. Сейчас все территории и базовые типы нарушений/полномочия и т.д. у нас импортируются в миграции, это неправильно. Во-первых, не всем нужны эти данные, а во-вторых, после импорта территорий производится их привязка друг к другу, которая из-за не оптимизированного кода и большого объема данных, требует для выполнения минимум 8Гб оперативной памяти и ~10 минут времени. Этот код писался на самом раннем этапе и мы тогда еще не знали про фикстуры и management-команды. Надо будет всю эту логику перенести в management-команду, чтобы она загружала из фикстур заранее подготовленные данные и импортировала их в базу, при необходимости загружая нужные картинки в S3-бакет.

  9. Журналирование. Сейчас журналы пишутся в stdout контейнеров и, по сути, хранятся в огромном JSON-файле в одной из директорий docker-compose.Также мы настроили конфиги так, чтобы Nginx и Django их сохраняли еще и в папки, смонтированные на хост.

  10. Мониторинг. Сейчас у нас помимо обычного хелсчека, на который стучится Traefik, чтобы определить жив ли контейнер при проксировании запросов,в файле с хелсчеками лежат представления, которые по сути не являются хелсчеками. На эти представления изредка заходит Uptime Kuma, чтобы мы в Телеграмме получали алерты если ресурсы хоста на исходе или что-то не так с бизнес-процессами (не уходит почта, выходят сроки модерации материалов, пользователь или модератор производят много отклоненных материалов). Все это по сути является метриками и необходимо это все, скажем, с использованием Prometheus, передавать на некие дашборды и от туда уже слать алерты.

  11. Автоматизация бэкапов. Сейчас у нас все сервера бэкапятся средствами хостера + мы периодически выгружаем эти бэкапы себе в облака. S3 вовсе не бэкапится нами... Необходимо наладить нормальное резервное копирование и сохранять все добро в какой-нибудь сторонний ледяной S3.

  12. Stateless. Сейчас у нас Stateful-архитектура. В частности, когда пользователь присылает фотографию нарушения, мы ее помимо S3, кладем в папку, примонтированную к хосту. После того как запрос успешно выполнился, запускаются фоновые задачи в контейнерах Celery. Одна из таких задач подбирает этот файл из той же папки, делает сжатую копию и стучится в Nomeroff, чтобы распознать номера. Контейнер Nomeroff ходит в ту же папку. В самом конце задачи, файл удаляется. Все это нам уже не очень нравится с архитектурной точки зрения.

  13. Тестирование. Сейчас в проекте ровно 0 тестов. По-хорошему нужны не только юнит-тесты, но и нормальные интеграционные тесты.Также мы пока не научились производить нагрузочное тестирование, поэтому на все вычисления (на все серверы) у нас в сумме задействовано 10vCPU и 18Гб памяти, что требует ~7000 рублей в месяц. Нагрузочное тестирование позволит оптимизировать использование ресурсов и сократить затраты.

❯ Какие сложности возникали

NomeroffNet и недоступность ресурсов

NomeroffNet использует собственный модуль ModelHub для загрузки моделей. С учетом различных санкционных политик, все эти модели оказались недоступны из РФ. Мы решили это достаточно тривиально:

  1. Пустили трафик локальной машины через 3-ю страну, и загрузили модели на локальную машину;

  2. Подправили пути в конфигах моделей;

  3. Положили все эти модели в S3 + сохранили их в репозитории, чтобы вообще не ходить лишний раз в S3, а просто примонтировать директории с кешированными моделями в нужную папку в контейнере.

GitLFS

У нас сейчас в репозитории два файла размером более 100Мб - GeoJSON с границами территорий и одна из моделей NomeroffNet. В один прекрасный момент у нас сломался NomeroffNet и начал ругаться на то, что с файлами моделей что-то не в порядке.

Мы долго разбирались в чем причина и почему на новом сервере ничего не работает, а на старом все хорошо запускается. Так мы узнали про существование GitLFS. Чтобы запустить ХХК, для начала необходимо установить GitLFS. Мы добавили необходимую проверку и команду в скрипты запуска ХХК.

PWA и кеширование

Думаю большинство из тех, кто хоть раз запускал собственные публичные проекты, старался сделать это как можно быстрее.Вот и нас настолько терзало любопытство, что мы выкатились в прод не проверив все до конца. Весь функционал вроде работает, на нескольких разных устройствах протестировали на стейжинге, все вроде ОК, пора в прод.

Но когда пришло время выкатывать обновленияфронта, выяснилось что все файлы первой версии фронта наглухо закешировались в браузерах пользователей. Так мы узнали что index.html кэшировать нельзя.

Мы писали публикации с просьбой к пользователям почистить кэш, но продолжали наблюдать в Я.Метрике визиты со старой версией фронта. По итогу нам пришлось менять адрес домена и указывать на переезд пользователям, чтобы они получили обновленную версию фронта. Сейчас мы видим что раз в 2-3 дня несколько пользователей так и заходят на старый домен с первой версией фронта в кэше своего браузера...

PWA+Django social auth

Если установить приложение как PWA, то наблюдалась проблема авторизации. Ты открываешь в браузере сайт и пытаешься войти. В результате тебя переводит на страничку OAuth-провайдера, а после авторизации, уже ведет назад. И тут у тебя уже открывается не «приложение в браузере», а непосредственно ранее установленное PWA и Django возвращает клиенту ответ со статусом 500.

Мы долго пытались разобраться в чем проблема, но истины так и не достигли. По логам единственное что видно, это то, что когда клиента ведет со страницы OAuth-провайдера в PWA, из базы удаляется запись о ранее созданной сессии. Но если начать авторизацию сразу в PWA, то все работает как надо.

Мы поняли что PWA открывается тогда, когда переходишь по ссылке на приложение, находясь на другом домене. В результате мы сделали следующее решение: когда пользователь пытается авторизоваться, мы его сначала ведем на домен авторизации, а тот уже делает редирект на эндпоинт Django social auth. При таком решении, если у пользователя установлено PWA, то оно откроется до начала процесса авторизации, и все куки будут в наличии, как и сессия.

Почтовый сервер Mailu

Для отправки и получения писем мы используем собственный почтовый сервер на базе Mailu. Это позволяет создавать несколько заявителей, от чьего имени ведется отправка писем в уполномоченные органы.

При отправке заявления, заявителю направляется скрытая копия письма, чтобы при необходимости можно было подтвердить отправку в органах прокуратуры. При получении ответа у нас письма перенаправляются на личные почты заявителей, в результате каждый заявитель может обрабатывать свои ответы.

На третьем месяце отправка поломалась. После разбора логов стало понятно что проблема с SSL-сертификатом почтового домена. Mailu получает сертификаты SSL в Let'sEncrypt только при запуске и складывает их в папку, смонтированную в контейнер с Nginx.

Мы пока не решали задачу с автоматическим выпуском новых сертификатов почтового сервера, поэтому просто перезагружаем сервер почты раз в пару месяцев.

PostGIS + Django

При первой миграции Django в любом случае пытается добавить расширение PostGIS в базу, даже если оно уже вручную добавлено. В результате, при выходе в прод мы получили проблемы с миграцией, т.к. у служебного пользователя базы не было соответствующих прав. Особо разбираться времени не было, поэтому мы дали ему права суперпользователя, выполнили миграции и вернули пониженные права назад.

На локальной машине этой проблемы нет, т.к. для нужд разработки мы используем образ контейнера PG+PostGIS от Kartoza, где пользователь имеет все права изначально.

Django+OpenLayers в админке

Пару месяцев назад у нас начались проблемы с админ-панелью Django: при попытке открыть страницу со сведениями о каком-нибудь экземпляре модели, где есть геометрия, страница либо вовсе не загружалась, либо на загрузку уходило до 5 минут.

Как оказалось, из-за поломанного рунета и перегруженных внешних каналов, сильно упала скорость до CDN, где лежит ol.js и ol.css, которые Django почему-то не кладет в статику. В качестве решения мы написали простейший виджет, положив нужные файлы в статику, и использовали этот виджет там, где есть геометрия.

UFW+Docker

Как оказалось, мы уже полчаса как стали частичкой чьего-то ботнета и от нас не только проводилось сканирование чужой инфраструктуры, но и велась атака + производился майнинг. Впоследствии мы поняли, что допустили ряд грубейших ошибок: контейнер с Redis вываливал порт наружу, на Redis не было авторизации, а на все наши правила UFW docker не обращал внимания.

Это был полезный опыт, в результате которого мы стали использовать авторизацию везде, где это возможно, а также четко контролировать все порты доступные извне. На боевых серверах мы использовали ufw-docker, чтобы все правила ufw распространялись на Docker тоже.

DataRace

Как только у нас появилось несколько параллельно выполняемых задач, мы получили первые случаи data race. Так брат познакомился с гонками данных и понял что где-то можно просто указывать конкретные поля, в которые пишешь, а где-то лучше использовать транзакции.

Idempotency

При тестировании «в полях», мы поняли, что качество связи не везде позволяет отправить фотографию с нарушениями. Изначально проблема была в выставленном в конфигурации axios тайм ауте в 30 секунд, из-за чего при плохой связи фотография не отправлялась и возникала ошибка.

На тот момент мы не понимали в чем сама проблема и у нас родилось решение, состоящее из велосипедов на костылях: мы начали в сторе перехватывать ошибки отправки и, если отправка не прошла, то делать ретрай через несколько секунд.

В результате иногда мы на сервере получали несколько одинаковых фотографий. Так мы узнали о требованиях к API в части идемпотентности. С тайм аутом мы разобрались, как и с идемпотентностью.

Traefik и sticky sessions

При выпуске своего первого обновления мы поймали проблему не только с кэшированием, но и с балансировкой. Дело в том, что на стейжинге у нас всего одна реплика приложения и из-за этого все выкатилось нормально. Но стоило нам одну из реплик обновить на проде,как сразу начались проблемы: браузер выдавал массу ошибок 404 в консоли и приложение не работало как надо.

Чуть позже мы поняли, что мы получили index.html с одной реплики, а остальные файлы отчасти запрашивались на других репликах Nginx, как и предполагают механизмы балансировщика. Разобравшись, мы узнали про Sticky sessions в Traefik, что позволяет закреплять определенный сервер за определенным пользователем на время.

OpenStreetMaps и вандализм

Где-то на третьем месяце работы мы узнали про вандализм на OpenStreetMaps. Нам повезло что в тот день я что-то правил на фронте и у меня перед глазами был браузер с запущенным локально приложением при отключенном кэшировании.

В один из моментов я перезагрузил страницу и увидел что весь город был перекрыт по диагонали дорогами с выдуманными названиями улиц, приводить которые я тут не буду. Открыв приложение на проде я этого вандализма сразу не увидел, пока не нажал Ctrl+F5.

После обновления кэша стало понятна перспектива получить неприятности в виде оскорбления чьих-то чувств или дискредитации какой-нибудь важной структуры. Хорошо что мы заранее предусмотрели механизм отключения приложения на обслуживание, что и было сделано.

Выдохнув и поняв, что эти пакости никто из посетителей не увидит, мы пошли разбираться в чем дело. В результате мы узнали про вандализм на OpenStreetMap и приняли решение реализовать механизм переключения тайловых слоев на лету (из админ-панели Django).

У нас изначально была сделана модель состояния сайта, которое периодически запрашивается фронтом,начиная с этапа загрузки приложения. Мы сделали модель тайлового слоя и в миграции добавили известные нам слои от разных поставщиков. Теперь если мы в админ-панели меняем слой, то через несколько секунд он поменяется у всех клиентов «на лету».

❯ Планы на будущее

Разработка

Дальнейшие возможности по улучшению ХХК мы описали выше, но из них хотелось бы выделить разработку мобильного приложения для возможности отправки заявлений от имени пользователей без доступа к их данным. С учетом реалий города Балаково, для начала будет достаточно простейшего функционала, который заключается в возможности авторизации, фиксации нарушений и отправки заявления с использованием настроенного в системе почтового клиента.

На Kotlin доводилось немного писать и в принципе есть понимание как это реализовать, но все упирается в отсутствие времени.

Исключение административных производств (и ХХК, как следствие)

Сейчас самое узкое место в нашем кейсе — это административные производства. По нашему опыту, больше 20 заявлений в один рабочий день это уже за гранью возможностей уполномоченного органа в городах, схожих с Балаково по населению. Для того чтобы понять почему так, давайте кратко опишу процесс:

  1. Мы направляем заявление.

  2. Это заявление регистрируют в отделе обращения граждан и относят главе в почту (о да, на бумаге!);

  3. С учетом структуры аппарата, письмо «спускается» до исполнителя за 2-4 дня, т.к. каждый разбирает почту и «отписывает» (поручает) подчиненному в рамках возложенных полномочий;

  4. Исполнитель готовит запросы в МВД (в ГИБДД и ФМС);

  5. Эти запросы перед отправкой проходят ту же самую цепочку согласований, но уже «вверх», что также занимает 2-4 дня;

  6. Запросы достаточно долго обрабатываются у адресатов (а бывает и теряются);

  7. Приходит ответ на запросы и также «спускается» 2-4 дня. В идеальном случае в ответах есть контакты собственника авто;

  8. Если контактов нет, то по адресу регистрации необходимо направить письмо (по тому же пути) касаемо административного протокола. Если контакт есть,то собственника приглашают на составление протокола (что тоже достаточно трудно и затратно по времени).

  9. Когда протокол составлен, назначается административная комиссия, состоящая из уполномоченных лиц из разных ведомств. Как правило, комиссии проходили пару раз в месяц и на них приглашалось 20-30 нарушителей. Сейчас периодичность и «наполняемость», естественно, иные.

  10. На комиссии каждого нарушителя по очереди приглашают, зачитывают ему протокол, дают слово, объясняют суть претензий и коллегиально принимают решение.

С учетом того, что все это выполняется «на бумаге», а также большая часть задействованных исполнителей делают это все в довесок к своим основным обязанностям, продуктивной такая модель работы стать не может.

Выход из этой ситуации — внедрение официального федерального приложения на базе ПАК «Помощник Москвы». Когда у нас получится добиться такого внедрения, потребность в ХХК отпадет и большая часть нарушений правил парковки будет проходить через эту систему в автоматическом режиме, что позволит комиссии заниматься другими делами, коих у них в достатке (свалка мусора, граффити, незаконная торговля, и т.д.).

Информационная работа

Также следует отметить необходимость продолжения информационной работы с населением. Даже сейчас, когда я пишу эту статью, нам приходят комментарии от горожан в стиле «на стоянках нет мест», хотя в самом посте четко указано, что это не так. Именно для этого, мы на базе ХХК сделали еще и отдельную карту стоянок. Это по сути отдельный контейнер Nginx со своей редакцией фронта.

Бэкенд используется тот же, только добавили отельные модели (1, 2, 3, 4) и сделали простейший эндпоинт.

Ну и напоследок, есть план действий на момент времени, когда все стоянки будут заполняться. В этот момент будем вести информационную работу с собственниками стоянок, ведь есть решения для увеличения их вместимости без строительства дорогих капитальных паркингов — роторные карусельные парковки.

Необходимо не упустить момент и сделать все для того, чтобы бизнес успел покрыть возрастающий спрос предложением, при этом не провоцируя новый виток роста уровня автомобилизации населения.

❯ Как помочь проекту

Код и компетенции

Лучший вклад, который вы можете сделать, это ваше время и компетенции. Поэтому если есть желание и возможности,мы будем рады предложениям и вашим MR.

Финансовая поддержка

Мы не принимаем прямые пожертвования на свою деятельность, но если у вас есть желание поддержать рублем, то вы можете внести вклад в оплату вычислительных ресурсов. TimeWeb позволяет любому желающему оплатить любую сумму, при этом необходимо выбрать опцию «оплата хостинга» и ввести имя домена хрюхрюкар.рф.

Информационная поддержка

Если вы владеете какими-либо информационными ресурсами и можете помочь привлечь внимание к проблеме, с которой мы боремся, напишите нам, нам есть что рассказать, чтобы более детально понять проблему.

❯ Ссылки

  1. Сайт: xxkap.app и хрюхрюкар.рф;

  2. Репозиторий;

  3. Группа в ВК;

  4. Канал в Telegram;

  5. Почта брата — его зовут Игорь и он за время работы над ХХК неплохо вырос как бэкенд-разработчик, ~90% кода backend — его работа. Если кому-то в проект нужен молодой и перспективный backend developer (Django/Flask), напишите ему, пожалуйста.

Ну и насчет обзавестись друзьями: благодаря ХрюХрюКару, у нас теперь сотни (если уже не тысячи) классных друзей.

Все они занимают первые места в чемпионате по выдумыванию конспирологических теорий насчет наших источников финансирования, совмещенному с безуспешными попытками оскорбления авторов и участников проекта.

Жаль что они пока всё еще продолжают плакать, колоться, но упорно грызть кактус, который мы от них отодвигаем. Ну и ладно, мы будем все дальше плыть по течению этой занятной IT-реки с урбанистическим уклоном.

Спасибо за ваше время!

UPD от 05.08.2024 22:40: заменили логотип.

Автор: Станислав Лепехов

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js