- под рантаймом bun vue-tsc не видит .vue: подмена fs.readFileSync не перехватывает загрузчик модулей - остальное (vite, vitest, biome, audit) работает, но прироста скорости на нашем объёме нет - записаны обходные пути, цена переезда и условия, при которых вернуться к вопросу
Личный трекер питания: записал текстом или снимком — сервер разобрал
запись на позиции и посчитал калории, белки, жиры и углеводы.
Что это
Self-hosted приложение для контроля питания на своём сервере: один бинарь на Go, одна база SQLite, клиент — PWA внутри того же бинаря. Отдельная причина, по которой проект вообще существует: прошлый трекер стал недоступен в стране, и повторения не хочется — система должна принадлежать своему хозяину целиком.
Запись — это событие с сырым содержимым: строка «каша овсяная на молоке 300 г, кешью 15 г», снимок тарелки, время. Клиент только фиксирует его и кладёт в очередь; разбор на позиции и подсчёт КБЖУ идут на сервере асинхронно. На экране — дневник дня, карточка приёма с позициями и остаток дневной нормы.
Пользователей у системы двое, и оба равноправны. Человек записывает еду, смотрит остаток нормы и правит неточности. AI-ассистент раз в неделю читает данные, обсуждает их с человеком и обновляет нормы: у питания есть своя половина картины, а сон, активность и анализы ассистент уже видит. Поэтому отдача данных наружу — не «экспорт как дополнительная фича», а вторая точка входа, под которую спроектирована модель данных.
Принципы
- Слушаем, но не осуждаем. Приложение фиксирует, а не морализирует. Факты показывать можно — «четвёртый подход сегодня», «сахар за день», — оценку фактам давать нельзя. Отсюда и палитра: у перебора нормы нет цвета тревоги, а единственный красный достаётся необратимому действию, но никогда еде.
- Сама запись и есть вмешательство. Необходимость залогировать конфету останавливает от её поедания. Следствие неочевидное: делать запись сладостей незаметной вредно. Скорость — там, где нет проблемы, осознанность — там, где она есть.
- У каждого числа есть источник. Измерено на весах, взято из каталога, оценено моделью, введено человеком. Ассистент должен понимать, спорит он с измерением или с догадкой.
- Прошлое не переписывается молча. Правка задним числом нормальна, но позиция журнала хранит собственный снимок названия и цифр: изменение каталога не трогает уже сделанные записи.
Чего приложение сознательно не делает: не соцсеть, не медицинский дневник, не тренер и не считает сожжённые калории. Остаток = норма − съеденное. Подробнее — в docs/product.md.
Как устроено
- Захват на клиенте, обработка на сервере. Клиенту не нужны ни модель, ни разбор ввода, и чисел он не считает: любой ввод, в том числе прилинкованный продукт каталога, уходит на разбор на сервер.
- Offline-first. Источник правды до отправки — локальная очередь на устройстве. Как появилась сеть, сырые данные уезжают на сервер; опорный сценарий — поезд: в пути ввёл текст и фото, на станции синхронизировал.
- Обычный REST вместо своего протокола синхронизации. Очередь проигрывает отложенные
запросы по порядку, от дублей защищают ULID с клиента, при разошедшихся правках
побеждает последний
PUT. Обратной очереди нет: сервер шлёт маркеры изменений, клиент перечитывает день целиком. - PWA — целевой вид приложения. Нативные обёртки и TestFlight отвергнуты, ограничения iOS-браузера принимаются и обходятся.
Подробнее — в docs/architecture.md и docs/pipeline.md.
Стек
| Часть | Чем сделано |
|---|---|
| Сервер | Go, SQLite; один бинарь, внутрь него вшит собранный клиент |
| Клиент | TypeScript, Vue, Vuetify, Dexie для локальной базы; SPA + PWA |
| Вход людей | Authelia перед приложением, личность приходит заголовками |
| Вход машин | долгоживущие токены: ассистент и скрипты через forward-auth не пройдут |
| AI | Bifrost как OpenAI-совместимый шлюз, за ним сменные провайдеры |
| Тулинг | Task, golangci-lint, Biome, lefthook, гейт на Python |
Что в репозитории
| Путь | Что там |
|---|---|
| cmd/diet | точка входа сервера |
| internal | разбор приёмов, хранилища, HTTP API, воркер, вход, конфигурация |
| frontend | клиент на Vue; собранная сборка вшивается в бинарь |
| docs | решения и объяснения «почему так», конвенции, ADR, исследования |
| tasks | что делаем дальше: задача на документ, по убыванию приоритета |
| scripts | гейт, проверка инструментов, запуск Node в контейнере |
Запуск локально
Нужны Go (версия — в go.mod), Python 3, Docker (в нём собирается фронт)
и Task. Что именно и каких версий — в tools.json,
проверяется командой task setup.
task setup # инструменты на месте?
cp config.example.toml config.toml # и поправить пути под себя
task front:install && task front:build
task run
В конфиге обязательного немного: адрес прослушивания, путь к базе, каталоги файлов
и кеша снимков. Оба шага разбора умеют работать заглушкой (type = "stub" в [llm.*]) —
приложение поднимается и живёт без ключей к модели. Для запуска по HTTP без TLS нужен
auth.secure_cookie = false, иначе браузер не сохранит сессионную куку. Личность
приложение берёт из заголовка Remote-User, так что перед ним в любом случае стоит
прокси, который этот заголовок ставит, — в рабочем контуре это Authelia.
Перед коммитом — task gate: все проверки в фиксированном порядке, до конца, со сводкой
в конце. Код 0 — сошлось, 1 — расхождение, 3 — в системе нет инструмента и проверка
не выполнялась.
Развёртывание за периметром — Caddy, Authelia, бэкапы — описано в docs/deployment.md.
Подключение ассистента
Второй пользователь системы ходит за данными по MCP — эндпоинт отдаёт сам сервер, отдельный процесс-мост ставить не нужно.
Сначала человек выпускает себе личный токен: в приложении Настройки → Токены доступа → Создать. Текст токена показывается один раз, при выпуске, — потом его можно только отозвать и выпустить новый.
claude mcp add --transport http --scope user diet \
https://<адрес приложения>/api/v1/mcp \
--header "Authorization: Bearer <токен>"
Область user, а не local: ассистент разбирает неделю рядом с медицинской картой,
а не в этом репозитории, и сервер должен быть виден ему отовсюду. Проверить, что
подключилось, — командой /mcp в Claude Code; там же видно четыре инструмента:
приёмы пищи за период, один приём, чтение нормы и её запись.
Отдаёт эндпоинт ровно то же, что обычное API, и ограничен данными владельца токена. Журнал ассистент только читает: инструмент ввода еды — само приложение. Что именно доступно и почему — в docs/assistant-api.md.
Куда смотреть дальше
| Документ | О чём |
|---|---|
| docs/README.md | карта всех решений: продукт, сценарии, модель данных, API, интерфейс |
| docs/conventions/README.md | правила, общие для всего кода, утверждениями с номерами |
| tasks/README.md | список работ: цель, описание, критерии приёмки, цена бездействия |
| docs/history.md | летопись: что сделано, что отменили и почему |
| CLAUDE.md | как ведётся работа над проектом и что известно о его хозяине |