[{"data":1,"prerenderedAt":460},["ShallowReactive",2],{"cases":3},[4,98,157,201,250,296,348,400],{"slug":5,"path":6,"title":7,"summary":8,"category":9,"status":10,"role":11,"period":12,"stack":15,"order":23,"featured":24,"metrics":25,"gallery":38,"links":39,"client":14,"team":40,"keywords":41,"terminalAlias":49,"accentHue":50,"hasCover":51,"clientUnderNda":51,"updatedAt":52,"window":53,"body":56},"animals-world","\u002Fprojects\u002Fanimals-world","My Animals World","Районная сеть для владельцев животных: карта, прогулки, чат, лента и распределённая модерация.","product","in-progress","Автор и разработчик",{"from":13,"to":14},2025,null,[16,17,18,19,20,21,22],"Nuxt 3","NestJS","Prisma","PostgreSQL","Flutter","TypeScript","Turborepo",5,true,[26,29,32,35],{"value":27,"label":28},"4","клиента на ядре",{"value":30,"label":31},"52k","строк Flutter",{"value":33,"label":34},"46k","строк TS и Vue",{"value":36,"label":37},"90+","тестов",[],[],"Один разработчик",[42,43,44,45,46,47,48],"продукт","монорепо","nestjs","prisma","flutter","карта","модерация","animals",150,false,"2026-08-19",{"w":54,"h":55},980,720,{"type":57,"value":58,"toc":91},"minimark",[59,63,68,71,75,78,81,84,88],[60,61,62],"p",{},"Свой продукт в разработке: соседи с животными видят, кто рядом, договариваются\nо прогулках, читают районную ленту и поднимают тревогу, если питомец пропал.",[64,65,67],"h2",{"id":66},"задача","Задача",[60,69,70],{},"Сделать не «ленту города», а район: охват считается от точки дома, а не от подписок.\nИз этого следует почти всё остальное — от модели данных до модерации, которая\nне может быть централизованной, потому что дежурных нет.",[64,72,74],{"id":73},"решение","Решение",[60,76,77],{},"Монорепо на Turborepo с четырьмя клиентами на одном ядре: веб на Nuxt 3,\nадминка модерации, API на NestJS с Prisma и PostgreSQL, мобильное приложение\nна Flutter со своим дизайн-слоем. Общие схемы — на Zod в отдельном пакете,\nпоэтому контракт один и для API, и для веба.",[60,79,80],{},"Модерация распределённая: жалоба на запись сообщества идёт сначала владельцу\nсообщества со суточным сроком и только потом эскалируется. У каждого скрытия\nесть причина, у каждого действия — след в журнале: на вопрос «за что удалили»\nотвечает факт, а не память модератора.",[60,82,83],{},"Качество держится проверками, а не аккуратностью: типы, дубликаты кода,\nмёртвый код, юнит-тесты и e2e — одной командой, до коммита.",[64,85,87],{"id":86},"результат","Результат",[60,89,90],{},"В разработке. Сейчас это 46 тысяч строк TypeScript и Vue, 52 тысячи строк Dart,\n91 набор тестов и 18 миграций базы. Карта, прогулки, чат и лента работают,\nмобильный клиент собирается под обе платформы.",{"title":92,"searchDepth":93,"depth":93,"links":94},"",2,[95,96,97],{"id":66,"depth":93,"text":67},{"id":73,"depth":93,"text":74},{"id":86,"depth":93,"text":87},{"slug":99,"path":100,"title":101,"summary":102,"category":103,"status":104,"role":105,"period":106,"stack":108,"order":114,"featured":24,"metrics":115,"gallery":119,"links":120,"client":121,"team":122,"keywords":123,"terminalAlias":129,"accentHue":130,"hasCover":51,"clientUnderNda":51,"updatedAt":52,"window":131,"body":132},"shtab-automation","\u002Fprojects\u002Fshtab-automation","Конструктор автоматизаций","Универсальный конструктор триггеров, условий и действий: одна модель покрывает более 500 сценариев.","admin","live","Frontend Developer",{"from":107,"to":14},2024,[109,21,110,111,112,113],"Vue 3","Pinia","TanStack Query","SCSS","Vite",10,[116],{"value":117,"label":118},"500+","сценариев",[],[],"Shtab","~6 фронтенд-разработчиков",[124,125,126,127,128],"автоматизация","триггеры","конструктор","no-code","таск-менеджер","automation",255,{"w":54,"h":55},{"type":57,"value":133,"toc":152},[134,137,139,142,144,147,149],[60,135,136],{},"Модуль автоматизаций в таск-менеджере: пользователь собирает правило вида\n«когда произошло это и выполнено условие — сделай то», не открывая код.",[64,138,67],{"id":66},[60,140,141],{},"Каждый новый тип автоматизации приходил как отдельная фича: свой экран, своя форма,\nсвоя валидация. Список запросов от пользователей рос быстрее, чем команда успевала\nих закрывать, а похожие правила приходилось описывать разными интерфейсами.",[64,143,74],{"id":73},[60,145,146],{},"Спроектировал и реализовал один конструктор вместо набора экранов: триггеры, условия\nи действия описаны как данные, а интерфейс собирается из этого описания. Новый тип\nправила добавляется расширением словаря, а не новой страницей.",[64,148,87],{"id":86},[60,150,151],{},"Одна модель покрывает более пятисот пользовательских сценариев. Запрос «а можно,\nчтобы при смене статуса ещё и уведомляло ответственного» перестал быть задачей\nна разработку и стал настройкой.",{"title":92,"searchDepth":93,"depth":93,"links":153},[154,155,156],{"id":66,"depth":93,"text":67},{"id":73,"depth":93,"text":74},{"id":86,"depth":93,"text":87},{"slug":158,"path":159,"title":160,"summary":161,"category":103,"status":104,"role":105,"period":162,"stack":163,"order":165,"featured":24,"metrics":166,"gallery":167,"links":168,"client":121,"team":122,"keywords":169,"terminalAlias":173,"accentHue":174,"hasCover":51,"clientUnderNda":51,"updatedAt":52,"window":175,"body":176},"shtab-calendar","\u002Fprojects\u002Fshtab-calendar","Календарь задач","Новый формат представления данных в таск-менеджере, сопоставимый по функциональности с Google Calendar.",{"from":107,"to":14},[109,21,164,110,112],"Pragmatic DnD",20,[],[],[],[170,171,172,128],"календарь","drag and drop","планирование","calendar",210,{"w":54,"h":55},{"type":57,"value":177,"toc":196},[178,181,183,186,188,191,193],[60,179,180],{},"Календарное представление задач: расписание вместо списка, с перетаскиванием,\nдиапазонами и несколькими масштабами времени.",[64,182,67],{"id":66},[60,184,185],{},"Задачи жили списком и доской. Планировать неделю на них неудобно: не видно\nзагрузки по дням и пересечений, а срок — это просто поле в карточке.",[64,187,74],{"id":73},[60,189,190],{},"Реализовал календарь как отдельный формат данных, а не как ещё один фильтр списка:\nсвоя модель раскладки, перетаскивание на Pragmatic DnD, работа с диапазонами\nи повторяющимися интервалами. Ориентир по функциональности — Google Calendar\nи Notion Calendar: то, что пользователь уже умеет, не должно требовать обучения.",[64,192,87],{"id":86},[60,194,195],{},"Планирование перестало быть чтением списка сроков. Задача переносится\nперетаскиванием, загрузка по дням видна сразу.",{"title":92,"searchDepth":93,"depth":93,"links":197},[198,199,200],{"id":66,"depth":93,"text":67},{"id":73,"depth":93,"text":74},{"id":86,"depth":93,"text":87},{"slug":202,"path":203,"title":204,"summary":205,"category":206,"status":104,"role":105,"period":207,"stack":208,"order":211,"featured":24,"metrics":212,"gallery":213,"links":214,"client":121,"team":122,"keywords":215,"terminalAlias":222,"accentHue":223,"hasCover":51,"clientUnderNda":51,"updatedAt":52,"window":224,"body":225},"shtab-comments","\u002Fprojects\u002Fshtab-comments","Модуль комментариев","Переиспользуемое обсуждение уровня мессенджера: треды, реакции, упоминания, голосовые, rich-text.","design-system",{"from":107,"to":14},[109,21,209,110,112,210],"WebSocket","Storybook",30,[],[],[],[216,217,218,219,220,221],"комментарии","треды","реакции","упоминания","rich-text","websocket","comments",285,{"w":54,"h":55},{"type":57,"value":226,"toc":245},[227,230,232,235,237,240,242],[60,228,229],{},"Обсуждение, которое встраивается в любую сущность продукта: задачу, документ,\nпроект — без копирования кода и без отдельной реализации на каждом экране.",[64,231,67],{"id":66},[60,233,234],{},"Комментарии в продукте были нужны в нескольких местах, и каждое место\nноровило получить свою версию: свой ввод, своё дерево ответов, свои реакции.\nТакой набор расходится через месяц: где-то нет упоминаний, где-то нет цитирования.",[64,236,74],{"id":73},[60,238,239],{},"Собрал один модуль с полным набором взаимодействий мессенджера: древовидные треды,\nреакции, цитирование, упоминания, голосовые сообщения, интеграция\nс rich-text-редактором. Модуль подключается к любой сущности и не знает,\nво что он встроен, — тот же принцип, по которому написан этот сайт.",[64,241,87],{"id":86},[60,243,244],{},"Комментарии перестали быть фичей экрана и стали возможностью продукта. Новое\nместо подключает готовое обсуждение, а не пишет его заново.",{"title":92,"searchDepth":93,"depth":93,"links":246},[247,248,249],{"id":66,"depth":93,"text":67},{"id":73,"depth":93,"text":74},{"id":86,"depth":93,"text":87},{"slug":251,"path":252,"title":253,"summary":254,"category":206,"status":104,"role":105,"period":255,"stack":256,"order":258,"featured":51,"metrics":259,"gallery":260,"links":261,"client":121,"team":122,"keywords":262,"terminalAlias":269,"accentHue":50,"hasCover":51,"clientUnderNda":51,"updatedAt":52,"window":270,"body":271},"shtab-vue3","\u002Fprojects\u002Fshtab-vue3","Миграция на Vue 3 и UI Kit","Перенос архитектуры с Vue 2 на Vue 3 и единый UI Kit вместо разошедшихся копий компонентов.",{"from":107,"to":13},[109,21,110,257,112,210,113],"Vuex",40,[],[],[],[263,264,265,266,267,268],"миграция","vue 3","ui kit","легаси","рефакторинг","storybook","vue3",{"w":54,"h":55},{"type":57,"value":272,"toc":291},[273,276,278,281,283,286,288],[60,274,275],{},"Перевод продукта с Vue 2 на Vue 3 и сборка UI Kit — параллельно с продуктовой\nразработкой, без остановки релизов.",[64,277,67],{"id":66},[60,279,280],{},"Приложение на Vue 2 с накопленным легаси: устаревшие компоненты, разные стилевые\nпаттерны в разных модулях, дублирующиеся решения одной и той же задачи.\nОстанавливать продукт на переписывание нельзя.",[64,282,74],{"id":73},[60,284,285],{},"Перенёс архитектуру и ключевые модули на Vue 3, сокращая легаси-код по мере\nдвижения. Параллельно переписал устаревшие компоненты в UI Kit и унифицировал\nстили и паттерны, чтобы новые экраны собирались из общего набора,\nа не из копий. Плюс code review и разбор архитектурных решений в команде\nиз шести фронтенд-разработчиков.",[64,287,87],{"id":86},[60,289,290],{},"Продукт на актуальном фреймворке, компоненты — в одном месте с одним набором\nправил. Стилевые расхождения между модулями перестали появляться сами.",{"title":92,"searchDepth":93,"depth":93,"links":292},[293,294,295],{"id":66,"depth":93,"text":67},{"id":73,"depth":93,"text":74},{"id":86,"depth":93,"text":87},{"slug":297,"path":298,"title":299,"summary":300,"category":103,"status":301,"role":105,"period":302,"stack":304,"order":309,"featured":51,"metrics":310,"gallery":311,"links":312,"client":313,"keywords":314,"terminalAlias":320,"accentHue":321,"hasCover":51,"clientUnderNda":51,"updatedAt":52,"window":322,"body":323},"latoken-dashboards","\u002Fprojects\u002Flatoken-dashboards","Дашборды кампаний криптобиржи","Панели управления Trading Competition и Airdrops: настройка активностей вместо ручных запусков.","archive",{"from":303,"to":107},2023,[305,21,306,307,308],"React","MobX","GraphQL","Material UI",50,[],[],[],"Latoken",[315,316,317,318,319],"дашборд","криптобиржа","кампании","airdrop","a\u002Fb тесты","latoken",45,{"w":54,"h":55},{"type":57,"value":324,"toc":343},[325,328,330,333,335,338,340],[60,326,327],{},"Интерфейсы управления маркетинговыми активностями криптобиржи и продуктовая\nлогика для трейдинга.",[64,329,67],{"id":66},[60,331,332],{},"Запуск торгового соревнования или airdrop требовал участия разработки: параметры\nкампаний правились не там, где ими управляют, а данные по активностям собирались\nиз разных мест.",[64,334,74],{"id":73},[60,336,337],{},"Разработал логику дашборда редактирования Trading Competition и дашборда для\nAirdrops с настройкой параметров кампаний, а также информационный модуль\nс актуальными данными по активностям. Обновил интерфейс Listing Competitions\nи UI лендинга по токенам. Подключил аналитику (Segment, Google Analytics)\nи A\u002FB-тесты, чтобы решения о доработках опирались на метрики,\nа не на предположения.",[64,339,87],{"id":86},[60,341,342],{},"Запуск кампании перестал быть задачей на разработку. Данные по активностям\nвидны в одном месте, а изменения в интерфейсе проверяются тестами\nна реальном трафике.",{"title":92,"searchDepth":93,"depth":93,"links":344},[345,346,347],{"id":66,"depth":93,"text":67},{"id":73,"depth":93,"text":74},{"id":86,"depth":93,"text":87},{"slug":349,"path":350,"title":351,"summary":352,"category":103,"status":301,"role":105,"period":353,"stack":355,"order":361,"featured":51,"metrics":362,"gallery":363,"links":364,"client":365,"keywords":366,"terminalAlias":372,"accentHue":373,"hasCover":51,"clientUnderNda":51,"updatedAt":52,"window":374,"body":375},"oz-ecommerce","\u002Fprojects\u002Foz-ecommerce","Дашборд для e-commerce","SPA с бизнес-логикой панели управления: данные, фильтры, формы, управление сущностями.",{"from":354,"to":354},2022,[305,21,356,357,358,359,360],"Redux","Node.js","Express","MongoDB","Nginx",60,[],[],[],"OZ-Digital",[367,315,368,369,370,371],"e-commerce","spa","react","node","mongodb","oz",320,{"w":54,"h":55},{"type":57,"value":376,"toc":395},[377,380,382,385,387,390,392],[60,378,379],{},"Панель управления магазином и SPA-интерфейсы: от вёрстки до серверного окружения.",[64,381,67],{"id":66},[60,383,384],{},"Небольшая команда, широкий фронт работ: нужно было закрыть и клиентские интерфейсы,\nи бизнес-логику панели, и обвязку вокруг деплоя.",[64,386,74],{"id":73},[60,388,389],{},"Собрал SPA на React и TypeScript с интеграцией API и работой с состоянием,\nреализовал ключевую логику дашборда: отображение данных, фильтры, формы,\nуправление сущностями. Написал небольшие backend-модули на Node.js и Express\nс MongoDB и настроил базовое серверное окружение на Linux и Nginx —\nконфигурацию окружений и деплой.",[64,391,87],{"id":86},[60,393,394],{},"Опыт по обе стороны API: понимание, во что превращается на клиенте решение,\nпринятое в контракте бэкенда. Он пригодился на всех следующих проектах.",{"title":92,"searchDepth":93,"depth":93,"links":396},[397,398,399],{"id":66,"depth":93,"text":67},{"id":73,"depth":93,"text":74},{"id":86,"depth":93,"text":87},{"slug":401,"path":402,"title":403,"summary":404,"category":9,"status":104,"role":11,"period":405,"stack":406,"order":409,"featured":51,"metrics":410,"gallery":416,"links":417,"client":14,"team":40,"keywords":418,"terminalAlias":424,"accentHue":425,"hasCover":51,"clientUnderNda":51,"updatedAt":52,"window":426,"body":427},"vpn-service","\u002Fprojects\u002Fvpn-service","Сервис доступа с Telegram-ботом","Выдача и учёт ключей через бота, мультисерверная маршрутизация и админка со статистикой.",{"from":13,"to":14},[357,358,407,408,109,110],"Telegraf","SQLite",70,[411,414],{"value":412,"label":413},"18k","строк кода",{"value":415,"label":37},"35",[],[],[419,420,421,422,423],"telegram bot","инфраструктура","sqlite","админка","ключи доступа","vpn",200,{"w":54,"h":55},{"type":57,"value":428,"toc":455},[429,432,434,437,439,442,450,452],[60,430,431],{},"Свой сервис в проде: пользователь получает доступ в переписке с ботом,\nадминистратор видит состояние узлов и статистику в веб-панели.",[64,433,67],{"id":66},[60,435,436],{},"Раздавать и учитывать ключи доступа руками не получается: нужно знать,\nкакой ключ у кого, какой узел жив, и уметь переключить пользователей\nна другой сервер, не переписывая всех по одному.",[64,438,74],{"id":73},[60,440,441],{},"Бот на Telegraf как единственный интерфейс выдачи: пользователю не нужно\nни приложение, ни регистрация. Учёт ключей и привязок — в SQLite, чтобы\nу сервиса не было внешних зависимостей. Веб-админка на Vue 3 показывает узлы,\nзанятость ключей и статистику; маршрутизация мультисерверная, узел переключается\nнастройкой, а не переносом пользователей.",[60,443,444,445,449],{},"Сборка проверяется одной командой: формат, линт, поиск мёртвого кода, тесты,\nсборка и ",[446,447,448],"strong",{},"сканирование секретов"," — последнее для инфраструктурного проекта\nобязательно, потому что цена утечки ключа здесь выше цены любого бага.",[64,451,87],{"id":86},[60,453,454],{},"Работает в проде. Выдача занимает у пользователя одно сообщение, у администратора —\nодин экран. Восемнадцать тысяч строк, тридцать пять наборов тестов.",{"title":92,"searchDepth":93,"depth":93,"links":456},[457,458,459],{"id":66,"depth":93,"text":67},{"id":73,"depth":93,"text":74},{"id":86,"depth":93,"text":87},1787136563299]