Домашний канбан: как я собрал его с ИИ-агентом за неделю

Доска «Наш дом»: задачи, подзадачи, связи и магниты исполнителей

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

За неделю она выросла из одного сообщения в полноценное приложение: восемь релизов, вход через Google, установка на телефон, два языка и выкатка через ArgoCD. Делал я её вместе с ИИ-агентом, и эта запись в основном о том, как это было. Код открыт: github.com/gera-corp/to-do-board.

С чего началось

Первое сообщение было почти бытовым: нужна канбан-доска с колонками To Do, In Progress, Review и Done, пользователи, назначение задач, база на SQLite. Язык программирования — на выбор агента. Он выбрал FastAPI и фронтенд на чистом JavaScript без сборки: одна база в одном файле, одна команда для запуска и никакого второго набора зависимостей. Ровно та же логика, по которой когда-то собирался этот блог.

К концу первого вечера работали доска с перетаскиванием, пользователи с паролями, сроки, категории и повторяющиеся дела. Задача «Вынести мусор» уходит в Done, и следующая появляется в To Do со сдвинутым сроком. В ответ на моё «может, ещё что-то нужно» агент предложил список: уведомления в Telegram, чек-листы, комментарии и историю, очки и рейтинг. Почти всё из этого я в итоге взял.

Как росла, по релизам

  • 0.1.0, 16 октября. Доска, пользователи, повторы, чек-листы, история с комментариями, очки и рейтинг, Telegram-бот с утренней сводкой. Сразу же — контейнер, CI и Helm-чарт с секретами из Vault.
  • 0.2.0. Семь тем оформления, тема хранится в профиле.
  • 0.2.1. Переезд в ArgoCD. Чарт публикуется в GHCR только после того, как собран образ той же версии.
  • 0.2.2. Значок с очками наезжал на магнит. Нашёлся, когда снимали скриншоты для первой версии этого поста.
  • 0.3.0. Приложение на телефон (PWA) и режим без сети.
  • 0.4.0, 18 октября. Вход через Google и защита входа по паролю.
  • 0.5.0, 23 октября. Подзадачи и связи между задачами.
  • 0.6.0, 23 октября. Английский и русский интерфейс, релизы на GitHub с описанием изменений.

Что умеет

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

Большую задачу можно разбить на подзадачи для разных людей. «Ремонт в детской» распадается на «Купить краску», «Покрасить стены» и «Вынести строительный мусор», и каждый получает очки за свою часть. Задачи можно связать: «Покрасить стены» ждёт «Купить краску», и это видно прямо на карточке. Когда всё, чего ждала задача, сделано, исполнитель получает в Telegram «Можно начинать».

Окно задачи: чек-лист, подзадачи, связи, а справа история и комментарии

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

Рейтинг за неделю

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

Оформление: язык и семь тем, каждое превью нарисовано цветами своей темы

Та же доска в теме «Полночь»

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

Телефон без сети: доска открыта из кеша, изменения недоступны

Входят по паролю или через Google. Через Google пускают только тех, кого администратор сам завёл на доске, так что посторонний с Google-аккаунтом не попадёт внутрь.

Страница входа

Как мы работали

Схема простая: я говорю, чего хочу, агент делает. Но интереснее не это, а где он останавливался и спрашивал.

Он спрашивал, когда решение было моим: как назвать репозиторий, в какой кластер ставить, на каком домене, что делать с паролями, когда появится Google. В ~/.kube у меня несколько kubeconfig, и не все от домашнего кластера. Агент не стал угадывать: перечислил, что нашёл, и дождался, пока я выберу нужный. Перед тем как что-то менять в кластере, он сначала смотрел, как там устроены соседние приложения, и делал так же: тот же ingress, тот же issuer для сертификатов, та же схема секретов.

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

Что сломалось

Вход, который иногда не входил. После логина следующий запрос время от времени получал «Нужно войти». В FastAPI код после yield в зависимости по умолчанию выполняется уже после отправки ответа, а там был commit. Браузер успевал прислать следующий запрос раньше, чем сессия попадала в базу. Тесты были зелёные: в тестовом клиенте запросы идут строго по очереди. Лечится одним параметром:

Conn = Depends(get_conn, scope="function")

WAL на NFS. В кластере база лежит на томе NFS, а режим WAL держит общую память рядом с файлом базы и на сетевой файловой системе ненадёжен. Режим журнала стал настройкой, в кластере стоит DELETE.

Distroless без shell. Образ собран на distroless: нет shell, нет пакетного менеджера, процесс не от root. Первый запуск упал с unable to open database file: каталога /data в образе не было, том создался от root, и писать было некуда. Заглянуть внутрь тоже нельзя, docker exec ... sh отвечает, что sh не найден. Это и есть hardening в действии.

Dependabot в первую же минуту. Сразу после создания репозитория он предложил поднять Python в сборочной стадии до 3.14. CI зелёный, PR выглядит безобидно. Только финальный образ работает на 3.13, и зависимости собираются ровно под неё. PR закрыт, минорные версии Python в Dependabot исключены.

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

Тест, который проверял не то. Режим без сети проверяли в Playwright, отключая сеть эмуляцией. Тест был зелёный, но плашка «нет связи» не появлялась. Оказалось, эмуляция не действует на сам service worker: он продолжал ходить на сервер, и доска честно считала, что сеть есть. Переписали проверку: сервер останавливается по-настоящему.

Новая версия со старым CSS. Честный тест сразу нашёл настоящий баг. При обновлении service worker собирал новую версию интерфейса, но брал файлы из HTTP-кеша браузера, и «новая» доска получала старые стили. Без проверки это всплыло бы на первом же релизе у всей семьи. Теперь предзагрузка идёт мимо кеша.

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

Как она попадает в кластер

Путь релиза: проверки, образ, чарт и релиз, ArgoCD и под в кластере

Релиз — это git-тег. Дальше всё само:

  1. Тесты, hadolint, helm lint и проверка, что в CHANGELOG есть запись для этой версии.
  2. Образ под amd64 и arm64: в кластере есть нода на Raspberry Pi. К образу прикладываются SBOM и подписанная аттестация происхождения, Trivy сканирует результат.
  3. Helm-чарт публикуется в GHCR с той же версией, а на GitHub появляется релиз с описанием изменений. Порядок важен: ArgoCD физически не может увидеть версию, для которой ещё нет образа.
  4. ArgoCD синхронизирует приложение.

Секреты, токен Telegram-бота и ключ для входа через Google, живут в Vault. В манифесте лежит только ссылка вида vault:secret/data/...#TELEGRAM_BOT_TOKEN, а bank-vaults webhook подставляет значение прямо в окружение процесса при старте. Ни в Kubernetes Secret, ни в etcd оно не попадает.

Под проходит профиль Pod Security restricted: без root, без capabilities, файловая система только для чтения. NetworkPolicy пускает к нему только ingress-контроллер. Вход по паролю защищён от перебора, а время ответа не выдаёт, существует ли логин. Реплика всегда одна, обновление идёт через Recreate: SQLite не терпит двух писателей. При релизе доска пропадает на несколько секунд, и для домашнего списка дел это честный обмен. Том с базой помечен так, что ни prune, ни удаление приложения в ArgoCD его не тронут.

Что дальше

Дома стало заметно меньше разговоров «а кто должен был». Из идей на будущее: кнопки «Взять в работу» и «Готово» прямо в сообщениях Telegram и дежурства по очереди, чтобы мусор выносили по графику, а не по памяти.