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

Дома постоянно всплывает один и тот же разговор: кто выносил мусор, кто обещал позвонить сантехнику и почему продуктов опять нет. Готовых трекеров полно, но они либо про работу, либо живут на чужой площадке. Мне хотелось простую доску для семьи, которая крутится у меня в кластере рядом с блогом.
За неделю она выросла из одного сообщения в полноценное приложение: восемь релизов, вход через 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, и окно задачи упало бы с ошибкой на первом же открытии. Нашлось до релиза, а тесты теперь следят, чтобы у каждой строки был перевод.
Как она попадает в кластер

Релиз — это git-тег. Дальше всё само:
- Тесты, hadolint,
helm lintи проверка, что в CHANGELOG есть запись для этой версии. - Образ под amd64 и arm64: в кластере есть нода на Raspberry Pi. К образу прикладываются SBOM и подписанная аттестация происхождения, Trivy сканирует результат.
- Helm-чарт публикуется в GHCR с той же версией, а на GitHub появляется релиз с описанием изменений. Порядок важен: ArgoCD физически не может увидеть версию, для которой ещё нет образа.
- 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 и дежурства по очереди, чтобы мусор выносили по графику, а не по памяти.