PunchCross River debt dropped by N70bn under my watch — OtuDaily MaverickGBVF RESPONSE: Ekurhuleni murders — Lesufi and SAPS pledge to keep women safe, outlining security measuresThe Jerusalem PostWhat happened when Isaac Herzog asked 150 Jews to solve their people’s problemsZDF heuteEntdecken Sie das ZDF-NachrichtenstudioRTL BoulevardMarc-Marie Huijbregts heeft kerstboom al staan, maar houdt gordijnen dicht uit schaamten-tv"Müssen harte Strafen folgen": Dobrindt macht zwei Gruppen für Randale verantwortlichSeeking AlphaS&P 500 earnings growth to slow as AI spending loses momentum, Goldman Sachs saysالشرق"رسالة الموت".. ثغرة أمنية تهدد بتعطيل هواتف أندرويد دون تدخل المستخدم20 MinutenPorsche in Krawallnacht demoliert: Schaden bis 30’000 FrankenWirtualna PolskaPonad 3,5 promila za kółkiem. Wjechał w znak na oczach świadkówEvening StandardLiverpool XI vs Arsenal: Predicted lineup, confirmed team news, injury latest for Premier League todayAntara NewsIndonesia's 2045 vision must ensure justice and dignity for all
The Daily Newsstand · Free, Always
Sunday, November 1, 2026

Цифровой аватар спортсмена: как я собрал работающий прототип на 3000 строк ванильного JS

Translate

Есть жанр проектов, где самое интересное — не то, что сделано, а то, что сознательно не сделано. Этот как раз такой.

Речь о системе «Цифровой аватар спортсмена» (ЦАС): верифицированный профиль спортсмена, методическое ядро тренера, аренда методик с роялти, кабинеты спонсора и федерации. Звучит как очередная «экосистема», и на этапе презентации обычно всё заканчивается слайдами. Мне было интересно другое: собрать работающий прототип, в котором каждое заявленное утверждение можно проверить двумя кликами, а не поверить на слово.

Ограничения были заданы заранее и жёстко:

  • ни сервера, ни базы данных, ни сборки — статические файлы;

  • никаких внешних запросов и CDN, всё локально;

  • персональных данных нет вообще, все данные синтетические;

  • юридическая рамка первична: сначала выясняем, что делать нельзя, и только потом проектируем, как сделать полезное.

Получилось 15 экранов, 14 функциональных модулей, 4 демонстрационные роли, 20 файлов и около 370 КБ без единой зависимости. Ниже — инженерная часть.

1. Три плоскости данных вместо одного «профиля»

Главная ошибка в таких системах — одна таблица «спортсмен» со всеми полями, к которой потом аккуратно раздают доступы. Проблема в том, что «аккуратно» не работает: рано или поздно тренер видит медицинские отметки, а спонсор — паспортные данные.

Здесь данные разделены на три плоскости с разными правилами:

Плоскость

Что внутри

Кто видит

P1. Витрина

псевдоним, регион, дисциплина, разряд, результаты турниров

все, включая публичный каталог

P2. Методическое ядро

методики тренера, деревья решений, версии

автор, арендаторы по договору, наследник по завещанию

P3. Закрытое хранилище

сон, ЧСС, признаки усталости, заметки тренера

сам спортсмен, законный представитель, тренер — только при живом согласии

Ключевое: P3 недоступна не потому, что в интерфейсе спрятана кнопка, а потому что проверка зашита в модель прав. Арендатор методики физически не может получить P3 — у него нет основания обработки, а не «нет кнопки».

function canAccessPlane3(state, actorId, subjectAvatarId) {
  var actor = find(state.accounts, actorId);
  var avatar = find(state.avatars, subjectAvatarId);
  if (!actor || !avatar) return { allow: false, reason: 'Объект не найден' };
  if (avatar.ownerId === actorId) return { allow: true, reason: 'Владелец аватара' };

  var link = find(state.accounts, actorId);
  if (link && link.wardId && avatar.ownerId === link.wardId)
    return { allow: true, reason: 'Законный представитель' };

  if (actor.role === 'coach') {
    var consent = state.consents.some(function (c) {
      return c.accountId === avatar.ownerId && c.type === 'coach' && !c.revokedAt;
    });
    return consent
      ? { allow: true,  reason: 'Согласие спортсмена на передачу тренировочных данных' }
      : { allow: false, reason: 'Согласие отозвано или отсутствует' };
  }

  if (actor.role === 'federation')
    return { allow: false, reason: 'Федерация видит только паспортные и соревновательные данные' };

  return { allow: false, reason: 'Нет основания обработки' };
}

Отзыв согласия — это не «пометка в базе», а немедленное изменение результата функции. В демо это проверяется в один клик: отзываем согласие на передачу данных тренеру — и следующий запрос тренера получает отказ с причиной. Причина показывается человеку, а не только пишется в лог: «403. Нет основания обработки» понятнее, чем пустой экран.

экран «Приватность», отзыв согласия и проверка доступа

экран «Приватность», отзыв согласия и проверка доступа

2. Экономика методик: каскад роялти, который сходится до копейки

Вторая часть, где обычно начинается магия на словах. Методика тренера — это объект, который можно арендовать, и у которого есть «родословная»: если я взял чужую методику и адаптировал под свою дисциплину, мой вариант помнит, откуда вырос.

При каждом платеже сумма делится на три части: комиссия платформы, роялти по цепочке наследования и остаток автору.

Правила простые:

  • комиссия — 7 % от суммы договора;

  • роялти первой линии — 15 % от пула (пул = сумма минус комиссия);

  • второй линии — 10 %, третьей — 5 %;

  • суммарный потолок роялти — 30 % пула;

  • остальное — автору блока.

function calcSplit(state, blockId, priceRub, commissionPct) {
  var base = C.round2(priceRub);
  var commission = C.round2(base * (commissionPct == null ? 7 : commissionPct) / 100);
  var pool = C.round2(base - commission);

  var chain = lineageChain(state, blockId);   // от блока вверх по derivedFrom
  var royalties = [], used = 0;

  chain.forEach(function (item, i) {
    var percent = RULES.cascade[i];           // [15, 10, 5]
    if (percent == null) return;
    if (used + percent > RULES.cascadeCap) return;   // потолок 30 %
    used += percent;
    royalties.push({
      level: item.level,
      blockId: item.block.id,
      beneficiaryAvatarId: item.block.ownerAvatarId,
      percent: percent,
      amount: C.round2(pool * percent / 100)
    });
  });

  var ownerShare = C.round2(pool - royalties.reduce(function (s, r) { return s + r.amount; }, 0));
  var check = C.round2(commission + royalties.reduce(function (s, r) { return s + r.amount; }, 0) + ownerShare);

  return { base: base, commission: commission, pool: pool, royalties: royalties,
           ownerShare: ownerShare, balanced: check === base, check: check };
}

Реальный расчёт из демо для блока третьего уровня (цена 1 100 ₽ за месяц):

Строка

Сумма

Цена договора

1 100,00 ₽

Комиссия платформы, 7 %

−77,00 ₽

Пул к распределению

1 023,00 ₽

Роялти 1-й линии, 15 % (автор родительского блока)

153,45 ₽

Роялти 2-й линии, 10 % (автор корневого блока)

102,30 ₽

Автору блока

767,25 ₽

Сходимость

1 100,00 ₽ — до копейки

граф методик и расчёт распределения

граф методик и расчёт распределения

Что здесь важнее самой арифметики — инвариант balanced. Каждый расчёт возвращает флаг сходимости, и интерфейс его показывает. Если завтра кто-то поменяет правило или добавит четвёртую линию, расхождение вылезет сразу, а не через полгода в бухгалтерии.

Отдельно про деньги: внутри системы нет «валюты». Есть неконвертируемые баллы (за вклад в сообщество, ими нельзя торговать и нельзя вывести) и рубли по договору — выплаты идут через платёжного партнёра, с НДФЛ. Баллы и деньги разведены на уровне модели, а не на уровне подписи в интерфейсе: одна функция не может вернуть баллы там, где ожидаются рубли.

3. Журнал операций: хеш-цепочка, и почему это не блокчейн

Все значимые действия — оплата договора, начисление роялти, выдача рекомендации, отзыв согласия, верификация — пишутся в append-only журнал. Каждая запись содержит хеш предыдущей:

function ledgerAppend(state, type, payload) {
  var prev = state.ledger.length ? state.ledger[state.ledger.length - 1].hash : '0'.repeat(64);
  var seq  = state.ledger.length + 1;
  var body = { seq: seq, type: type, at: new Date().toISOString(), payload: payload, prevHash: prev };
  body.hash = C.sha256(JSON.stringify(body));
  state.ledger.push(body);
  return body;
}

function ledgerVerify(state) {
  var prev = '0'.repeat(64);
  for (var i = 0; i < state.ledger.length; i++) {
    var e = state.ledger[i];
    if (e.prevHash !== prev) return { ok: false, brokenAt: e.seq };
    var body = { seq: e.seq, type: e.type, at: e.at, payload: e.payload, prevHash: e.prevHash };
    if (C.sha256(JSON.stringify(body)) !== e.hash) return { ok: false, brokenAt: e.seq };
    prev = e.hash;
  }
  return { ok: true, count: state.ledger.length, brokenAt: null };
}

Почему не блокчейн: для задачи «доказать, что журнал не переписывали» достаточно хеш-цепочки, а любой распределённый реестр добавляет стоимость, юридическую неопределённость и вопросы к хранению персональных данных. Я сознательно не использую это слово в интерфейсе — там написано «журнал с хеш-цепочкой».

Честная оговорка: хеш-цепочка защищает от правки отдельной записи, но не от подмены всего журнала целиком. Настоящая гарантия появляется, когда хеш головы журнала периодически «якорится» во внешней системе (нотариальное депонирование, независимый сервис меток времени). В прототипе этого шага нет, и в интерфейсе он помечен как отложенный — это принципиально: лучше показать дырку, чем сделать вид, что её нет.

Ещё одна деталь: SHA-256 реализован на чистом JS, без crypto.subtle. Причина простая — при открытии index.html двойным щелчком (протокол file:) crypto.subtle недоступен, а стенд должен работать и так. Проверено на векторах RFC: sha256("abc") = ba7816bf…f20015ad, sha256("") = e3b0c442…7852b855.

доходы, расчёт и проверка журнала

доходы, расчёт и проверка журнала

4. ИИ без нейросети: правила, факторы и человек в контуре

Ещё одно место, где можно было «прикрутить LLM» и получить красивую демонстрацию с непредсказуемым поведением. Вместо этого — движок на правилах и статистике, который объясняет каждый вывод списком факторов с весами.

Логика: считаем признаки накопленной усталости (тренд нагрузки, дефицит сна, рост времени реакции), сравниваем с порогами, формируем рекомендацию. У каждой рекомендации есть:

  • список факторов с числами — «сон 5,8 ч против 7,2 ч на прошлой неделе»;

  • уверенность и порог, выше которого решение уходит тренеру;

  • версия модели и запись в журнал — что именно было предложено и как отреагировал человек.

Это HITL (human-in-the-loop) не как модное слово, а как архитектурное решение: при высоком уровне сигнала система не меняет план тренировок, а создаёт задачу тренеру.

Что ИИ делает

Что ИИ не делает никогда

считает признаки усталости по нагрузке, сну и реакции

не ставит диагнозы и не назначает лечение

предлагает микроцикл из существующих блоков методики

не делает выводов о допинге

объясняет каждый вывод факторами с весами

не даёт финансовых и инвестиционных советов

передаёт спорные решения тренеру

не прогнозирует доход и не оценивает «профпригодность»

ИИ-коучинг с факторами и подтверждением тренером

ИИ-коучинг с факторами и подтверждением тренером

5. Самое интересное: семь механик, которые пришлось заменить по закону

Исходная концепция была написана «как продукт» — без оглядки на 152-ФЗ, 323-ФЗ и здравый смысл в части персональных данных несовершеннолетних. Задача стояла не «сделать юридическое заключение», а переписать механику так, чтобы она осталась полезной, но перестала быть противоправной.

Что изменилось (в интерфейсе есть отдельный экран, где это показано таблицей — 11 учтённых норм и 7 замен):

Было в концепции

Стало в системе

Причина

биометрический контроль состояния и «антидопинговые выводы»

импорт официального статуса РУСАДА; система не делает собственных выводов

биометрия — спецкатегория ПД, выводы о допинге — компетенция аккредитованных организаций

внутренняя конвертируемая валюта спортсмена

неконвертируемые баллы + рубли по договору через платёжного партнёра

нельзя создавать денежный суррогат

собственная «касса» и выплаты внутри системы

выплаты через НПО (платёжного партнёра), с налоговой отчётностью

расчёты — лицензируемая деятельность

наследование аккаунта с персональными данными

наследование методики и права на роялти, ПД не передаются

ПД умершего не наследуются в общем порядке

«инвестиции в спортсмена» с доходностью

лицензионные договоры и дарение

инвестиционный продукт — иная регуляторная рамка

трансграничная передача данных

обезличенная методика без ПД

152-ФЗ, требования к локализации

ИИ-советы по финансам

только аналитика и отчётность

финансовые рекомендации — регулируемая деятельность

Отдельно про несовершеннолетних: для профиля 16-летнего спортсмена согласия даёт законный представитель, экономика юниора скрыта, публичная витрина не содержит ни почты, ни точной даты рождения.

Именно эта часть, а не красивый дашборд, оказалась главной в проекте. Когда показываешь чиновнику систему, где заранее вырезаны все спорные механики и на каждое ограничение есть ответ «почему именно так», разговор идёт совсем иначе.

экран «Правовой контур»

экран «Правовой контур»

6. Технические решения и почему не React

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

  • Ванильный JS, ES5-совместимый синтаксис. Никакой сборки: открыл index.html — работает. Не нужен Node, не нужен npm install за пять минут до презентации.

  • Один localStorage как хранилище состояния. Есть кнопка «Сбросить демо» — состояние возвращается к исходному, включая журнал.

  • Ноль внешних запросов. Шрифт PT Sans лежит четырьмя .woff2-файлами рядом с кодом, favicon — SVG. При проверке в DevTools вкладка Network показывает только запросы к своему же домену.

  • Модули разбиты по доменам, а не по типам файлов: screens-sport.js, screens-method.js, screens-admin.js. Для прототипа на три тысячи строк этого достаточно, и навигация по коду остаётся предсказуемой.

  • Экранирование всего пользовательского ввода. Есть функция esc() и правило: любые данные, попавшие в состояние (название блока, ФИО, комментарий), проходят через неё при рендере. Проверял внедрением <img src=x> в название методики — не срабатывает.

Чего я сознательно не делал: не тащил фреймворк ради «правильной архитектуры». Для демонстрационного контура, который живёт без бэкенда, React добавил бы 150 КБ, сборку и цепочку инструментов — и всё это ровно для того, чтобы получилось то же самое.

мобильный вид

мобильный вид

7. Что здесь не работает (и это важно)

Честный список ограничений — потому что прототип, у которого нет ограничений, обычно просто не запускали.

  1. Нет бэкенда и базы. Всё состояние — в браузере посетителя. Два человека не увидят одни и те же данные.

  2. Нет реальных интеграций. ЕСИА, ГИС ФКиС, ЕКП, носимые устройства, ЕБС — не подключены. В интерфейсе они помечены как имитация, а не как «интеграция в разработке».

  3. Платежи и выплаты имитируются. Никаких платёжных шлюзов, только запись в журнал и демонстрационный номер платежа.

  4. k-анонимность для спонсорских отчётов работает на синтетике. Порог k ≥ 10 реализован, но на реальных малых данных потребуется отдельная политика подавления.

  5. Хеш-цепочка без внешнего якоря не защищает от подмены журнала целиком (см. выше).

  6. Нет многопользовательской работы и ролей на сервере. Переключатель ролей — демонстрационный: он меняет точку зрения, а не права доступа.

  7. Нет медицинской валидации. Признаки усталости — правило на трёх метриках, а не клинически проверенная модель.

Все эти пункты выписаны и в README, и в интерфейсе на экране «О системе». Когда ограничения заявлены самим автором, их невозможно предъявить как обман.

8. Что получилось в цифрах

  • 15 экранов, 14 модулей, 4 демонстрационные роли;

  • 20 файлов, около 370 КБ, 0 зависимостей, 0 внешних запросов;

  • 60 комбинаций «экран × роль» — 0 ошибок JS;

  • ширины от 320 px до 2 560 px — горизонтальной прокрутки нет;

  • расчёты роялти сходятся до копейки на всех проверенных сценариях.

Ссылки

  • Демо: https://mabus46.github.io/cas-mvp/

  • Исходники: https://github.com/Mabus46/cas-mvp

  • Лицензия: проприетарная, все права принадлежат автору; условия — в файле LICENSE в репозитории. Демонстрационное ознакомление и цитирование со ссылкой разрешены.

Если интересно, в следующей статье разберу подробнее граф методик: как хранится «родословная» блока, как считается глубина наследования и почему каскад роялти ограничен потолком в 30 % — там больше математики, чем кажется.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.