NBA Feed
Your teams' news and games, with scores hidden until you reveal them.
Manage a power cycle in a fantasy setting, tracking the hero's progress and training.
AI translation
Вести силовой цикл в фэнтези-сеттинге, отслеживая прогресс героя и тренировки
AI summary · README
Sports & fitnessJavaScript · CSSGitHub @Vsevolod-GundorovOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
Мобильное веб-приложение для ведения силового цикла в фэнтези-сеттинге: герой, уровни, статы, вердикты «отработал / схалявил». Программа — Цикл V «Арена»: сплит Верх / Низ / Фулбоди-добор, 3 тренировки в неделю, 4 нагрузочные недели плюс разгрузочная. Каждая мышечная группа получает два активных дня в неделю по 2–3 упражнения за день, косвенной работы больше. Это адаптация профессиональных бодибилдерских сплитов (Кевин Леврон, Ронни Колеман, Ли Прист) под натурального атлета: специализация дня как у про, но объём на группу в восстанавливаемых рамках, а частота поднята с 1 до 2 раз.Read the full README ↗
Мобильное веб-приложение для ведения силового цикла в фэнтези-сеттинге: герой, уровни, статы, вердикты «отработал / схалявил».
Программа — Цикл V «Арена»: сплит Верх / Низ / Фулбоди-добор, 3 тренировки в неделю, 4 нагрузочные недели плюс разгрузочная. Каждая мышечная группа получает два активных дня в неделю по 2–3 упражнения за день, косвенной работы больше. Это адаптация профессиональных бодибилдерских сплитов (Кевин Леврон, Ронни Колеман, Ли Прист) под натурального атлета: специализация дня как у про, но объём на группу в восстанавливаемых рамках, а частота поднята с 1 до 2 раз.
Акценты недели всегда 2 + 1: недели 1 и 3 — Верх и Низ силовые, Фулбоди объёмный; недели 2 и 4 — наоборот. Так каждая группа каждую неделю видит и тяжёлую, и объёмную работу. Недели 3–4 идут на второй волне движений: вспомогательные заменяются родственными вариантами, а движение дня ног — на родственный вариант того же паттерна (присед со штангой → фронтальный присед).
Пятая неделя цикла — разгрузка: те же квесты, те же движения, то же число подходов, но вес −15%
(DELOAD в data/progression.js). Усталость копится быстрее, чем сила, и неделя на 85% возвращает свежесть
до того, как она превратится в застой — это не пропущенная неделя, а условие, при котором следующий блок
стартует сильнее. Такая неделя не двигает рабочий максимум ни вверх, ни вниз: движок помечает её в журнале
(plan.deload) и при чтении истории пропускает. Три разгрузочных квеста подряд не считаются застоем.
Гак-присед раньше попадал почти в каждый второй квест — теперь он остался в двух квестах нагрузочного блока,
а в ротацию добавлены фронтальный присед (движение дня ног на волне B), болгарские выпады и
плие-присед (приводящие в растянутой позиции — им в обычном приседе прямой работы не достаётся).
У каждого свой рабочий максимум: фронтальный стартует от приседа через коэффициент k = 0,82, дальше его
ведут собственные подходы.
У каждого движения в пуле есть цена cns: 3 — максимальная база (присед, становая, фронтальный присед),
2 — тяжёлое многосуставное со свободным весом, 1 — многосуставное на тренажёре, 0,5 — изоляция.
Из неё считается нагрузка квеста (sessionLoad) и действует жёсткое правило: одна максимальная база за сессию.
Присед и становая никогда не попадают в один день, после становой нет отдельной работы на поясницу,
а движения с cns 3 не уходят в многоповторку. Всё это проверяется тестами и не ломается пользовательскими заменами:
если поставить становую рядом с приседом вручную, квест покажет предупреждение о перегрузе.
Становая в цикле одна — отдельный силовой день на третьей неделе, где нет ни приседа, ни наклонов, а квадрицепс добирается тренажёрами. Нагрузка каждого квеста видна в его шапке и в списке квестов.
В квестах живут две сущности: суперсет (два движения подряд без отдыха, задаёт порядок) и дроп-сет (сброс веса на последнем подходе). Движок ставит дроп-сет не чаще одного раза за квест и только на изоляции. Остальные «приёмы интенсивности» из практики про-атлетов убраны: широкой аудитории они дают выбор там, где его не должно быть.
Одно приложение, две подачи — переключается в профиле, блок «Оформление».
| Сага (по умолчанию) | Чистая | |
|---|---|---|
| палитра | золото по чёрному, сине-зелёный туман | чёрный, тёмно-синий, голубой, белый |
| шрифт заголовков | Cormorant (антиква) | Manrope — тот же, что в тексте |
| иконки | game-icons.net (CC BY 3.0) | Lucide (ISC), 24×24, обводка 2px |
| фактуры | кожа, зерно, виньетка, гербовые рамки | нет, плоские поверхности |
| переход между экранами | заставка с руной | без заставки |
| слова | квесты, боссы, саги, баффы, знаки отличия | тренировки, добавки, достижения |
| названия тренировок | «Клинок Титанов» | «Верх тела · силовая» |
| достижения | «Рассветный Клинок» | «Утренняя тренировка» |
| добавки | «Масло Левиафана» | «Омега-3 (рыбий жир)» |
| профиль атлета | уровень, опыт, класс, характеристики | три числа: тренировок, поднято, недель подряд |
Трекер открывают, чтобы записать подход, а не читать. Поэтому во второй редакции
убрано всё, что объясняет очевидное, и ужат воздух. Замерено обходом экранов в браузере
(высота #app при ширине 414 px):
| экран | было | стало |
|---|---|---|
| профиль | 1765 px | 1139 px |
| добавки | 2263 px | 795 px |
| питание | 2207 px | 1587 px |
| прогресс | 1321 px | 771 px |
| тренировки | 677 px | 600 px |
Что именно убрано: уровень, опыт, класс и шкалы характеристик (вместо них три числа, которые атлету действительно нужны); подписи к разделам, дублирующие название вкладки; подсказка «как пользоваться» под каталогом; советы по таймингу питания; вердикт «ПОЧТИ НИЧЕГО» над шкалами; строка «осталось N» под каждой шкалой (её и так видно по полоске); повторяющееся «пока пусто» в каждой из четырёх кривых роста; ежедневные подсказки к добавкам; бейдж «волна A». Каталог добавок занимал 62% экрана — он справочник, и в «Чистой» спрятан под раскрытие. Отступы панелей 18 → 13 px, между панелями 20 → 10 px.
Достижения про уровень и характеристики в «Чистой» не показываются — их раздел скрыт (90 из 100 вместо 100). Сами они никуда не делись: начисляются по-прежнему и видны в «Саге», где этим понятиям есть место.
«Чистая» — не просто другая палитра: у неё своя редакция всех текстов, которые видит
пользователь. Сто достижений переписаны целиком (ACH_PLAIN в data/achievements.js):
те же условия и тот же прогресс, другое название и формулировка. «Триста Спартанцев» →
«300 повторений», «Гора Железа» → «8 тонн за тренировку», «Перекованный» → «+35% к
стартовым весам». Разделы списка тоже свои: «Путь героя» → «Путь», «Дисциплина» →
«Регулярность», «Герой» → «Форма», «Судьба» → «Бонусы».
Названия добавок в «Чистой» берутся из поля real, которое и так лежит в данных:
в каталоге стоит «Креатин моногидрат», а не «Эликсир Силы». Класс персонажа становится
профилем подготовки, который считается из тех же характеристик. Заметки в журнале
получений («тренировок: 42» вместо «квестов: 42») пишутся в редакции, которая была
включена в момент получения, и задним числом не переписываются — это история.
Переключение темы ничего не обнуляет: прогресс, достижения и журнал общие.
Тема — это три слоя, и живут они в data/theme.js: палитра и типографика
(css/theme-plain.css по атрибуту data-theme), набор иконок и словарь.
Остальное приложение про темы не знает.
css/styles.css остаётся темой «Сага» без изменений: все фэнтезийные цвета в нём
вынесены в токены-тройки (--t-acc, --t-sec, --t-bad, --t-good и т.п.), поэтому
вторая тема — это переопределение токенов плюс полсотни правил там, где фэнтезийность
зашита в сам рисунок: градиенты, тиснение, фактуры, геральдика. Все правила второй темы
заперты внутри :root[data-theme="plain"], и это проверяется тестом — иначе они утекут
в «Сагу». Перевод литералов в токены был сверен покомпонентно: 34 749 вычисленных
свойств по всем экранам, ноль расхождений.
Тема ставится до первой отрисовки крошечным скриптом в index.html: он читает
отдельный ключ localStorage, иначе при каждом запуске на долю секунды мигала бы не та
тема. Источник истины при этом — настройки журнала, скрипт лишь снимает мигание.
Выбран Lucide: ISC (можно коммерчески, без указания авторства в интерфейсе), 2100+ иконок на единой сетке 24×24 с обводкой 2px, один стиль без вариантов — то, что нужно, когда интерфейс должен выглядеть собранным. Рассматривались Phosphor (MIT, 9000+ иконок в шести начертаниях — избыточно), Tabler (MIT, 5900+, очень близкий аналог) и Heroicons (MIT, но всего ~300 иконок — не хватило бы на достижения).
Иконки не тянутся с CDN: tools/build-plain-icons.mjs вынимает нужные 118 из пакета
lucide-static и кладёт в data/icons-plain.js (27 КБ) — ровно тем же способом, каким
в проекте уже собраны остальные наборы. Семьдесят пять фэнтезийных медалей достижений
сведены к полутора десяткам предметных значков (кубок, медаль, мишень, календарь).
Анатомические иконки движений (data/icons-exercise.js) в обеих темах одни и те же:
они и так предметные, и это самая полезная графика в приложении.
Почти чёрный фон, тёмно-синие панели, ОДИН голубой акцент на всё приложение и белый текст — так сегодня выглядят и трекеры силовой (Hevy, Strong, Boostcamp), и продуктовые интерфейсы вроде Linear и Vercel. Общее у них одно: цвет работает указателем, а не украшением. Поэтому в «Чистой» голубым подсвечено только то, что сейчас важно — рабочий вес, активная вкладка, следующая тренировка; всё остальное живёт на оттенках серо-синего.
api/ + Postgres (Neon). Настройка — в SETUP.md, раздел «Сервер журнала»data/program.jsdata/exercises.jsdata/achievements.js; иконки достижений — data/icons-achievements.jsdata/theme.js (темы + словарь), css/theme-plain.css, data/icons-plain.jsКлиент (js/):
model/ — журнал, расчёты, синхронизация. Не знает про DOM и экраны; о событиях сообщает
подписками (onSaved, onAchievements, состояние сервера и облака);view/ — разметка: получает готовые данные, ничего не меняет и не вешает обработчиков;controller/ — экраны: берут данные из модели, отдают виду, обрабатывают действия;main.js — единственная точка входа, связывает модель с интерфейсом.Сервер (server/, маршруты — тонкие файлы в api/):
models/ — SQL-запросы, только с параметрами; views/ — что уходит клиенту;
controllers/ — порядок проверок и работа с журналом; security/ — подпись Telegram,
проверка тела, источник запроса; db/ — подключение, роль приложения, миграции.Границы слоёв закреплены тестом tests/architecture.test.mjs (клиент) и tests/server.test.mjs (сервер).
Панели «Сохранение» (экспорт/импорт JSON и ручные кнопки облака) и «Приватность» убраны с экрана: первая вернётся, когда доработаем резервные копии, вторая была справкой для разработчика. Код экспорта и импорта на месте, достижения «Хранитель Хроник» и «Возрождённые Хроники» ждут его возвращения. Перенос журнала текстовым кодом вырезан совсем — внутри Телеграма журнал и так уезжает в облако аккаунта.
npx serve .
# или python3 -m http.server 8000
С сервером журнала — на своём Postgres, тем же кодом, что на Vercel:
npm ci
DATABASE_URL=postgres://… DATABASE_DRIVER=pg BOT_TOKEN=… node tools/dev-server.mjs # ALLOWED_USERS=… — закрытая бета
node --test tests/*.mjs # без базы
TEST_PG_ADMIN_URL=postgres://postgres:…@localhost:5432/postgres npm run test:db # на живом Postgres
tests/server.test.mjs — сервер без базы: подпись Telegram (подмена, чужой токен, просрочка, дубли полей),
открытая регистрация и закрытая бета, проверка тела (загрязнение прототипа, NUL, битый Unicode, глубина, размеры), источник
запроса, и статические запреты: ни одного SQL из склейки строк, process.env только в настройках,
в журнал функций не пишутся ни данные, ни токены.tests/db/server.test.mjs — сервер на живом Postgres (в CI — отдельная задача с Postgres 16, в двух
режимах владельца базы): миграции и одновременный старт, права роли приложения, RLS (чужой журнал не
прочитать, не изменить и не подделать даже прямым SQL), инъекции, гонки записей, история версий,
атаки на API (подделка подписи, чужой сайт, неверные методы и типы, 3 МБ+ с лживым Content-Length,
не-UTF-8), лимиты частоты, жёсткая граница отдельного логина.tests/foods.test.mjs и tests/foods-model.test.mjs — продукт «на 100 г» (с порции, из кДж, по БЖУ),
клетчатка и вода, напитки по названию без ложных срабатываний, свои продукты, обычная порция, приёмы
пищи, бережный поиск в Open Food Facts (от 3 букв, по паузе, 8 в минуту, запоминание ответов).tests/db/foods.test.mjs — каталог на живом Postgres: поиск с опечатками, пополнение только с
перепроверкой в OFF по штрихкоду, спецсимволы LIKE и SQL в запросе, лимиты.tests/e2e/foods.mjs — вкладка питания в браузере: каталог, OFF по паузе, предохранитель,
экранирование названий, приём пищи, журнал с записью на сервере.tests/sync-server.test.mjs — решения синхронизации: перенос, новое устройство, конфликт, сравнение
журналов без учёта порядка ключей.tests/e2e/server-sync.mjs — сквозной прогон в браузере (не в CI): перенос журнала, второе устройство,
правка, конфликт, работа без сети, новый человек (сам заводится в базе; со списком — не пускается),
обычный браузер без Telegram.tests/program.test.mjs — периодизация (2+1 в неделю), разгрузочная неделя (один состав и то же число
подходов, что в нагрузочной), активная частота 2×/нед на группу, объём за неделю и за сессию, концентрация
по группам внутри сессии и накопленная работа к каждому движению, волны разнообразия, дозировка приёмов
интенсивности, парность суперсетов, сборка квеста, правки состава, расчёт рабочих весов, недельный тоннаж
и тренд, целостность пула и архива.tests/achievements.test.mjs — достижения: пороги, счётчики повторяемых, журнал получений, миграция.tests/theme.test.mjs — две темы: обе описаны полностью, в словаре ни один ключ не потерян
и нет лишних, у каждой реально отрисовываемой иконки есть штриховая пара, фрагменты SVG
валидны, стили второй темы не протекают в «Сагу». Отдельно — словарь запрещённых слов:
ни в одной строке «Чистой» и ни в одном из ста достижений не должно быть фэнтези.
Сравнение идёт по началу слова, иначе «превратится» ловится на «врат», а «отмеченных» —
на «меч». Там же: все сто достижений переписаны, лишних записей нет, названия не
повторяются, а исходная редакция не изменилась. Плюс: в «Чистой» скрыт ровно раздел
про уровень и характеристики, сами достижения из данных не пропали, а счётчик считает
по видимым.У цифровой клавиатуры iOS нет кнопки «готово»: закрыть её можно только тапом мимо поля. Поэтому пока поле в фокусе, над клавиатурой висит своя полоска «Готово», а тап по пустому месту снимает фокус сам. Полоска прячется не мгновенно: iOS дошлёт по тем же координатам клик, и если полоски уже нет, он прилетит по тому, что под ней.
Пока клавиатура открыта, нижние панели уезжают с экрана (body.kbd): и таб-бар, и плашка отдыха прибиты
к низу окна, а клавиатура этот «низ» сдвигает — на iOS они начинают болтаться посреди экрана. Заодно на
это время замирают отступы безопасных зон Телеграма: он шлёт viewportChanged с другими значениями, и
вёрстка прыгала прямо под пальцем. После закрытия клавиатуры всё возвращается, плюс короткий толчок
скролла — iOS иногда оставляет fixed-элементы там, где был сдвинутый вьюпорт.
Два места, где клавиатура раньше ломалась, и почему:
touchend и звала preventDefault() на любое второе касание
за 350 мс — быстрый тап просто не доходил до страницы, поле не теряло фокус, и клавиатуру нельзя было
ни закрыть, ни открыть заново. Двойной тап и так не зумит: за это отвечает touch-action: manipulation;Принцип — функциональная простота: на экране бейджи, объяснения под кнопкой «?».
квадрицепс 10): на какую группу приходится основной объём сессии.
Он же краснеет, если правка атлета пробила потолок в 10 подходов на группу. В раскрытой карточке движения
подписано, почему вес ниже ожидаемого: «квадрицепс к этому моменту уже отработал 7 подходов — вес на 10%
ниже, чем на свежую мышцу».58,8 т), сколько это рабочих подходов и процент к прошлой непустой неделе (▲ +11% к неделе 1).
Это единственное число, в котором «я себя перегружаю» видно раньше, чем это почувствуют колени: план
показывает вес в подходе, а не сумму за неделю. Разминка не в счёт, повторный заход в тот же квест
заменяет прошлый — считается неделя, а не количество попыток (weekTonnage / tonnageTrend).102,5 кг × 4–6 повт), счётчик закрытых подходов и точки.
Раскрытая начинается с блока цели: слева крупно вес и повторы, справа мелко пол движения и куда поехал
вес (▲ +2,5 кг), ниже свёрнутая строка «прошлый раз» (раскрывается по тапу: в зале она нужна не каждый
подход). Заголовка у блока нет — и так понятно, что делать. Ярлыки RPE/RIR с экрана убраны: широкой
аудитории они ничего не говорят, а движку прогрессии запас по-прежнему нужен и живёт в данных.
Закрытое движение гаснет, чтобы взгляд цеплялся за незакрытые.data/exercises.js — справочник движений: мышечная группа, паттерн, снаряд, уровень (для таймера отдыха),
описание, техника и коэффициент k для оценки 1ПМ от базового лифта.
Вес в подходах не зашит в программу и не берётся из рекордов — его ведёт журнал (data/progression.js).
Правило одно, двойная прогрессия по каждому движению:
Разминочные подходы (легче 80% рабочего веса) движок отсекает сам — и в разборе сессии, и в счёте квеста,
и в карточке движения. Правило одно, isWarmup() в data/progression.js.
Всё раскрывается из одного числа — рабочего максимума движения. Из него считается рабочий вес на сегодня: сколько повесить в этой схеме. Его квест и подставляет в подходы, поэтому в зале считать нечего. Рядом идёт пол — 90% рабочего веса: ниже него подход перестаёт быть рабочим. Это та же граница, за которую движок не пускает рабочий максимум при серии неудач, и по ней же считается интенсивность квеста.
Схемы цикла чередуются (силовая 4–6, объёмная 8–10, добивающая 15–20), поэтому рабочий максимум хранится в пересчёте на максимум по одной кривой «проценты от 1ПМ» со скидкой на запас (RIR). Прибавка, взятая на объёмной неделе, не теряется на силовой, а подход на 20 повторов не получает вес десятиповторного.
Объём и отдых входят в расчёт. Один подход на десять повторов и четыре таких подхода по полторы минуты
отдыха — это разные веса, хотя повторы в плане одни и те же: к последнему подходу копится усталость.
Поправка (loadFactor): каждый подход после первого стоит 2%, каждые полминуты недоотдыха до 2,5 минут —
ещё 1,5%, глубже 18% не идём. Работает в обе стороны: когда вес назначается — вниз, когда журнал читается —
вверх, иначе один и тот же квест занижал бы сам себя с каждым разом. Отдых берётся из общего справочника
(restFor): тяжёлой базе от четырёх минут, многосуставному от двух, изоляции минута-полторы — те же числа
показывает таймер отдыха, чтобы план и расчёт не разъезжались.
Из-за этого одно движение получает разный вес в разных квестах: жим с рабочими 102,5 кг на силовой неделе (4×4–6) на объёмной (4×8–10) даёт 90 кг, а не те же 102,5.
Накопленная за тренировку работа входит в расчёт. loadFactor считает усталость внутри упражнения,
а между упражнениями её считает freshFactor: разгибания ног третьим движением идут не на свежий квадрицепс —
к ним он уже отработал семь подходов приседа и жима ногами. Каждый сделанный до движения рабочий подход по той
же группе снимает 1,5%, вторичная работа (ягодицы в приседе, трицепс в жиме) считается вполовину, глубже 15%
не идём. Движение дня всегда первое и всегда получает вес как на свежую мышцу.
Поправка симметрична: при назначении веса делит, при чтении журнала умножает обратно, а накопленную работу движок восстанавливает из снимка состава сессии — поэтому и сессии, записанные до появления поправки, читаются по тем же правилам, и при неизменном составе квеста вес не меняется. Двигается он ровно тогда, когда меняется состав или порядок: уберёшь жим ногами — разгибания подрастут с 35 до 40 кг, добавишь ещё одно движение на квадрицепс — соседи просядут. Это же правило закрывает старую дыру: первый заход в подсобку раньше считался от свежего максимума (жим ногами третьим в квесте получал 185 кг вместо 170).
В шапке квеста стоит бейдж концентрации (квадрицепс 10) — на какую группу приходится основной объём сессии.
Потолок SESSION_CAP = 10 подходов на группу за сессию: выше добавочный подход уже не растит, а только
отнимает восстановление (Baz-Valle 2022). Шаблоны в потолок укладываются, а если его пробивает правка атлета,
бейдж краснеет и в разборе квеста написано, что убрать. Полная раскладка по группам — там же, под «?».
Самочувствие входит в расчёт. Перед квестом атлет отмечает состояние — это единственное, чего движок не может вывести из журнала: сон, стресс и болезнь в подходы не записываются.
| состояние | вес | подходы | отдых |
|---|---|---|---|
| Свежий | +2% | как в плане | ×0,85 |
| Норма | план как есть | как в плане | как в плане |
| Устал | −5% | на один меньше (не ниже двух) | ×1,25 |
Поправка симметрична, как и поправка на объём: движок помнит, в каком состоянии закрыт квест (sessions[].feel),
и при чтении истории делит её обратно — тяжёлый день не читается как откат силовых. Состояние живёт
в журнале и сбрасывается на «норму» на следующий день: вчерашняя разбитость не должна молча резать сегодняшний
квест. На том же жиме 102,5 кг силовая схема даёт 105 кг свежему и 100 кг разбитому — при трёх подходах вместо четырёх.
Личный максимум (расчётный 1ПМ, среднее формул Эпли и Бжицки с капом на 10 повторах) живёт отдельным счётчиком в карточке движения и в «Хрониках». В подборе веса он не участвует: рекорд одного удачного дня не должен задирать рабочую неделю. Рекордный сингл в конце квеста идёт в этот счётчик, но рабочий вес не роняет.
Вес героя меняется в профиле по тапу: от него считаются подтягивания, брусья и гиперэкстензия — там рабочий вес это довесок к своему, и устаревшее число врёт в каждом подходе.
Поправить рабочий вес руками можно в карточке движения: кнопки −10% / −5% / +5% и «↺ расчёт».
Правка запоминается датой (S.workReset), и с этого дня отсчёт начинается заново: всё, что было раньше,
в расчёт не идёт, а дальше вес снова ведут подходы. Это для возвращения после болезни или перерыва —
догонять шагами по 2,5 кг оттуда, где ты уже не стоишь, значит потерять месяц.
Пока по движению нет подходов, вес стартует от базовых лифтов через коэффициент k (◎) с плановой
надбавкой недели; как только появляются подходы, вес двигают они (★), а надбавка из шаблона отключается.
Для гантелей вес делится на руку, для подтягиваний и брусьев выдаётся довеском к своему весу — считается
всё по полной системе, а показывается то, что реально вешаешь.
«Хроники прокачки» показывают по каждому движению рабочий вес на следующий раз, личный максимум, тренд в %/месяц и состояние: рост, держим, застой (3 квеста без прибавки — делоад или смена движения) или откат.
В любом квесте движение можно заменить (подбираются похожие по паттерну и группе), добавить из арсенала
или убрать — правки хранятся в S.plan[wid] и не трогают шаблон. Схема подходов наследуется от роли слота,
поэтому замена движения дня остаётся движением дня. Подстраница «Арсенал движений» в разделе квестов
показывает весь пул с рабочими весами атлета, разбор техники и недельное покрытие мышц.
Ключевые решения опираются на мета-анализы и RCT 2020–2026:
100 уникальных достижений в шести рангах: бронза → серебро → золото → алмаз → изумруд → титан.
Уникальные (вехи, клубы силы, серии) выдаются один раз; повторяемые (рекорд, безупречный квест, тоннаж за квест,
дары судьбы) при каждом новом получении увеличивают счётчик ×N. У каждого знака есть журнал получений:
дата и причина (тоннаж и квест, рекорд с килограммами, время, серия) — виден в раскрытии знака. Правила — чистые функции над контекстом
(buildAchievementCtx в js/app.js), список и оценка — data/achievements.js.
Иконки — game-icons.net (CC BY 3.0); новые вытаскиваются скриптом tools/extract-icons.py из локального клона библиотеки.
data/icons-plain.js) — Lucide, ISC
(© Lucide Contributors; набор вырос из Feather, © 2013–2023 Cole Bemis).
Файл собирается скриптом tools/build-plain-icons.mjs из пакета lucide-static — там же
таблица «какое имя в приложении какой иконке Lucide соответствует». Руками файл не правится.
Текст лицензии — data/icons-plain-LICENSE.txt.data/bodymap.js собирается скриптом tools/build-bodymap.mjs: он берёт модель, раскачивает её
до телосложения атлета (ширина по профилю высоты, руки переносятся отдельно) и раскладывает части
модели по нашим группам мышц. Руками файл не правится. Текст лицензии — data/bodymap-LICENSE.txt.
Служебные значки снарядов и приёмов (data/icons-ui.js) нарисованы для проекта.data/icons-exercise.js) — из двух открытых наборов:
game-icons.net (CC BY 3.0; авторы Lorc, Delapouite, sbed и др.) и
healthicons.org (MIT). Файл собирается скриптом
tools/build-exercise-icons.mjs — там же таблица «какой значок какому движению» и правило раскладки:
многосуставное движение узнаётся по самому движению, изоляция — по мышце, которую грузит.
Руками файл не правится. Текст лицензий — data/icons-LICENSE.txt.Приложение работает и как обычная веб-страница, и как мини-приложение Telegram: внутри Телеграма подхватываются системная кнопка «Назад», тактильная отдача, безопасные зоны, цвет шапки и облако.
Что делает код (js/telegram.js):
ready() и expand() на старте, блокировка свайпа вниз, иначе жест закрывает приложение прямо во время подхода;HapticFeedback, потому что navigator.vibrate в Телеграме на iOS не работает;safeAreaInset и contentSafeAreaInset пробрасываются в CSS-переменные --tg-top и --tg-bottom;CloudStorage уходит резервная копия:
состояние режется на части по 3800 символов, мета-запись пишется последней, хвост прошлого
сохранения подчищается. 22 КБ журнала это 7 частей.Вне Телеграма ничего из этого не включается: панели облака нет, навигация работает своими кнопками.
Если сервер настроен, главная копия журнала — в Postgres. Регистрация открытая: при первом запуске
человек заводится в базе сам (с ALLOWED_USERS — только те, кто в списке):
initData, HMAC токеном бота); id из тела
запроса на веру не берётся;migrate;rev журнала и serverRev, от которой он считан. Новое устройство забирает журнал
молча, правки с двух устройств — вопрос, какую версию оставить, вторая остаётся резервной копией;SETUP.md;Защита на сервере:
bu_app без права обходить RLS, на таблицах FORCE ROW LEVEL SECURITY: база сама
отдаёт каждому только его строки, даже при ошибке в коде. С DATABASE_URL_APP — отдельный логин,
и граница жёсткая даже для произвольного SQL;no-store, ошибки — только код, без подробностей;server/, node_modules/, package*.json снаружи не отдаются; тесты и утилиты на Vercel не уезжают.Без сервера приложение тоже мультипользовательское:
bodyupgrade.v1.u<id>), поэтому на одном
телефоне могут заходить несколько аккаунтов и прогресс не смешивается;CloudStorage изолирован связкой «бот + пользователь», так что облако у каждого своё;rev, у синхронизации — syncedRev. Направление выбирает чистая функция
decideSync: заливать, тянуть, ничего не делать или спросить пользователя, если журнал менялся
и на устройстве, и в облаке;Открыл приложение с другого телефона под тем же аккаунтом — журнал подтянется сам.
/newbot, задать имя и username, получить токен./newapp → выбрать бота → название, короткое описание, иконка 640×360, ссылка на продакшн (адрес Vercel) →
короткое имя приложения. Появится прямая ссылка вида t.me/<bot>/<app>./mybots → бот → Bot Settings → Menu Button → задать URL приложения, чтобы кнопка меню в чате открывала его./setdescription, /setabouttext, /setuserpic.Шаги 3 и 4 можно не делать руками — есть скрипт, токен в репозиторий не попадает:
BOT_TOKEN=123456:AA... APP_URL=https://твой-проект.vercel.app ./tools/setup-bot.sh
Он ставит кнопку меню на мини-приложение, имя, описания и команды бота. Регистрацию самого
мини-приложения (/newapp) Bot API не умеет, её проходят в BotFather один раз.
Проверять удобно в Telegram Desktop: правый клик по мини-приложению → «Inspect» открывает devtools.
Ничего секретного в репозитории нет и быть не должно.
.env закрыт в .gitignore, в репозитории лежит только .env.example с пустыми значениями;.env, а токен вырезает из любого вывода, чтобы он не попал в логи;.githooks/pre-commit отменяет коммит, если в изменениях виден токен или файл .env.
Включается один раз: git config core.hooksPath .githooks;./tools/check-secrets.sh --history проверяет рабочее дерево и историю коммитов;tests/security.test.mjs.Если токен всё же где-то засветился — сразу /revoke в BotFather, старый перестанет работать.
Журнал лежит на устройстве, в облаке Telegram (доступно только самому пользователю) и — для на сервере приложения, в базе, где каждая строка закрыта политикой RLS.
| Адрес | Зачем | Что уходит |
|---|---|---|
свой /api/journal | сервер журнала | журнал и подпись Telegram |
свой /api/foods | общий каталог продуктов | поисковая строка; штрихкод выбранного продукта Open Food Facts |
| telegram.org | SDK мини-приложения | ничего, обычная загрузка скрипта |
| world.openfoodfacts.org | поиск продуктов | только введённое слово, и только если поиск включён |
Больше приложение никуда не ходит:
assets/fonts, поэтому Google не видит пользователей (обновить: tools/fetch-fonts.sh);initData уходит только на свой /api/
(без cookies и перенаправлений) — это закреплено тестом;vercel.json задана политика безопасности: браузер заблокирует обращение к любому адресу,
которого нет в списке, а также запрещены камера, микрофон, геолокация и передача реферера.Вкладка устроена как в популярных дневниках питания:
Откуда продукты, от быстрого к медленному:
/api/foods): продукты Open Food Facts, которые кто-то уже ел.
Пополняется сам: выбрали продукт OFF — сервер сам берёт его из OFF по штрихкоду, считает на
100 г и проверяет. Значениям с телефона сервер не верит, иначе любой мог бы испортить каталог;Каждый продукт сводится к одному формату (per100 в data/nutrition.js): значения на 100 г (мл),
клетчатка, признак напитка и индекс гидратации — от них считаются шкала клетчатки и вода.
Данные Open Food Facts — по лицензии ODbL, подпись в выдаче.
Пуш в main → Vercel собирает автоматически (Framework Preset: Other, без build command, output = корень).
Функции api/ — Node 22, регион fra1 (рядом с базой). Секреты — только в переменных окружения Vercel.
Счёт = 65% покрытие плановых подходов + 35% выход на свой рабочий вес (движение дня весит ×2).
XP → уровень героя. Статы: СИЛА (рост расчётных 1ПМ от базы 147/170/195/100), ВЫНОСЛИВОСТЬ (средний тоннаж), ДИСЦИПЛИНА (частота за 14 дней), СТОЙКОСТЬ (средний счёт).
tests/progression.test.mjs — прогрессия рабочих весов: шаг вверх и вниз, застой, лёгкий день, гантели
на руку и довесок к своему весу, перенос прибавки между силовой и объёмной схемой, личный максимум отдельно,
поправки на объём/отдых, самочувствие и накопленную за тренировку работу (все в обе стороны), разгрузка
не двигает максимум и не считается застоем, тоннаж без разминки.plan.pair / plan.unpair) и не трогают шаблон. Пересчёт идёт сам:
в паре на разные группы мышца отдыхает, пока работает партнёр, — отдыха даже больше обычного; пара на
одну группу это двойной подход без отдыха, и рабочий вес под неё падает. Об этом сказано и в списке
кандидатов, и в шапке собранной пары.