Дневник питания - оффлайн и без сервера

Врач попросил вести дневник питания - время и что съел. Все готовые приложения хотят регистрацию, подписку, и именно времени приёма пищи - нет у многих. Написал своё: eat.x-crm.in - открыл, записал, закрыл. Бесплатно, с синхронизацией через гугл аккаунт.
Что умеет
- Записи за секунды - textarea + время (подставляется само), Ctrl+Enter. Каждая строка - отдельная позиция приёма пищи
- Подсказки по уже съеденному - на каждой строке дневник предлагает то, что вы уже записывали: набрали «овс» - подставили «Овсянка на воде» стрелками, Tab или кликом. Что ели чаще и недавнее - выше в списке
- Запрещённые продукты - то, что доктор велел исключить, помечается один раз; дальше такие позиции видны красным в дневнике и зачёркнуты на распечатке
- Оценка калорий по кнопке - со своим ключом OpenRouter у записи появляется «≈»: нажали - модель прикинула ккал, число осталось в записи (подробнее ниже)
- Работает оффлайн - service worker кэширует оболочку, данные живут в IndexedDB браузера. В метро, в самолёте - без разницы
- Ставится как приложение - пункт «Установить приложение» в меню кладёт значок на рабочий стол Android/десктопа, запускается в своём окне без адресной строки
- Синхронизация между устройствами - опционально, через ваш Google Диск
- Версия для печати - Ctrl+P или пункт «Печать»: чистый список с полными датами, чёрным по белому, в две-три колонки на лист. Можно выбрать диапазон дней, и дневник сам предложит продолжить с того, на котором кончилась прошлая распечатка - новые листы допечатываются к прошлым. То, ради чего всё затевалось - отдать врачу бумажку
- Экспорт и импорт JSON - резервная копия одним файлом, импорт сливает без дублей
Без сервера - это как?
Буквально: бэкенда нет. Статика лежит на GitHub Pages, а синхронизация ходит из браузера напрямую в Google Drive API - в скрытую папку приложения (appDataFolder), которую не видно среди файлов Диска и до которой другие приложения не дотянутся.
Авторизация - Google Identity Services, token flow: никакого client_secret, никакого промежуточного сервера, который читает ваши данные. Access token живёт около часа и продлевается только по клику - при запуске приложение не открывает никаких окон и вообще не ходит в сеть.
Источник истины - локальная база. Облако лишь реплика: слияние по правилу last-write-wins на каждую запись, удаления разносятся tombstone-ами и вычищаются через 30 дней. Одновременная правка на двух устройствах не превращается в кашу - побеждает более поздняя версия.
Калории - чужой моделью, но на своём ключе
Считать калории руками - тоска, а «умные» трекеры просят за это подписку. Здесь сделано дёшево и честно: вы вписываете свой ключ OpenRouter в настройках, и браузер ходит в их API напрямую - CORS у них открыт, бэкенд опять не нужен.
Выглядит это так: у записи появляется кнопка «≈», по клику модель получает текст записи и возвращает одно число - примерные ккал. Число ложится в саму запись, едет на другие устройства вместе с ней и стирается, если текст записи изменили: старая оценка к новому тексту уже не относится. Никаких фоновых запросов - не нажали, ничего никуда не ушло.
По умолчанию стоит mistralai/mistral-small-3.2-24b-instruct - победитель замера в августе 2026: правдоподобные оценки на всех пробах, около 0.4 с и порядка $0.0005 за сотню оценок. Модель меняется в настройках, список подтягивается из OpenRouter. Рассуждающие модели брать не советую - они жгут невидимые reasoning-токены, и реальная цена выходит в десятки раз выше заявленной.
Ключ хранится вместе с остальными настройками и едет на другие устройства тем же файлом на Google Диске. Внутри файла он в base64 - это защита от случайного взгляда, а не шифрование, и притворяться иначе не буду.
Приватность
Данные хранятся ровно в двух местах: в вашем браузере и (если включили синхронизацию) в скрытой папке вашего Google Диска. Промежуточных серверов нет, аналитики нет, cookies нет. Отозвать доступ можно в настройках Google-аккаунта - приложение при этом продолжит работать локально.
Единственное исключение - оценка калорий: по нажатию «≈» текст записи уходит в OpenRouter на вашем же ключе. Не нужно - не вписывайте ключ, кнопки просто не будет.
Не залогинившись, получаете то же самое минус синхронизация - для дневника «на неделю для врача» и этого хватает.
Под капотом
React 19 + TypeScript + Vite, vite-plugin-pwa (Workbox), IndexedDB через idb. Логика слияния, подсказки и разбор ответа модели - чистые функции, покрытые тестами через node --test без Jest. Деплой - GitHub Actions на поддомен, public/CNAME в артефакте.
Исходники открыты: github.com/william-aqn/eat. Форкается в свой вариант за вечер: словарь нового языка - один файл, замена облака - один адаптер (RemoteStore), палитра - CSS-переменные в :root.
Ссылки
- Приложение: eat.x-crm.in
- Код: github.com/william-aqn/eat