Привет, сообщество! Уже долгое время я отмечал на для себя на карте бесплатные парковочные места в центре города; Этот список быстро начал "ходить по рукам" среди друзей и знакомых. Мне пришла в голову идея, а что если развить "списочек для друзей" в какой-то более общественно полезный проект? И с какими техническими челленджами придется столкнуться при попытке навайбкодить мало-мальски production-ready приложение?

Так появилась ГдеПарковка — карта бесплатных парковок, которую наполняют сами водители. Без регистрации: открыл, нашёл место рядом, поехал. Можно добавить точку, фото, комментарий, проголосовать «место живо / места нет», уточнить вместимость и построить маршрут в Яндекс Картах, 2ГИС или Google Maps.

Но ведь платные парковки существуют не просто так!

Легко понять негодование жителей центральных районов, которые не любят шум машин под окнами, а для своей машины, как зеницу ока, оберегают бесплатные "островки". Но треть пробок в центре — это машины, которые уже приехали. Они не едут из точки А в точку Б, они кружат по кварталу в поисках места. Классические исследования городского трафика (Дональд Шуп, UCLA) оценивают долю «поискового» трафика в загруженных районах в 20–30%. Водитель, который заранее знает, где встать, проезжает по вашему району один раз, а не наматывает пять кругов мимо окон, дворов и детских площадок. Карта парковок не добавляет машин в город. Она сокращает самый бессмысленный и раздражающий их пробег.

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

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

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

  • «Понаедут чужие в наш двор» — двор публикует только тот, кто хочет. Ваш двор без вас на карте не появится, а если появился и мешает, то жалуйтесь, уберём.

  • «Из-за вас больше машин» — количество машин определяется работой и делами, а не картой. Карта сокращает круги по кварталу, а не создаёт поездки.

  • «Пусть платят за парковку» — платят те, кто может. Остальные и так ищут бесплатные места, только медленно и объезжая по три раза ваш квартал.

  • «Вы легализуете парковку где попало» — наоборот: рейтинг и комментарии топят плохие места, а дисклеймер прямо требует проверять знаки. У хаоса появился фильтр.

Техническая часть

Я не написал руками ни строчки кода. Весь проект — от схемы БД до деплоя — сделан в диалоге с Claude Code, за пару вечеров, с помощью модели Fable.

Босяцкий хак проекта: импорт из закладок Яндекс Карт

У меня была своя подборка из 68 бесплатных парковок Петербурга в закладках Яндекс Карт. Экспорта у Яндекса нет. Зато можно поделиться своей подборкой по ссылке, и публичная подборка это SSR, и в HTML лежит <script class="state-view"> с полным JSON-состоянием, включая все точки:

const state = JSON.parse(html.match(/<script[^>]*class="state-view"[^>]*>(.*?)<\/script>/s)[1]);
const items = state.config.bookmarksPublicList.children;
// { title: "Парковка", uri: "ymapsbm1://pin?ll=30.290309%2C59.970660",
//   description: "Санкт-Петербург, улица Профессора Попова, 40" }

Координаты достаются прямо из ymapsbm1://pin?ll=.... Скрипт импорта идемпотентен — точки ближе 30 метров к существующим пропускаются, так что его можно гонять повторно. Так у сервиса в день запуска появился стартовый каталог по Петербургу, а не пустая карта.

Стек

Я придерживаюсь мнения, что при кодинге с ИИ-шкой лучше использовать хорошо задокументированные и скучные технологии. Грубо говоря, используя модный и не сильно хорошо описанный фреймворк, либо сильно меняющийся между версиями, то он будет слабее отражен в весах модели, и будут возникать ошибки. Помню, как я писал экспорт скрипты для KIWI TCMS, и клод вообще не мог сконструировать валидный запрос, пока я не копипастнул ему в контекст избранные файлы из имплементации самого KIWI TCMS, только тогда он разобрался в формате апишки. Поэтому в промпте был заложен принцип: никаких фреймворков. Такое тривиальное архитектурное решение сработало вполне хорошо!

  • Бэкенд: Node.js + Express + SQLite. Причём не better-sqlite3, а встроенный node:sqlite из Node 22+ — ноль нативных зависимостей, npm install не может сломаться в принципе.

  • Фронтенд: vanilla JS + Leaflet с тайлами OSM. Без сборки вообще — файлы из public/ отдаются как есть. Никакого вебпака, вайта, транспиляции. Деплой фронта — это git pull.

  • Зависимостей в package.json — четыре: express, multer, sharp, compression. И leaflet, который отдаётся статикой из node_modules.

В сумме: один процесс, одна БД-файл, один каталог data/ со всем состоянием. Бэкап — это sqlite3.backup плюс rsync каталога фотографий, восстановление — скопировать каталог обратно.

Хостинг — VPS на RuVDS за ~300 ₽/мес: 1 vCPU, 1 ГБ RAM. Перед Node стоит Caddy, HTTPS с автопродлением сертификата из коробки. Кстати, HTTPS тут не опция: браузеры отдают navigator.geolocation только в secure context.

Простота использования и секьюрность

Секьюрности нет)))

Защита от накрутки простенькая. «Без регистрации» и «один голос на человека» — требования, противоречащие друг другу. Разрешение конфликта — анонимный clientId (UUID в localStorage) плюс второй рубеж: в таблице голосов хранится солёный хэш IP, и с одного адреса на одну точку принимается не больше пяти голосов от разных «клиентов». Запас — на CGNAT и общий Wi-Fi, сырые IP не хранятся вообще. Это не защита от целеустремлённого ботовода, это защита от накрутки «за пять минут скриптом». Для моего кейса это осознанно достаточный уровень.

Фотографии: сжимаем варварски, и это фича

Пользовательские фото парковок — это «общий план двора». Качество не нужно от слова совсем, поэтому сжатие агрессивное: JPEG quality 45 (mozjpeg), ресайз до 1024px по большей стороне. Телефонное фото на 6 МБ превращается в 50–150 КБ.

await sharp(buffer, { limitInputPixels: 30_000_000 })
  .rotate() // применить EXIF-ориентацию до удаления метаданных
  .resize({ width: 1024, height: 1024, fit: 'inside', withoutEnlargement: true })
  .jpeg({ quality: 45, mozjpeg: true })
  .toFile(path);

В этих пяти строчках три неочевидных решения:

  1. limitInputPixels — защита от pixel-flood. JPEG 50000×50000 весит меньше 15 МБ, но при декодировании разворачивается в гигабайты RAM. На VPS с гигабайтом памяти это гарантированный OOM. Лимит в 30 мегапикселей режет бомбу на этапе чтения заголовка.

  2. Пересжатие как санитайзер. Что бы ни прислали под видом картинки — полиглот-файл, картинку со скриптом в метаданных — на диск ложится честный JPEG, собранный заново.

  3. Re-encode убивает EXIF — включая GPS-координаты автора снимка. Для краудсорсингового сервиса это не мелочь: человек фотографирует парковку у дома.

Плюс все фото проходят через последовательную очередь (sharp.concurrency(1)) — два параллельных декода больших снимков на маленьком VPS складывают процесс, проверено намеренно.

А поскольку имена файлов — UUID и не меняются никогда, отдаются они с Cache-Control: immutable, max-age=1y: повторные открытия карточек не трогают сервер вообще.

Автомодерация комментариев: 70 строк вместо npm-пакета

Хотелось автоматом резать мат и спам-дубликаты до того, как они падают в очередь модератору. Готовые пакеты для русского мата на npm оказались заброшены и наивны, ML-API — платная зависимость с латентностью на каждый комментарий. В итоге — собственный модуль на 70 строк: словарь корней + нормализация против обфускации.

Нормализация делает из «м у д @ к» и «сууукааа» канонические формы: латиница-двойники и цифры приводятся к кириллице, символы-маски выбрасываются, повторы букв схлопываются, «р а з р я д к а» склеивается обратно в слово. А анкеры в регулярках подобраны так, чтобы «курс рубля», «колебание», «обед», «веб-камера» и «не психуй» проходили спокойно. На 24 тестовых кейсах — ноль промахов в обе стороны.

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

Интерфейс

Интерфейс mobile-first и бессовестно вдохновлён ГдеБЕНЗом: сверху компактная плашка-дисклеймер, дальше карта на весь экран с зелёными, красными и серыми пинами — цвет считается из баланса голосов «место живо / места нет». На телефоне между картой и списком переключаешься пилюлей сверху, на десктопе список висит слева постоянно.

Тап по пину открывает нижнюю шторку-карточку, и в ней весь функционал: описание, вместимость по категориям (от «маленькая, 1–4 места» до «огромная, 50+»), голосование с возможностью передумать, фото, комментарии и история голосов. Там же кнопка «Построить маршрут», которая пробрасывает точные координаты точки в Яндекс Карты, 2ГИС или Google Maps: на телефоне ссылку перехватывает установленное приложение и сразу строит маршрут. Добавление парковки — три действия: кнопка, тап по карте, короткая форма с опциональным фото. Регистрации нет нигде, «личность» пользователя — UUID в localStorage. Ну и PWA: сайт ставится на домашний экран и открывается как приложение, без адресной строки. Также есть админка со статистикой: посещения по дням, счётчики действий, топ точек по рейтингу и «самые спорные» — места, у которых много и 👍, и 👎, то есть парковки-лотереи.

Выводы

Проект от «а сделаю-ка я» до продакшена с доменом, HTTPS, PWA, SEO и стартовым каталогом занял, суммарно, пару вечеров диалога. Что показывает, что говнопалочные пет-проекты сейчас стало делать проще, чем никогда - в целом, с новыми моделями уже не особо важны технические знания, даже первая итерация по расплывчатому ТЗ более-менее работала и даже неплохо выглядела. Но имея инженерные знания, можно промптить модель так чтобы она закрывала бреши в производительности и безопасности, делать ей всякие замечания и тп. Ну и главное, работает стабильно, лучше чем мои курсовые с первого курса :)

Что еще помогло не накосячить с ИИшкой: скучный стек, коммиты на каждый шаг и живое тестирование руками на реальном устройстве.

Сервис живой: gdeparkovka.ru. По Петербургу каталог уже есть, по другим городам карта ждёт ваших точек, добавление занимает полминуты и не требует регистрации. Критику проекта жду в комментариях! Ну и пользуйтесь на здоровье, информация это общественное благо :)