Проекты02 / 05

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

blackbrookbarns.co.uk

Главная страница Blackbrook Barns с открытым AI-ассистентом площадки
Ассистент на сайте самой площадки. Саму страницу менять не пришлось.
Клиент
Blackbrook Barns, свадебная площадка в Стаффордшире, Англия
Роль
Разработка: API, админ-панель, виджеты, автоматизация
Период
Май – октябрь 2026
Стек
TypeScript, Fastify, Drizzle, PostgreSQL, Next.js, Preact, Make, Heroku, Vercel
Онлайн
blackbrookbarns.co.uk

Blackbrook Barns — свадебный амбар между Личфилдом и Саттон-Колдфилдом, который снимают целиком. Команда продаж небольшая, а сайт на Webflow уже делал своё дело. Новый сайт никому не был нужен. Нужно было, чтобы работа за ним перестала быть ручной.

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

Отсюда жёсткое ограничение: всё должно было жить внутри чужой страницы, на сайте, который я не контролировал, и выдерживать всё, что эта страница вытворяет.

  1. 01

    Запись на просмотр прямо на странице площадки

    21-дневное расписание в Shadow DOM, чтобы CSS сайта и мой не могли задеть друг друга. Еженедельный график дежурств решает, кто проводит просмотр; пара видит, с кем встретится, выбирает время и получает экран подтверждения и письмо. Работает на ширине телефона.

  2. 02

    API, где запись не может сорваться по чужой вине

    Fastify, Drizzle и PostgreSQL. Сначала коммитится строка; почта и система площадки работают после неё и могут только записать сбой в лог. Плохой день у третьей стороны никогда не лишит пару слота.

  3. 03

    Автоматизация писем

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

  4. 04

    Одна админ-панель

    Next.js: сетка свадебных дат на три года, календарь просмотров, доступность каждого менеджера, график дежурств с отпускным переключателем «всё на X», аккаунты команды и неотправленные уведомления. Забронированная свадьба сама закрывает этот день для просмотров.

  5. 05

    Events 500 без ручного ввода

    Каждая запись сама появляется в системе площадки с пометкой, откуда пришла: сайт, чат-бот или письмо с брошюрой.

  6. 06

    AI-ассистент на сайте

    Чат-ассистент на сайте отвечает на вопросы о помещениях, вместимости, ценах Twilight и просмотрах из базы знаний на 13,9 КБ и направляет пары на страницу записи.

Как это устроено

  1. Пара

    • Страница площадки на Webflow
    • Виджет записи40 КБ, Shadow DOM
    • Нативные формыподключены через вебхук
  2. Ядро

    • API36 путей
    • PostgreSQLарбитр для каждого слота
  3. После коммита

    • Почтаподписанный вебхук → Make → ящик
    • Events 500односторонняя отправка
  4. Команда

    • Админ-панель
    • График дежурствкто отвечает за какие часы
Расписание записи на просмотр со свободным временем на сайте Blackbrook Barns
21-дневное расписание: виджет ведёт себя так, будто был здесь всегда.

То, что обычно не попадает в портфолио. Всё это было на самом деле, и всё это есть в истории коммитов.

  1. Сбой 01

    Полоса прокрутки, на которую ушло пять исправлений

    Симптом
    Страницы с виджетом застревали. Парам не удавалось докрутить до формы записи или футера: прокрутка упиралась в 1 470 из 2 636 пикселей.
    Причина
    На сайте работает плавный скролл Lenis, который кэширует высоту страницы при старте. Виджет, выросший позже, для него невидим. Каждое исправление находило слой глубже: событие resize, отправленное слишком рано, опрос, который сдавался слишком быстро, а потом выяснилось, что Lenis вообще игнорирует resize, когда сайт жёстко задаёт высоту html.
    Решение
    Lenis держит свой экземпляр приватным, поэтому виджет ровно на один кадр перехватывает метод в его прототипе, ловит живой экземпляр в момент вызова из рабочего цикла, возвращает метод на место и вызывает resize() при каждом изменении высоты. Этот код живёт внутри бандла: скрипты в футере, на которые он раньше опирался, позже исчезли с сайта. Страница, в которую ты встраиваешься, тебе не принадлежит.
  2. Сбой 02

    Интеграция с API, которого почти нет

    Симптом
    PDF вендора документирует один эндпоинт, и то с вырезанными справочными разделами. Поэтому я прощупал живой сервис и записал, что там есть на самом деле.
    Причина
    expires_in задокументирован как секунды, а возвращается ISO-таймстамп. Создание события не принимает дату. Чтение событий не возвращает идентификаторов. Нет ни обновления, ни удаления, ни вебхуков.
    Решение
    Кэш токенов принимает обе формы срока действия. Дата, время и менеджер едут в поле заметок, чтобы запись была полезна команде. Отправка никогда не бросает исключений и идемпотентна по сохранённому ID. Edit и Delete в админке скрыты, хотя написаны и протестированы: до системы площадки они дойти не могут, а два расходящихся календаря хуже одной отсутствующей кнопки. Проверка отмен, когда она запускается, реагирует только на однозначный сигнал: ошибочно отменить настоящую встречу гораздо хуже, чем заметить отмену с опозданием.
  3. Сбой 03

    Летнее время, дважды

    Симптом
    В мае, в британское летнее время, упали первые тесты доступности: понедельник определился как воскресенье. Потом код-ревью нашло кое-что похуже в осеннем переводе часов.
    Причина
    Слот — это время по лондонским настенным часам, а сервер работает в UTC. Полночь в Лондоне во время BST — это 23:00 предыдущего дня, поэтому день недели выходил неправильный. А шаги по 24 часа через 25-часовой день 25 октября могли повторить дату и потерять день в окне записи.
    Решение
    В коде одна граница часовых поясов. Каждый день привязан к полудню по Лондону, а это всегда далеко от перевода часов. День недели читается из метки даты, никогда из момента времени. Три сквозных теста фиксируют осенний перевод, весенний перевод и обычную зимнюю неделю. Площадку бронируют за месяцы, поэтому календарь должен быть верным и в те даты, на которые никто не смотрит.
  4. Сбой 04

    Пусть арбитром будет база данных

    Симптом
    Две пары могут нажать на один слот в один и тот же момент; админ может дважды кликнуть Cancel. Ревью нашло тот же класс багов и в других местах, до продакшена.
    Причина
    Между проверкой и записью остаётся зазор. Самый странный случай: при повторном использовании refresh-токена код отзывал семейство токенов и тут же бросал исключение внутри той же транзакции, так что отзыв откатывался вместе с ошибкой. Повторный запрос получал 401, а семейство оставалось действующим.
    Решение
    Частичный уникальный индекс по менеджеру, дате и времени там, где статус confirmed. Запись — это обычный INSERT, который ловит конфликт и возвращает «слот занят»; проверено настоящей параллельной гонкой, победитель ровно один. Отмена — один условный UPDATE, поэтому письмо отправляет только победитель. Админ может обойти политику, но не индекс: конфликт — это не политика.
  5. Сбой 05

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

    Симптом
    Пары должны получать письма от своего менеджера, но почтовое подключение принадлежит одному человеку. Очевидное решение — поставить в From адрес этого менеджера.
    Причина
    Адрес, от имени которого подключение не может отправлять, роняет модуль, а упавший запуск может выключить мгновенный сценарий. Достаточно одного нового менеджера без нужного разрешения, чтобы молча остановились все подтверждения для всех.
    Решение
    В каждом пейлоаде отдельный отправитель: адрес менеджера, только если он есть в белом списке, иначе адрес по умолчанию, а Reply-To всё равно ведёт к менеджеру. Добавляем менеджера, и ничего не ломается. Ретраи пересчитывают отправителя из текущего конфига, так что сохранённый пейлоад не может воспроизвести отозванное разрешение. Проверено настоящей отправкой: строки «on behalf of» нет.

  • автотестов в 62 файлахлог CI, 27 октября 2026
  • зелёных прогонов CI с июняGitHub Actions
  • каждый виджет в gzipReact заменён на Preact при сборке; сборка падает, если больше 50 КБ
  • таблиц в базе данных36 путей API, 47 операций
  • день в живом расписании записивидно на сайте площадки
  • дней от первого коммита до последнего342 коммита; сделано с Claude Code

Пульс проекта

Каждый коммит в репозиториях проекта на одной линии. Чем выше столбик, тем жарче было.

май 2026 г.окт. 2026 г.
  1. Репозиторий, требования, дорожная карта на шесть фаз
  2. CI, API, админка и виджеты на продакшен-хостинге
  3. Запрос на изменение: свой календарь у каждого менеджера
  4. Код для Events 500, после прощупывания живого API
  5. Отправка в Events 500 запущена
  6. Письма уходят с личного ящика менеджера

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

Написать бриф

Быстро отвечаю в WhatsApp и Telegram

Или спросите о нём цифровой разум
Следующий кейсLeonida Ledger