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

Читаю фидбек. Кто-то пишет про офлайн-режим - "а если на даче интернет плохой, измерение пропадёт?". Другой спрашивает про PWA - "можно ли установить как нормальное приложение?". Третий интересуется кабинетом для ветеринаров - "как врачу посмотреть данные всех своих пациентов в одном месте?". Четвёртый жалуется что забыл пароль и нет восстановления.

Выписываю все запросы в блокнот. Смотрю на список и прикидываю объём работы. Это легко месяц-два разработки для обычной команды - PWA с офлайн-синхронизацией сам по себе недели две, плюс B2B функционал, плюс мелочи типа восстановления пароля.

Но с Claude Code... может попробовать сделать за пару вечеров?

Пока я разрабатывал, главный тестировщик спал

Понедельник вечером: офлайн-режим

Понедельник, 17 ноября. Закончил работать в шесть вечера, поужинал, открыл ноутбук. Описываю ChatGPT для Claude задачу: нужен полноценный PWA с работой без интернета. Пользователь должен спокойно добавлять измерения глюкозы на даче где связь плохая, всё сохраняется локально, а когда появляется сеть - автоматически синхронизируется с сервером.

Получаю развёрнутый ответ с планом. Service Worker будет кешировать всю статику чтобы приложение загружалось офлайн. IndexedDB станет локальной базой данных в браузере. Очередь синхронизации будет хранить все операции которые нужно отправить на сервер. UI покажет пользователю что он работает офлайн и сколько изменений ждут синхронизации.

Начинаем по порядку. Добавляем vite-plugin-pwa в зависимости, настраиваем конфигурацию Workbox. Первый запуск - Service Worker не регистрируется, приложение ведёт себя как обычный сайт. Лезу в код, ищу где проблема. Час отладки и нахожу - забыли вызвать registerSW() в точке входа приложения. Добавляю одну строчку, перезапускаю. Теперь в DevTools вижу что Service Worker активен, статика кешируется.

Тестирую базовую функциональность. Открываю приложение, отключаю WiFi на ноутбуке, обновляю страницу. Приложение загружается! Все стили на месте, интерфейс работает, можно кликать по кнопкам. Правда данные не загружаются - это ожидаемо, до IndexedDB ещё не дошли.

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

Пишем сервис который умеет работать с этой базой. Добавление новой записи - сохраняем в IndexedDB с временным UUID вместо ID, добавляем в очередь синхронизации пометку "создать на сервере". Редактирование записи - обновляем в IndexedDB, добавляем в очередь "обновить на сервере". Удаление - помечаем удалённой локально, добавляем в очередь "удалить на сервере".

Самое сложное - синхронизация. Когда появляется сеть, нужно взять все операции из очереди и выполнить их по порядку на сервере. Причём если мы создали запись офлайн с временным UUID, после успешной отправки на сервер нужно заменить этот UUID на нормальный числовой ID который вернул сервер. Иначе при следующей синхронизации мы попытаемся создать ту же запись ещё раз.

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

Тестирую самый извращённый сценарий какой могу придумать. Отключаю интернет на ноутбуке. Добавляю пять новых измерений для Манишки. Редактирую одно из старых измерений. Удаляю ещё одно старое. Включаю интернет обратно. Открываю консоль разработчика и наблюдаю.

Синхронизация запускается автоматически через секунду после появления сети. Вижу в логах как отправляются запросы на сервер по очереди. Сначала пять POST запросов создают новые записи и возвращают их ID. Потом PATCH запрос обновляет отредактированную запись. Потом DELETE удаляет помеченную. IndexedDB обновляется - временные UUID заменяются на серверные ID, очередь синхронизации очищается. Всё работает идеально!

Теперь нужны визуальные индикаторы чтобы пользователь понимал что происходит. Добавляем баннер вверху экрана - "Нет подключения к интернету, изменения будут синхронизированы позже" с количеством несинхронизированных операций. Добавляем маленькие бейджи "Pending sync" на записях которые созданы или изменены офлайн. Отключаем кнопки которым обязательно нужен интернет - AI-анализ требует обращения к внешнему API, настройки профиля нужно сохранять на сервере, админка вообще не имеет смысла офлайн.

Проблема. График не обновляется после добавления записи в офлайн-режиме. Пользователь добавил измерение, а на графике его нет. Лезу в код компонента с графиком - там стоит задержка перед обновлением, видимо чтобы дать серверу время ответить. Но в офлайн-режиме мы не ждём сервер, данные сразу в IndexedDB. Убираю setTimeout, делаю чтобы график обновлялся немедленно после завершения операции с локальной базой. Теперь работает мгновенно.

Вторая проблема. Пользователь работал офлайн, появилась сеть, синхронизация прошла успешно, но график показывает "нет данных". Ещё полчаса отладки. Оказывается компонент графика не отслеживает изменение статуса сети и не перезагружает данные автоматически. Добавляю переменную isOnline в зависимости React хука useEffect - теперь при любом изменении подключения график автоматически обновляется.

Последний штрих - иконки для установки PWA. Генерируем набор картинок разных размеров с лапками на градиентном фиолетовом фоне который совпадает с цветами нашего дизайна. Тестирую на телефоне - открываю сайт в Chrome, появляется предложение "Установить приложение". Жму, приложение устанавливается на домашний экран с нашей иконкой и названием Diabnostic. Выглядит как настоящее мобильное приложение, только сделано за вечер.

баннер "Офлайн-режим" с счётчиком операций
Мобильный вид

К половине второго ночи понедельника PWA полностью работает. Коммитов сорок семь, новых строк кода три с половиной тысячи. Иду спать с мыслью что один вечер превратил сайт в полноценное прогрессивное веб-приложение с офлайн-поддержкой.

Вторник вечером: кабинет ветеринара

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

Продумываю архитектуру сидя с чаем. Клиника - это юридическое лицо которое нанимает ветеринаров. У клиники есть главный врач с правами админа и обычные сотрудники. Клиника генерирует специальный инвайт-код типа CLINIC-ABC123XYZ789. Ветеринары регистрируются по этому коду и автоматически становятся частью клиники. Каждый ветеринар дополнительно получает свой личный VET-код который владельцы животных используют для подключения. Владелец вводит VET-код - его питомец появляется в списке пациентов этого ветеринара.

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

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

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

Пробую войти на следующий день под этим аккаунтом - получаю ошибку 500. Открываю логи сервера - там exception про отсутствующий VET-код. Копаюсь в коде час. Проблема в том что при регистрации через инвайт мы создаём пользователя и привязываем к клинике, но забываем сгенерировать ему личный VET-код. А этот код обязателен - без него владельцы не могут подключать к ветеринару своих питомцев. Добавляю автоматическую генерацию VET-кода при первом входе в систему после регистрации. Проблема решена.

Теперь управление клиникой. Главный врач должен видеть список всех сотрудников своей клиники и иметь возможность повышать их до админов, понижать обратно до обычных членов, или вообще отключать от клиники если человек уволился. Создаю три новых API эндпоинта - получить список членов клиники, изменить роль члена, отключить члена от клиники.

Интерфейс делаю простой таблицей. Столбцы - имя врача, email, роль, дата присоединения, действия. В столбце действий две кнопки для админа. Первая переключает роль между обычным членом и админом. Вторая отключает врача от клиники с подтверждением "вы уверены?". Обычный член клиники эти кнопки не видит вообще.

Скриншот: кабинет управления клиникой с таблицей врачей

Тестирую систему прав. Регистрирую через инвайт ещё двух "врачей" открывая режим инкогнито дважды. Одного повышаю до админа через интерфейс. Захожу под этим новым админом - вижу полный доступ к управлению клиникой. Захожу под обычным членом - вижу только свой список пациентов без кнопок управления. Система прав работает корректно.

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

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

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

кабинет ветеринара с поиском пациентов и AI-анализом
ИИ анализ
ИИ анализ
Страница пациентов в кабинете ветеринара
Страница пациентов в кабинете ветеринара

Вспоминаю что в предыдущей версии приложения был технический долг. Когда пользователь запускает AI-анализ, мы создаём фоновую задачу которая обращается к внешнему API и может работать до тридцати секунд. Статус этой задачи мы храним просто в памяти процесса Python. Проблема в том что если запустить несколько копий сервера для балансировки нагрузки, у каждой копии своя память. Пользователь отправил запрос на первый сервер, проверяет статус - попадает на второй сервер по round-robin, получает "задача не найдена".

Решение - Redis. Это быстрая база данных в памяти которая работает как единое хранилище для всех копий сервера. Переписываю весь TaskManager чтобы хранить задачи в Redis вместо обычного Python словаря. Добавляю автоматическое удаление старых задач через тридцать дней используя встроенный TTL механизм Redis.

Пока переделываю на Redis, попутно решаю ещё одну проблему. Когда сервер запускается, он выполняет миграции базы данных - создаёт новые таблицы или добавляет колонки если схема изменилась. Если запустить десять копий сервера одновременно, все десять попытаются мигрировать базу параллельно и получится race condition. Добавляю распределённую блокировку через Redis используя атомарную операцию SET NX - только первый процесс получит блокировку, остальные будут ждать. Блокировка автоматически освобождается через пять минут на случай если процесс упал.

К часу ночи вторника B2B платформа полностью готова. Клиники создаются, ветеринары регистрируются по инвайт-кодам, админы управляют командами, врачи видят своих пациентов, AI-анализ работает, Redis обеспечивает горизонтальное масштабирование без проблем с синхронизацией состояния.

Коммитов тридцать восемь, новых строк кода две тысячи восемьсот.

Довесок: восстановление пароля

Уже поздно, но остался последний пункт из списка - восстановление пароля. Функция нужная, а сделать относительно быстро.

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

Продумываю защиту. Rate limiting - максимум три запроса в час с одного IP на отправку письма. Email enumeration protection - отправляем одинаковый ответ "если этот email зарегистрирован, мы отправили письмо" независимо от того существует email или нет, чтобы нельзя было проверять регистрацию. Timing attack mitigation - для несуществующих email добавляем фейковую задержку чтобы время ответа было такое же как для существующих. Автоматическая инвалидация старых токенов - если пользователь запросил сброс пароля дважды, первый токен становится недействительным.

Создаю два новых API эндпоинта. Первый принимает email, проверяет captcha, создаёт токен сброса пароля, отправляет красивое HTML письмо с ссылкой. Второй принимает токен и новый пароль, проверяет что токен валидный и не истёк, меняет пароль, помечает токен использованным.

Пишу HTML шаблоны писем на русском и английском языках. Делаю их красивыми с брендингом приложения, большой синей кнопкой "Сбросить пароль", футером со ссылкой на сайт и disclaimer что это автоматическое письмо.

Вооот такое

Создаю две новые страницы во фронтенде. Первая /forgot-password с формой ввода email. Вторая /reset-password с формой нового пароля куда пользователь попадает после клика по ссылке из письма. Добавляю обработку всех возможных ошибок - токен не найден, токен истёк, токен уже был использован, новый пароль слишком слабый.

Забыли пароль? Ну что ж теперь.

Тестирую полный цикл. Ввожу свой тестовый email, жду минуту, получаю письмо, перехожу по ссылке, ввожу новый пароль. Работает! Пытаюсь использовать ту же ссылку ещё раз - получаю ошибку "токен уже был использован". Генерирую новую ссылку, жду больше часа, пытаюсь использовать - получаю ошибку "токен истёк". Всё работает согласно спецификации.

Прогоняю код через Bandit - это статический анализатор безопасности для Python который ищет типичные уязвимости. Находится несколько предупреждений средней важности про потенциальные SQL инъекции в скриптах начальной инициализации базы. Переписываю эти места используя параметризованные запросы вместо string interpolation. Повторный прогон показывает ноль средних и высоких находок.

Вывод Bandit и рейтинги Sonarqube
Что нашел бандит
Что нашел бандит
Для проекта на 17к LOC вполне неплохо
Для проекта на 17к LOC вполне неплохо

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

Прогоняю финальную батарею тестов. Все автотесты зелёные, покрытие держится на уровне 86%. SonarQube анализирует код и показывает ноль багов, ноль уязвимостей, ноль code smells. Staging окружение работает без единой ошибки.

Итоги

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

Сижу считаю сколько это заняло бы без AI. PWA с офлайн-синхронизацией - недели две. B2B платформа с ролями и правами - полторы недели. Восстановление пароля с защитой - дня четыре. Складываю - месяц работы, сто шестьдесят часов.

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

Манишке пока не лучше, буду честен. Средний сахар держится на 19.2 ммоль/л при норме 5-12. Только восемнадцать процентов всех измерений попадают в целевую зону. График показывает утренние скачки до тридцати трёх и опасные дневные провалы до трёх. Но теперь у меня есть полная картина происходящего - вижу паттерны, понимаю закономерности, могу показать ветеринару детальные графики прямо с телефона через QR-код. Врач сканирует и сразу понимает что происходит без моих попыток объяснить на пальцах.

 Главный бенефициар проекта одобряет разработку
Главный бенефициар проекта одобряет разработку

P.S. Если вы представляете ветеринарную клинику и хотели бы протестировать новый функционал управления клиниками и командами — буду рад дать доступ и помочь с настройкой. Система позволяет вести базу пациентов, управлять командой ветеринаров и отслеживать показатели глюкозы у всех подопечных животных в одном месте. Пишите в телеграм @diabnostic_support — обсудим детали и настроим всё под ваши нужды. ?

Комментарии (0)