Compare commits

...

955 Commits

Author SHA1 Message Date
coopops 5e58c173ee chore(release): publish
Build bootstrap container / build (push) Successful in 3m45s
Release / publish-packages (push) Successful in 29m23s
Release / release (push) Successful in 29m7s
Release / trigger-mono-docs (push) Successful in 2s
Release / trigger-coopenomics-docs (push) Successful in 2s
2026-07-21 20:19:26 +00:00
coopops c1ce7a095d [989-10][@ant] fix: убрать поле СФР и сделать поля организации/подписанта в форме нулёвых отчётов read-only
Поле «Регистрационный номер СФР» в форме ЕФС-1 не использовалось генератором вовсе (в документ и в имя файла идёт только pfrRegNumber) — убрано вместе с ложной проверкой в fss4.generator.ts, требовавшей его заполнения.

Остальные поля секций «Организация»/«Подписант» в ZeroReportEditor.vue (ФИО, СНИЛС, ИНН/КПП/ОКВЭД/ОГРН/ОКТМО, рег. номер ПФР, должность, тип подписанта) всегда приходят из Реквизитов/БД кооператива и правка через этот черновик никуда не сохранялась — редактируемые поля создавали иллюзию, что правка что-то меняет. Переведены в read-only (readonly+disable, как уже сделано для орг-полей в SettingsPage) — правка доступна только на странице Реквизитов. Раздел «Отчётный год/период/корректировка» остался редактируемым — он не приходит из Реквизитов, это per-документ данные.
2026-07-21 20:17:24 +00:00
coopops 995f65f524 [989-10][@ant] fix: убрать лишний поясняющий хинт у поля «Рег. номер ПФР» — по замечанию пользователя ни у одного другого поля в реквизитах/форме ЕФС-1 такого хинта нет, заполняющий и так знает, что это за номер 2026-07-21 19:12:09 +00:00
coopops 5298e55a7b [989-10][@ant] fix: ЕФС-1 (4-ФСС) — использовать рег. номер ПФР, а не СФР, в имени файла и <ЕФС8:РегНомер>
Сторонние бухгалтерские системы (СБИС и др.) при приёме ЕФС-1 отклоняли выгруженный XML: "Рег. номер отправителя из наименования файла не соответствует ни рег. номеру ПФР организации, ни рег. номеру СФР". Причина в двух местах:
1) generateGenericFileName для FSS4 хардкодил сегмент отправителя как "0000000000" — реальный номер туда не попадал вообще (тот же класс бага, что чинили в idFile для нулёвок ранее).
2) fss4.generator.ts писал в <ЕФС8:РегНомер> sfrRegNumber (10-значный унифицированный номер СФР) — но внешние системы сверяют этот тег с рег. номером ПФР организации (формат XXX-XXX-XXXXXX), который у них уже настроен через канал ФСС ЭДО, а не с номером СФР.

Добавлено отдельное обязательное поле pfrRegNumber (report_requisites.pfr_reg_number, XXX-XXX-XXXXXX) — не заменяет sfrRegNumber (оставлен, вдруг понадобится), а используется отдельно для генерации ЕФС-1: и в сегменте имени файла, и в теге <ЕФС8:РегНомер>. Заодно сузили маску sfrRegNumber до строго 10 цифр (раньше маска дублировала формат ПФР и два поля выглядели взаимозаменяемыми — по замечанию в ревью). Цепочка regen пройдена (generate-schema → generate-client → sdk build), controller tsc и desktop vue-tsc чистые, добавлены/обновлены unit-тесты Fss4Generator.
2026-07-21 18:45:12 +00:00
coopops db28ba1137 [989-10][@ant] fix: чинить генерацию имени файла нулёвых отчётов (idFile/ИдФайл) — терялся код формы ФНС/СФР
generateGenericFileName брал префикс как первый underscore-сегмент имени XSD (xsd.split('_')[0]), а для всех форм с xsdFile вида 'NO_<Форма>_...' первый сегмент всегда равен 'NO' — код формы (NDFL6.2, RASCHSV, PERSSVFL, USN, UVISCHSUMNAL) терялся из имени файла. Внешние системы (в частности приёмка Сбера) отклоняли выгруженный XML как нераспознанный по имени, а после ручного добавления '_NDFL6.2_' в имя падала уже сверка тега <ИдФайл> с фактическим именем файла — оба генерировались одной и той же испорченной функцией. Исправлено: префикс строится из первых двух сегментов XSD-имени (slice(0, 2)), что совпадает с реальным форматом имени файла ФНС/СФР и с эталонными фикстурами tests/fixtures/reports-references/*.
2026-07-21 16:23:21 +00:00
coopops 6a05e88ba1 [989-10][@ant] fix: убрать stack-раскладку (иконка над текстом) у кнопок панели действий в ReportEditorDialog расширения reports — canon-паддинг q-btn (0 14px, без вертикального зазора) при вертикальной раскладке icon+label прижимал иконку к верхней границе кнопки, из-за чего панель «Действия»/«Скачать»/«Отметить сданным» выглядела недоделанной; иконка возвращена в строку рядом с текстом 2026-07-21 15:56:09 +00:00
coopops 09eaa6192c [reports][@ant] fix: показывать серверные ошибки валидации формы отчёта для полей шапки и строк баланса — счётчик «Ошибок: N» был непрозрачным без единой подсказки
BuhotchEditor/ZeroReportEditor подключали fieldErrors только для части полей
(организация, часть подписанта). Поля шапки (reportYear/period/docDate/
correctionNumber) и все строки баланса (BalanceRowEditor — otch/prev/prePrev)
не имели inline-подсветки вообще, хотя backend (validateReportEdits) исправно
возвращал path+message. В проде это привело к невидимой ошибке
organization.ogrn — 12 vs 13 цифр (опечатка в реквизитах, отдельно исправлена
data-фиксом в organizations, не кодом).

- BalanceRowEditor: добавлены basePath/fieldErrors пропсы, подсветка otch/prev/prePrev.
- BuhotchEditor: прокинул field-errors во все 6 BalanceRowEditor, подключил
  header.reportYear/docDate/correctionNumber.
- ZeroReportEditor: подключил header.reportYear/period.
- ReportEditorDialog: добавлен явный список ошибок (path → человекочитаемое
  сообщение) в панели действий — страховка на случай ещё не подключённых полей.
2026-07-21 15:45:20 +00:00
coopops 81df662cfa chore(release): publish
Build bootstrap container / build (push) Successful in 46s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / release (push) Successful in 34m17s
Release / trigger-mono-docs (push) Successful in 2s
Release / publish-packages (push) Failing after 13m36s
2026-07-21 08:50:38 +00:00
coopops 845c13605d fix(reports): разрешить 10-значный рег.номер СФР наравне с XXX-XXX-XXXXXX
XSD-схема ЕФС-1 (ТипРегНомерОбщ) допускает регистрационный номер
страхователя в двух форматах: действующий XXX-XXX-XXXXXX (12 цифр,
3-3-6) или прежний — 10 цифр без разделителей. Валидация на бэке и
фронте (форма реквизитов кооператива + редактор нулевого отчёта
ЕФС-1) принимала только 12-значный формат с тире, из-за чего
кооперативы со старым 10-значным номером ФСС не могли его сохранить.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 08:43:46 +00:00
Alex Ant 76b4988da8 migrate 2026-07-20 11:34:49 +03:00
coopops 3db80a30e9 chore(release): publish
Release / release (push) Failing after 2m16s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
Build bootstrap container / build (push) Successful in 49s
2026-07-19 17:01:42 +00:00
coopops 1b457b8f87 fix(controller): stop migration failures being masked as success
Backfill-миграция V2.3.2 падала на testnet TS-ошибкой компиляции
(mongo.db возможно undefined, strictNullChecks), но deploy-скрипт
(blue-green-deploy.sh) рапортовал успех — 2.3.2 не появилась в
таблице migrations, extensions.capital_program_doc_data_hash осталась
пустой, а Semaphore-лог показывал "Database migrations completed
successfully".

Причина ложноположительного результата — не баг деплой-скрипта, а
Sentry: в production Sentry.init() (enabled: config.env==='production')
ставит process.on('unhandledRejection', ...) с дефолтным mode:'warn',
который перехватывает необработанный reject из bootstrap()'а --migrate
ветки и не роняет процесс — в отличие от штатного поведения голого
Node. В dev (Sentry выключен) тот же баг воспроизводимо валит процесс
exit 1; на testnet — exit 0. Подтверждено локальным воспроизведением
обеих версий на идентичном коде.

Фикс на двух уровнях:
1. index.ts: явный try/catch + process.exit(1) вокруг --migrate и
   --migrations-only веток. Не полагаемся на ambient unhandled-rejection
   поведение (Sentry/Node/версия) — exit code теперь детерминирован
   независимо от окружения.
2. V2.3.2: mongo.db действительно типизирован как возможно undefined —
   явная runtime-проверка с throw вместо каста.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 16:57:59 +00:00
coopops ba6d3d1310 chore(release): publish
Build bootstrap container / build (push) Successful in 1m10s
Release / publish-packages (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
Release / release (push) Successful in 28m13s
Release / trigger-coopenomics-docs (push) Has been skipped
2026-07-19 15:20:08 +00:00
ant 8e01373179 Merge pull request 'feat(capital): CPP document templates via PrivateData' (#154) from feature/capital-cpp-private-data-templates into dev
Build bootstrap container / build (push) Successful in 3m30s
Reviewed-on: #154
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-07-19 15:14:39 +00:00
coopops 31940461c6 fix(desktop): remove electron dependency, fix latent typecheck errors
Typecheck / desktop (pull_request) Successful in 15m11s
Typecheck / controller (pull_request) Successful in 13m55s
electron был не нужен вообще — тянулся транзитивно только через
неиспользуемую devDependency @vue/devtools (нет ни одного импорта в
коде). Убрана из desktop/package.json и из onlyBuiltDependencies
корня; lockfile пересобран. Это устраняет флап CI: pnpm install
изредка валился на сетевой ошибке при скачивании ~200MB бинарника
electron (run #745 и run #732 на этом PR), из-за чего typecheck
падал до первой TS-проверки.

Так как install перестал падать раньше срока, vue-tsc теперь
реально доходит до конца и вскрывает предсуществующие баги:

- useCapitalProgramDocParams.ts, CapitalProgramDocumentParametersWidget.vue:
  импорт `CapitalProgramPrivateData` напрямую из 'cooptypes' никогда не
  работал — тип лежит в Cooperative.Registry, не на верхнем уровне
  (остальной код capital-расширения уже импортирует так же). Каст
  результата Object.fromEntries (индексная сигнатура) в интерфейс с
  именованными полями TS отклоняет как insufficient overlap; вместо
  unknown-каста — каст в Record<EditableFieldKey, string> (тот же
  паттерн, что и в соседней createEmptyForm), который затем обычной
  структурной проверкой присваивается в CapitalProgramPrivateData,
  т.к. набор из 11 полей совпадает один в один.
- CapitalProgramDocumentParametersWidget.vue: legacy draft.activeTab
  стал optional при переходе на wizardStepKey и больше не
  записывается читалкой драфта — добавлен фолбэк на дефолт 994,
  как и в самой readCapitalProgramDocParamsDraft.
- CapitalProgramInlineDocumentPreview.vue: querySelectorAll с
  составным CSS-селектором типизируется как NodeListOf<Element>;
  cleanupEditors использовал узел как HTMLTextAreaElement без
  сужения (соседняя syncInlineEditors уже делает `as
  HTMLTextAreaElement` для той же ситуации).
- ExtensionInstall.vue: prop schema был типизирован как unknown,
  хотя передаётся напрямую в ZodForm, которому нужен
  IExtensionConfigSchema — типизирован точно под фактическое
  использование.
- sdk/queries/paymentMethods/getPaymentMethods.ts: `const name`
  был без export вопреки конвенции всех остальных query-модулей
  (Queries.X.Y.name используется для чтения ключа GraphQL-ответа).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 14:25:47 +00:00
coopops 37b0227735 fix(ci): skip electron binary download in typecheck workflow
Typecheck / desktop (pull_request) Failing after 9m12s
Typecheck / controller (pull_request) Has been cancelled
electron тянется транзитивно через @vue/devtools-electron (dev-only,
приложение devtools вне браузера) и попадает в onlyBuiltDependencies —
pnpm install качает ~200MB бинарник с github releases, который здесь
никогда не запускается. Изредка скачивание валит install сетевым
таймаутом/TLS-обрывом до того, как дело доходит до vue-tsc/tsc.

Run #745 (PR #154): desktop job упал именно на этом шаге, не на
типах — коммит 2cfb37c5d7 тут ни при чём, чистая инфраструктурная
нестабильность CI-раннера.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 14:07:54 +00:00
coopops 2cfb37c5d7 fix(agenda): resolve program-agreement links via soviet::newresolved, not wallet::signagree
Typecheck / desktop (pull_request) Failing after 6m47s
Typecheck / controller (pull_request) Has been cancelled
Прошлый фикс менял receiver=coopname на data.coopname прямо в explorer-запросе
к wallet::signagree — рабочий, но не объясняющий, почему стандартный
receiver-фильтр не подходил именно здесь: wallet::signagree не делает
require_recipient(coopname) сам по себе, поэтому explorer индексирует его
только под receiver='wallet'.

На самом деле уведомление уже существует и не требует правки контракта:
Soviet::make_complete_document (lib/core/soviet/soviet.hpp) на КАЖДЫЙ вызов —
из soviet::sndagreement И из wallet::signagree одинаково — шлёт inline
soviet::newresolved/newsubmitted, а require_recipient(coopname/username)
зашит централизованно внутри самих newresolved.cpp/newsubmitted.cpp
(soviet/src/doc/*.cpp). Проверено на живом хэше оферты Благорост: newresolved
уже приходит с receiver=voskhod, receiver=veszmgpjteqs, receiver=soviet — без
единой правки на контрактах.

Правильный fallback для findLinkedAgreementDocument — искать soviet::newresolved
(с обычным receiver=coopname, как везде), а не действие wallet::signagree
напрямую (у которого никакого receiver-уведомления и не предполагалось).
Расширять notification-поверхность wallet::signagree не нужно — единственное
место для этого уже newresolved.cpp/newsubmitted.cpp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:48:57 +00:00
coopops 1c605dd2a5 fix(agenda): resolve wallet::signagree linked documents by data.coopname, not receiver
Typecheck / desktop (pull_request) Failing after 9m39s
Typecheck / controller (pull_request) Has been cancelled
findLinkedAgreementDocument искал wallet::signagree по фильтру receiver=coopname,
но wallet::signagree не делает require_recipient(coopname) (в отличие от
Soviet::make_complete_document) — explorer индексирует это действие только с
receiver='wallet'. Фильтр receiver=coopname всегда давал 0 строк, поэтому
buildLinkedAggregate молча возвращал null, а программные оферты (capital:
Благорост/Генератор, program_id>0, подписываются через wallet::signagree, не
soviet::sndagreement) пропадали из links повестки совета — платформенные
соглашения (wallet/signature/privacy/user, идут через sndagreement) отображались,
а оферта ЦПП — нет, хотя её hash был на месте в meta.links и сам документ был
в реестре.

Воспроизведено и проверено на живом кандидате voskhod: 5 хэшей в meta.links,
но до фикса только 4 резолвились в документ-агрегаты. Скоуп по кооперативу
берём из payload действия (data.coopname), а не из индекса receiver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:36:58 +00:00
coopops b1b1775277 fix(contracts): migrate voskhod programs.program_type back to 'capital'
Typecheck / desktop (pull_request) Failing after 8m46s
Typecheck / controller (pull_request) Successful in 13m44s
Автоматическая CI/CD-миграция (soviet::migrate) раньше переименовывала
programs.program_type[id=4] voskhod в 'blagorost', но coagreements.type для
той же программы никогда не переименовывался и остаётся 'capital' — отсюда
расхождение двух таблиц на одном и том же боевом кооперативе, которое видно
любым прямым запросом к цепочке и вносит путаницу.

'capital' — каноничное значение по всей цепочке решений в этой сессии: это
то, что реально в coagreements.type, то, на что рассчитывает _capital_program
в lib/consts.hpp, и то, что теперь везде в controller/boot. Новый блок в
migrate() приводит programs.program_type к нему же — идемпотентно, сработает
автоматически на следующем деплое soviet, без ручных действий на проде.
soviet и capital пересобраны локально — компилируются чисто.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 12:35:37 +00:00
coopops 5f4281ac01 revert(contracts): _capital_program back to 'capital', drop stray blagorost migration
Typecheck / desktop (pull_request) Failing after 8m52s
Typecheck / controller (pull_request) Failing after 6m44s
Единственный источник правды — продовая цепочка voskhod: coagreements.type
для program_id=4 буквально 'capital' (проверено прямым запросом к
185.70.105.87), это боевое неизменяемое значение. Contract-константа
_capital_program в lib/consts.hpp была переименована в 'blagorost' коммитом
e1538bb5ab (2026-01-26) и никогда не доезжала до прода — из-за этого dev-цепочки
собирали программу с другим именем и падали на 'Недопустимый тип программы'
при попытке засеять её как 'capital'.

Заодно убран автоматический CI/CD-migrate в soviet::migrate(), который на
каждом деплое насильно переименовывал programs.program_type[id=4] voskhod в
'blagorost' — он и был причиной, что programs.program_type на проде уже
'blagorost', а coagreements.type там же остался 'capital' (миграция трогала
только одну из двух таблиц). Раз откатываем контракт на 'capital' насовсем —
эта миграция больше не нужна и мешает: на следующем деплое она снова
рассинхронила бы данные.

Boot-скрипты и BLAGOROST_AGREEMENT_TYPE в controller уже используют 'capital'
(предыдущий коммит) — теперь везде единое значение, dev-цепочка при
pnpm run reboot соберёт programs+coagreements так же, как боевой voskhod, без
какой-либо миграции на проде.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:58:50 +00:00
coopops d35e88d951 revert(capital): keep BLAGOROST_AGREEMENT_TYPE='capital', fix boot seed drift instead
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Прямой запрос к продовой цепочке voskhod (185.70.105.87) показал реальный
on-chain тип программы Благорост — 'capital' (program_id=4), не 'blagorost'.
Прод годами работает с этим значением, ошибок в логах нет. Предыдущий коммит
(5360daa471) поменял константу на 'blagorost' по данным ТОЛЬКО локальной
dev-цепочки mono-ai-2 — для прода это было бы регрессией. Откатываю.

Настоящий источник дрейфа — boot-сидинг: components/boot/src/init/cooperative.ts
createPrograms() и src/scripts/seed-capital/phases/01-programs.ts заводили
новый кооператив (createprog) с type='blagorost', расходясь с давно
задеплоенным продовым значением. Правлю сидинг под реальность, а не наоборот
(та же логика, что 'дрейф стандарт↔код — прав контракт': on-chain состояние
неизменяемо и авторитетно, код подстраивается). Заодно поправлены прочие
литералы 'blagorost' в тестах boot (capital.test.ts, capital-import.test.ts,
08-investments.ts, tests/capital/consts.ts), которые создавали/подписывали
программу под тем же неверным именем.

> monocoop@ reboot /home/admin/mono-ai-2
> cd components/boot/scripts && bash reboot.sh

Останавливаем и удаляем контейнеры с volumes...
Останавливаем blockchain контейнер...
Удаляем blockchain data...
Пересоздаем и запускаем базы данных...
Ждем готовности MongoDB...
MongoDB готов!
Ждем готовности PostgreSQL...
PostgreSQL готов!
Запускаем boot процесс...

> @coopenomics/boot@2026.6.24 boot /home/admin/mono-ai-2/components/boot
> esno src/index.ts boot

Node is healthy. Continue. {
  server_version: 'ee002f93',
  chain_id: 'db79c8409645082749ca50640d6f4ee511575acf26c4e2c8e4748e6bf6a01ed4',
  head_block_num: 10,
  last_irreversible_block_num: 9,
  last_irreversible_block_id: '000000093c561104c62b9058c82eab12559686eba92fdbe4bcf4df9bf2600131',
  head_block_id: '0000000ae4bffe027ec2a2c89ca92832422a84ec052649f1a80436fff5c61327',
  head_block_time: '2026-07-19T11:48:57.500',
  head_block_producer: 'eosio',
  virtual_block_cpu_limit: 1008036,
  virtual_block_net_limit: 1058054,
  block_cpu_limit: 1000000,
  block_net_limit: 1048576,
  server_version_string: 'v6.0.0',
  fork_db_head_block_num: 10,
  fork_db_head_block_id: '0000000ae4bffe027ec2a2c89ca92832422a84ec052649f1a80436fff5c61327',
  server_full_version_string: 'v6.0.0-ee002f9372237aa07b05879df772a95f9d54867f-dirty',
  total_cpu_weight: 0,
  total_net_weight: 0,
  earliest_available_block_num: 1,
  last_irreversible_block_time: '2026-07-19T11:48:57.000'
}
Created account: eosio.token
Created account: eosio.bpay
Created account: eosio.vpay
Created account: eosio.msig
Created account: eosio.wrap
Created account: eosio.power
Created account: eosio.saving
Created account: registrator
Created account: capital
Created account: loan
Created account: ledger
Created account: ledger2
Created account: test
Created account: meet
Created account: wallet
Created account: soviet
Created account: branch
Created account: marketplace
Created account: draft
Created account: gateway
Created account: fund
Created account: contributor
Created account: apps
Created account: expense
Created account: voskhod
ok -> init activation:  { result: 'ok' }
on set:  {
  name: 'eosio.boot',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.boot',
  target: 'eosio'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.boot/eosio.boot.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.boot/eosio.boot.abi
contract setted:  eosio
Фича активирована:  ACTION_RETURN_VALUE
Фича активирована:  CONFIGURABLE_WASM_LIMITS2
Фича активирована:  BLOCKCHAIN_PARAMETERS
Фича активирована:  GET_SENDER
Фича активирована:  FORWARD_SETCODE
Фича активирована:  ONLY_BILL_FIRST_AUTHORIZER
Фича активирована:  RESTRICT_ACTION_TO_SELF
Фича активирована:  DISALLOW_EMPTY_PRODUCER_SCHEDULE
Фича активирована:  FIX_LINKAUTH_RESTRICTION
Фича активирована:  REPLACE_DEFERRED
Фича активирована:  NO_DUPLICATE_DEFERRED_ID
Фича активирована:  ONLY_LINK_TO_EXISTING_PERMISSION
Фича активирована:  RAM_RESTRICTIONS
Фича активирована:  WEBAUTHN_KEY
Фича активирована:  WTMSIG_BLOCK_SIGNATURES
Фича активирована:  GET_CODE_HASH
Фича активирована:  GET_BLOCK_NUM
Фича активирована:  CRYPTO_PRIMITIVES
on set:  {
  name: 'eosio.system',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.system',
  target: 'eosio'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.system/eosio.system.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.system/eosio.system.abi
contract setted:  eosio
on set:  {
  name: 'eosio.token',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.token',
  target: 'eosio.token'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.token/eosio.token.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.token/eosio.token.abi
contract setted:  eosio.token
on set:  {
  name: 'eosio.msig',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.msig',
  target: 'eosio.msig'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.msig/eosio.msig.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.msig/eosio.msig.abi
contract setted:  eosio.msig
on set:  {
  name: 'eosio.wrap',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.wrap',
  target: 'eosio.wrap'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.wrap/eosio.wrap.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.wrap/eosio.wrap.abi
contract setted:  eosio.wrap
on set:  {
  name: 'registrator',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/registrator',
  target: 'registrator'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/registrator/registrator.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/registrator/registrator.abi
contract setted:  registrator
on set:  {
  name: 'soviet',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/soviet',
  target: 'soviet'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/soviet/soviet.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/soviet/soviet.abi
contract setted:  soviet
on set:  {
  name: 'marketplace',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/marketplace',
  target: 'marketplace'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/marketplace/marketplace.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/marketplace/marketplace.abi
contract setted:  marketplace
on set:  {
  name: 'draft',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/draft',
  target: 'draft'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/draft/draft.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/draft/draft.abi
contract setted:  draft
on set:  {
  name: 'branch',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/branch',
  target: 'branch'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/branch/branch.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/branch/branch.abi
contract setted:  branch
on set:  {
  name: 'gateway',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/gateway',
  target: 'gateway'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/gateway/gateway.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/gateway/gateway.abi
contract setted:  gateway
on set:  {
  name: 'fund',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/fund',
  target: 'fund'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/fund/fund.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/fund/fund.abi
contract setted:  fund
on set:  {
  name: 'ledger',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/ledger',
  target: 'ledger'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/ledger/ledger.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/ledger/ledger.abi
contract setted:  ledger
on set:  {
  name: 'ledger2',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/ledger2',
  target: 'ledger2'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/ledger2/ledger2.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/ledger2/ledger2.abi
contract setted:  ledger2
on set:  {
  name: 'test',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/test',
  target: 'test'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/test/test.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/test/test.abi
contract setted:  test
on set:  {
  name: 'contributor',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/contributor',
  target: 'contributor'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/contributor/contributor.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/contributor/contributor.abi
contract setted:  contributor
on set:  {
  name: 'capital',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/capital',
  target: 'capital'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/capital/capital.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/capital/capital.abi
contract setted:  capital
on set:  {
  name: 'wallet',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/wallet',
  target: 'wallet'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/wallet/wallet.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/wallet/wallet.abi
contract setted:  wallet
on set:  {
  name: 'loan',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/loan',
  target: 'loan'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/loan/loan.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/loan/loan.abi
contract setted:  loan
on set:  {
  name: 'meet',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/meet',
  target: 'meet'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/meet/meet.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/meet/meet.abi
contract setted:  meet
on set:  {
  name: 'apps',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/apps',
  target: 'apps'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/apps/apps.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/apps/apps.abi
contract setted:  apps
on set:  {
  name: 'expense',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/expense',
  target: 'expense'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/expense/expense.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/expense/expense.abi
contract setted:  expense
создаём токен
Токен создан:  { issuer: 'eosio', maximum_supply: '1000000000.0000 AXON' }
выпускаем токены
Токен выпущен:  { to: 'eosio', quantity: '100000.0000 AXON', memo: '' }
Updated account permissions: eosio.saving -> eosio.saving
Updated account permissions: registrator -> registrator
Updated account permissions: capital -> capital
Updated account permissions: loan -> loan
Updated account permissions: ledger -> ledger
Updated account permissions: ledger2 -> ledger2
Updated account permissions: test -> test
Updated account permissions: meet -> meet
Updated account permissions: wallet -> wallet
Updated account permissions: soviet -> soviet
Updated account permissions: branch -> branch
Updated account permissions: marketplace -> marketplace
Updated account permissions: draft -> draft
Updated account permissions: gateway -> gateway
Updated account permissions: fund -> fund
Updated account permissions: contributor -> contributor
Updated account permissions: apps -> apps
Updated account permissions: expense -> expense
Updated account permissions: voskhod -> registrator
Системный контракт инициализирован { version: 0, core: '4,AXON' }
Эмиссия инициализирована {
  init_supply: '1000.0000 AXON',
  tact_duration: 86400,
  emission_factor: 0.618
}
Система аренды инициализирована { args: { powerup_days: 1, min_powerup_fee: '0.0001 AXON' } }
Шаблон создан:  1
Шаблон создан:  2
Шаблон создан:  3
Шаблон создан:  4
Шаблон создан:  50
Шаблон создан:  51
Шаблон создан:  100
Шаблон создан:  101
Шаблон создан:  200
Шаблон создан:  201
Шаблон создан:  300
Шаблон создан:  301
Шаблон создан:  302
Шаблон создан:  303
Шаблон создан:  304
Шаблон создан:  501
Шаблон создан:  599
Шаблон создан:  600
Шаблон создан:  699
Шаблон создан:  700
Шаблон создан:  701
Шаблон создан:  702
Шаблон создан:  800
Шаблон создан:  801
Шаблон создан:  802
Шаблон создан:  900
Шаблон создан:  901
Шаблон создан:  994
Шаблон создан:  995
Шаблон создан:  996
Шаблон создан:  997
Шаблон создан:  998
Шаблон создан:  999
Шаблон создан:  1000
Шаблон создан:  1001
Шаблон создан:  1002
Шаблон создан:  1003
Шаблон создан:  1004
Шаблон создан:  1005
Шаблон создан:  1006
Шаблон создан:  1007
Шаблон создан:  1010
Шаблон создан:  1011
Шаблон создан:  1020
Шаблон создан:  1025
Шаблон создан:  1030
Шаблон создан:  1040
Шаблон создан:  1041
Шаблон создан:  1042
Шаблон создан:  1050
Шаблон создан:  1051
Шаблон создан:  1060
Шаблон создан:  1061
Шаблон создан:  1062
Шаблон создан:  1070
Шаблон создан:  1071
Шаблон создан:  1072
Шаблон создан:  1080
Шаблон создан:  1090
Шаблон создан:  2010
Шаблон создан:  2011
Арендуем ресурсы провайдеру
Аренда ресурсов:  {
  payer: 'eosio',
  receiver: 'voskhod',
  days: 1,
  payment: '10000.0000 AXON',
  transfer: true
}
{ from: 'eosio', to: 'voskhod', quantity: '1000.0000 AXON', memo: '' }
Перевод токенов:  { from: 'eosio', to: 'voskhod', quantity: '1000.0000 AXON', memo: '' }
Базовая инфраструктура установлена
Провайдер добавлен:  {
  username: 'voskhod',
  type: 'coop',
  short_name: 'ПК "Восход"',
  full_name: 'Потребительский Кооператив "ВОСХОД"',
  represented_by: {
    first_name: 'Иван',
    last_name: 'Иванов',
    middle_name: 'Иванович',
    position: 'Председатель',
    based_on: 'Решение общего собрания №1'
  },
  country: 'Российская Федерация',
  city: 'Москва',
  fact_address: '117593 г. Москва, муниципальный округ Ясенево, проезд Соловьиный, дом 1, помещение 1/1',
  full_address: '117593 г. Москва, муниципальный округ Ясенево, проезд Соловьиный, дом 1, помещение 1/1',
  email: 'copenomics@yandex.ru',
  phone: '+71234567890',
  details: { inn: '9728130611', ogrn: '1247700283346', kpp: '772801001' }
}
Vars сохранены с указанным _id и _created_at
Все документы удалены из коллекции sync
Все документы удалены из коллекции actions
Все документы удалены из коллекции deltas
Добавляем пайщика ant
{
  coopname: 'voskhod',
  referer: '',
  username: 'ant',
  type: 'individual',
  created_at: '2025-01-15T10:00:00',
  initial: '100.0000 RUB',
  minimum: '200.0000 RUB',
  spread_initial: true,
  meta: 'Основатель кооператива ВОСХОД',
  registration_hash: '1e87d2a7d35f1be95da5a676ae799262e970647d0ba69cd19c0eaff0afd5b488'
}
Добавлен пользователь:  ant
Устанавливаем дефолтный публичный ключ для ant
{
  coopname: 'voskhod',
  changer: 'voskhod',
  username: 'ant',
  public_key: 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV'
}
Изменён ключ для пользователя:  ant
Инициализируем пользователей в PostgreSQL
Подключение к PostgreSQL установлено для инициализации пользователей
Инициализировано 1 пользователей в PostgreSQL
Инициализируем vault в PostgreSQL
Подключение к PostgreSQL установлено для инициализации vault
Vault инициализирован в PostgreSQL
Создаём совет
{
  coopname: 'voskhod',
  username: 'ant',
  type: 'soviet',
  members: [
    {
      username: 'ant',
      is_voting: true,
      position_title: 'Председатель совета',
      position: 'chairman'
    }
  ],
  name: 'Совет',
  description: 'Совет кооператива ВОСХОД'
}
Создан совет:  {
  coopname: 'voskhod',
  username: 'ant',
  type: 'soviet',
  members: [
    {
      username: 'ant',
      is_voting: true,
      position_title: 'Председатель совета',
      position: 'chairman'
    }
  ],
  name: 'Совет',
  description: 'Совет кооператива ВОСХОД'
}
Создаём программы и соглашения
params {
  coopname: 'voskhod',
  username: 'voskhod',
  type: 'marketplace',
  title: 'КООПЛЕЙС',
  announce: '',
  description: '',
  preview: '',
  images: '',
  calculation_type: 'relative',
  fixed_membership_contribution: '0.0000 RUB',
  membership_percent_fee: 10000,
  meta: '',
  is_can_coop_spend_share_contributions: false
}
Программа установлена:  {
  coopname: 'voskhod',
  username: 'voskhod',
  type: 'marketplace',
  title: 'КООПЛЕЙС',
  announce: '',
  description: '',
  preview: '',
  images: '',
  calculation_type: 'relative',
  fixed_membership_contribution: '0.0000 RUB',
  membership_percent_fee: 10000,
  meta: '',
  is_can_coop_spend_share_contributions: false
}
params {
  coopname: 'voskhod',
  username: 'voskhod',
  type: 'generator',
  title: 'Целевая потребительская программа "Генератор"',
  announce: '',
  description: '',
  preview: '',
  images: '',
  calculation_type: 'free',
  fixed_membership_contribution: '0.0000 RUB',
  membership_percent_fee: 0,
  meta: '',
  is_can_coop_spend_share_contributions: true
}
Программа установлена:  {
  coopname: 'voskhod',
  username: 'voskhod',
  type: 'generator',
  title: 'Целевая потребительская программа "Генератор"',
  announce: '',
  description: '',
  preview: '',
  images: '',
  calculation_type: 'free',
  fixed_membership_contribution: '0.0000 RUB',
  membership_percent_fee: 0,
  meta: '',
  is_can_coop_spend_share_contributions: true
}
params {
  coopname: 'voskhod',
  username: 'voskhod',
  type: 'capital',
  title: 'Целевая потребительская программа "Благорост"',
  announce: '',
  description: '',
  preview: '',
  images: '',
  calculation_type: 'free',
  fixed_membership_contribution: '0.0000 RUB',
  membership_percent_fee: 0,
  meta: '',
  is_can_coop_spend_share_contributions: true
}
 ELIFECYCLE  Command failed with exit code 1.
Запускаем parser...
Запускаем контроллер...
Перезапуск завершен! пересоздаёт blockchain-data с нуля и гоняет
> monocoop@ boot /home/admin/mono-ai-2
> lerna run boot --scope @coopenomics/boot

> @coopenomics/boot:boot

> @coopenomics/boot@2026.6.24 boot /home/admin/mono-ai-2/components/boot
> esno src/index.ts boot
Node is healthy. Continue. {
  server_version: 'ee002f93',
  chain_id: 'db79c8409645082749ca50640d6f4ee511575acf26c4e2c8e4748e6bf6a01ed4',
  head_block_num: 123,
  last_irreversible_block_num: 122,
  last_irreversible_block_id: '0000007adc2e2215250e7ffbd9c7961faa675f57c3134be365b336292fda7e72',
  head_block_id: '0000007bf4fa5276948ff1ea2dce877a2032ac28be3a753271efd198ed5f79d5',
  head_block_time: '2026-07-19T11:49:54.000',
  head_block_producer: 'eosio',
  virtual_block_cpu_limit: 1128638,
  virtual_block_net_limit: 1184640,
  block_cpu_limit: 1000000,
  block_net_limit: 1048576,
  server_version_string: 'v6.0.0',
  fork_db_head_block_num: 123,
  fork_db_head_block_id: '0000007bf4fa5276948ff1ea2dce877a2032ac28be3a753271efd198ed5f79d5',
  server_full_version_string: 'v6.0.0-ee002f9372237aa07b05879df772a95f9d54867f-dirty',
  total_cpu_weight: 999999997,
  total_net_weight: 999999997,
  earliest_available_block_num: 1,
  last_irreversible_block_time: '2026-07-19T11:49:53.500'
}
error -> init activation:  AxiosError: Request failed with status code 500
    at settle (file:///home/admin/mono-ai-2/node_modules/.pnpm/axios@1.13.6/node_modules/axios/lib/core/settle.js:20:7)
    at IncomingMessage.handleStreamEnd (file:///home/admin/mono-ai-2/node_modules/.pnpm/axios@1.13.6/node_modules/axios/lib/adapters/http.js:844:13)
    at IncomingMessage.emit (node:events:531:35)
    at endReadableNT (node:internal/streams/readable:1698:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:89:21)
    at Axios.request (file:///home/admin/mono-ai-2/node_modules/.pnpm/axios@1.13.6/node_modules/axios/lib/core/Axios.js:46:41)
    at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
    at async startInfra (/home/admin/mono-ai-2/components/boot/src/init/infra.ts:45:22)
    at async boot (/home/admin/mono-ai-2/components/boot/src/init/booter.ts:6:22)
    at async Command.<anonymous> (/home/admin/mono-ai-2/components/boot/src/index.ts:144:7) {
  isAxiosError: true,
  code: 'ERR_BAD_RESPONSE',
  config: {
    transitional: {
      silentJSONParsing: true,
      forcedJSONParsing: true,
      clarifyTimeoutError: false,
      legacyInterceptorReqResOrdering: true
    },
    adapter: [ 'xhr', 'http', 'fetch' ],
    transformRequest: [ [Function: transformRequest] ],
    transformResponse: [ [Function: transformResponse] ],
    timeout: 0,
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    maxBodyLength: -1,
    env: { FormData: [Function], Blob: [class Blob] },
    validateStatus: [Function: validateStatus],
    headers: Object [AxiosHeaders] {
      Accept: 'application/json, text/plain, */*',
      'Content-Type': 'application/json',
      'User-Agent': 'axios/1.13.6',
      'Content-Length': '102',
      'Accept-Encoding': 'gzip, compress, deflate, br'
    },
    method: 'post',
    url: 'http://127.0.0.1:8898/v1/producer/schedule_protocol_feature_activations',
    data: '{"protocol_features_to_activate":["0ec7e080177b2c02b278d5088611686b49d739925a92d9bfcacd7fc6b74053bd"]}',
    allowAbsoluteUrls: true
  },
  request: <ref *1> ClientRequest {
    _events: [Object: null prototype] {
      abort: [Function (anonymous)],
      aborted: [Function (anonymous)],
      connect: [Function (anonymous)],
      error: [Function (anonymous)],
      socket: [Function (anonymous)],
      timeout: [Function (anonymous)],
      finish: [Function: requestOnFinish]
    },
    _eventsCount: 7,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    destroyed: true,
    _last: false,
    chunkedEncoding: false,
    shouldKeepAlive: true,
    maxRequestsOnConnectionReached: false,
    _defaultKeepAlive: true,
    useChunkedEncodingByDefault: true,
    sendDate: false,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    strictContentLength: false,
    _contentLength: 102,
    _hasBody: true,
    _trailer: '',
    finished: true,
    _headerSent: true,
    _closed: true,
    _header: 'POST /v1/producer/schedule_protocol_feature_activations HTTP/1.1\r\n' +
      'Accept: application/json, text/plain, */*\r\n' +
      'Content-Type: application/json\r\n' +
      'User-Agent: axios/1.13.6\r\n' +
      'Content-Length: 102\r\n' +
      'Accept-Encoding: gzip, compress, deflate, br\r\n' +
      'Host: 127.0.0.1:8898\r\n' +
      'Connection: keep-alive\r\n' +
      '\r\n',
    _keepAliveTimeout: 0,
    _onPendingData: [Function: nop],
    agent: Agent {
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      options: [Object: null prototype],
      defaultPort: 80,
      protocol: 'http:',
      requests: [Object: null prototype] {},
      sockets: [Object: null prototype] {},
      freeSockets: [Object: null prototype],
      keepAliveMsecs: 1000,
      keepAlive: true,
      maxSockets: Infinity,
      maxFreeSockets: 256,
      scheduling: 'lifo',
      maxTotalSockets: Infinity,
      totalSocketCount: 1,
      agentKeepAliveTimeoutBuffer: 1000,
      [Symbol(shapeMode)]: false,
      [Symbol(kCapture)]: false
    },
    socketPath: undefined,
    method: 'POST',
    maxHeaderSize: undefined,
    insecureHTTPParser: undefined,
    joinDuplicateHeaders: undefined,
    path: '/v1/producer/schedule_protocol_feature_activations',
    _ended: true,
    res: IncomingMessage {
      _events: [Object],
      _readableState: [ReadableState],
      _maxListeners: undefined,
      socket: null,
      httpVersionMajor: 1,
      httpVersionMinor: 1,
      httpVersion: '1.1',
      complete: true,
      rawHeaders: [Array],
      rawTrailers: [],
      joinDuplicateHeaders: undefined,
      aborted: false,
      upgrade: false,
      url: '',
      method: null,
      statusCode: 500,
      statusMessage: 'Internal Server Error',
      client: [Socket],
      _consuming: false,
      _dumped: false,
      req: [Circular *1],
      _eventsCount: 4,
      responseUrl: 'http://127.0.0.1:8898/v1/producer/schedule_protocol_feature_activations',
      redirects: [],
      [Symbol(shapeMode)]: true,
      [Symbol(kCapture)]: false,
      [Symbol(kHeaders)]: [Object],
      [Symbol(kHeadersCount)]: 8,
      [Symbol(kTrailers)]: null,
      [Symbol(kTrailersCount)]: 0
    },
    aborted: false,
    timeoutCb: null,
    upgradeOrConnect: false,
    parser: null,
    maxHeadersCount: null,
    reusedSocket: true,
    host: '127.0.0.1',
    protocol: 'http:',
    _redirectable: Writable {
      _events: [Object],
      _writableState: [WritableState],
      _maxListeners: undefined,
      _options: [Object],
      _ended: true,
      _ending: true,
      _redirectCount: 0,
      _redirects: [],
      _requestBodyLength: 102,
      _requestBodyBuffers: [],
      _eventsCount: 3,
      _onNativeResponse: [Function (anonymous)],
      _currentRequest: [Circular *1],
      _currentUrl: 'http://127.0.0.1:8898/v1/producer/schedule_protocol_feature_activations',
      _timeout: null,
      [Symbol(shapeMode)]: true,
      [Symbol(kCapture)]: false
    },
    [Symbol(shapeMode)]: false,
    [Symbol(kCapture)]: false,
    [Symbol(kBytesWritten)]: 0,
    [Symbol(kNeedDrain)]: false,
    [Symbol(corked)]: 0,
    [Symbol(kChunkedBuffer)]: [],
    [Symbol(kChunkedLength)]: 0,
    [Symbol(kSocket)]: Socket {
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: null,
      _closeAfterHandlingError: false,
      _events: [Object],
      _readableState: [ReadableState],
      _writableState: [WritableState],
      allowHalfOpen: false,
      _maxListeners: undefined,
      _eventsCount: 6,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: null,
      _server: null,
      timeout: 5000,
      parser: null,
      _httpMessage: null,
      [Symbol(async_id_symbol)]: -1,
      [Symbol(kHandle)]: [TCP],
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: Timeout {
        _idleTimeout: 5000,
        _idlePrev: [TimersList],
        _idleNext: [TimersList],
        _idleStart: 11165,
        _onTimeout: [Function: bound ],
        _timerArgs: undefined,
        _repeat: null,
        _destroyed: false,
        [Symbol(refed)]: false,
        [Symbol(kHasPrimitive)]: false,
        [Symbol(asyncId)]: 3415,
        [Symbol(triggerId)]: 3413,
        [Symbol(kAsyncContextFrame)]: undefined
      },
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(shapeMode)]: true,
      [Symbol(kCapture)]: false,
      [Symbol(kSetNoDelay)]: true,
      [Symbol(kSetKeepAlive)]: true,
      [Symbol(kSetKeepAliveInitialDelay)]: 1,
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0
    },
    [Symbol(kOutHeaders)]: [Object: null prototype] {
      accept: [Array],
      'content-type': [Array],
      'user-agent': [Array],
      'content-length': [Array],
      'accept-encoding': [Array],
      host: [Array]
    },
    [Symbol(errored)]: null,
    [Symbol(kHighWaterMark)]: 65536,
    [Symbol(kRejectNonStandardBodyWrites)]: false,
    [Symbol(kUniqueHeaders)]: null
  },
  response: {
    status: 500,
    statusText: 'Internal Server Error',
    headers: Object [AxiosHeaders] {
      server: 'nodeos/v6.0.0',
      'access-control-allow-origin': '*',
      'content-type': 'application/json',
      'content-length': '371'
    },
    config: {
      transitional: [Object],
      adapter: [Array],
      transformRequest: [Array],
      transformResponse: [Array],
      timeout: 0,
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxContentLength: -1,
      maxBodyLength: -1,
      env: [Object],
      validateStatus: [Function: validateStatus],
      headers: [Object [AxiosHeaders]],
      method: 'post',
      url: 'http://127.0.0.1:8898/v1/producer/schedule_protocol_feature_activations',
      data: '{"protocol_features_to_activate":["0ec7e080177b2c02b278d5088611686b49d739925a92d9bfcacd7fc6b74053bd"]}',
      allowAbsoluteUrls: true
    },
    request: <ref *1> ClientRequest {
      _events: [Object: null prototype],
      _eventsCount: 7,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      destroyed: true,
      _last: false,
      chunkedEncoding: false,
      shouldKeepAlive: true,
      maxRequestsOnConnectionReached: false,
      _defaultKeepAlive: true,
      useChunkedEncodingByDefault: true,
      sendDate: false,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      strictContentLength: false,
      _contentLength: 102,
      _hasBody: true,
      _trailer: '',
      finished: true,
      _headerSent: true,
      _closed: true,
      _header: 'POST /v1/producer/schedule_protocol_feature_activations HTTP/1.1\r\n' +
        'Accept: application/json, text/plain, */*\r\n' +
        'Content-Type: application/json\r\n' +
        'User-Agent: axios/1.13.6\r\n' +
        'Content-Length: 102\r\n' +
        'Accept-Encoding: gzip, compress, deflate, br\r\n' +
        'Host: 127.0.0.1:8898\r\n' +
        'Connection: keep-alive\r\n' +
        '\r\n',
      _keepAliveTimeout: 0,
      _onPendingData: [Function: nop],
      agent: [Agent],
      socketPath: undefined,
      method: 'POST',
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      joinDuplicateHeaders: undefined,
      path: '/v1/producer/schedule_protocol_feature_activations',
      _ended: true,
      res: [IncomingMessage],
      aborted: false,
      timeoutCb: null,
      upgradeOrConnect: false,
      parser: null,
      maxHeadersCount: null,
      reusedSocket: true,
      host: '127.0.0.1',
      protocol: 'http:',
      _redirectable: [Writable],
      [Symbol(shapeMode)]: false,
      [Symbol(kCapture)]: false,
      [Symbol(kBytesWritten)]: 0,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kChunkedBuffer)]: [],
      [Symbol(kChunkedLength)]: 0,
      [Symbol(kSocket)]: [Socket],
      [Symbol(kOutHeaders)]: [Object: null prototype],
      [Symbol(errored)]: null,
      [Symbol(kHighWaterMark)]: 65536,
      [Symbol(kRejectNonStandardBodyWrites)]: false,
      [Symbol(kUniqueHeaders)]: null
    },
    data: { code: 500, message: 'Internal Service Error', error: [Object] }
  },
  status: 500
}
on set:  {
  name: 'eosio.boot',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.boot',
  target: 'eosio'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.boot/eosio.boot.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.boot/eosio.boot.abi
contract setted:  eosio
on set:  {
  name: 'eosio.system',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.system',
  target: 'eosio'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.system/eosio.system.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.system/eosio.system.abi
contract setted:  eosio
on set:  {
  name: 'eosio.token',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.token',
  target: 'eosio.token'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.token/eosio.token.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.token/eosio.token.abi
 ELIFECYCLE  Command failed with exit code 1.

 Lerna (powered by Nx)   Running target boot for project @coopenomics/boot failed

Failed tasks:

- @coopenomics/boot:boot

Hint: run the command with --verbose for more details.

 ELIFECYCLE  Command failed with exit code 1.
(booter.ts → cooperative.ts createPrograms) — именно так локальная dev-цепочка
получила 'blagorost' вместо 'capital' и воспроизвела баг, которого на проде
никогда не было.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:50:04 +00:00
coopops 5360daa471 fix(capital): correct BLAGOROST_AGREEMENT_TYPE to match on-chain 'blagorost'
Typecheck / desktop (pull_request) Failing after 8m53s
Typecheck / controller (pull_request) Has been cancelled
Константа хранила 'capital', но boot/src/init/cooperative.ts createPrograms()
сеет программу с type='blagorost' на генезисе (soviet::createprog), на всех
кооперативах. account.adapter.ts искал coagreement по 'capital' — не находил,
programId откатывался на 0, и вместо wallet::signagree уходил
soviet::sndagreement, который на цепи падает: get_coagreement_or_fail не
находит type='capital' в coagreements — «Соглашение указанного типа не
найдено». Ловилось при приёме платежа регистрации (registerBlockchainAccount
из gateway.interactor при PaymentType.REGISTRATION), падало для КАЖДОГО
участника, зарегистрированного по офферте Благорост.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:41:03 +00:00
coopops 652f7c722e fix(cooptypes): add missing space after colon before eoap_definition placeholder
Typecheck / desktop (pull_request) Failing after 8m50s
Typecheck / controller (pull_request) Successful in 13m45s
п.1.1 «а именно:{{ doc_data.eoap_definition }}» без пробела — во всех шести
шаблонах ЦПП (994/995/996/998/999/1000), фраза идентична во всех программах.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:46:00 +00:00
coopops a2b132b457 fix(registration): resolve doc_data_hash for extension offers via registry spec
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Шаблоны оферт ЦПП (#996/#1000) с c7824545cf требуют PrivateData, но общий
путь регистрации (RegistrationDocumentsService) не передавал doc_data_hash —
регистрация пайщика по GENERATION/CAPITALIZATION падала на генерации превью.

Ядро остаётся слепым к расширениям: AgreementRegistrationSpec получает
опциональный resolve_doc_data_hash, который расширение-владелец прикрепляет
к своим офертам при регистрации в реестре. Capital резолвит hash свежим
чтением своего конфига на каждую генерацию (совет может пересохранить
параметры без перезапуска). Гранулярность — per-оферта: при разделении
параметров по программам ядро менять не потребуется.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:41:57 +00:00
coopops a6cc71d6a3 fix(extension): unwrap TypeORM [rows, count] tuple in patchConfig UPDATE...RETURNING
TypeORM возвращает для UPDATE/DELETE кортеж [rows, affectedCount] (в отличие
от INSERT с голым rows-массивом) — без распаковки row оказывался кортежем и
все поля config читались как undefined, роняя buildState онбординга.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:41:45 +00:00
coopops b67581ce80 fix(extension): atomic jsonb config patch to stop onboarding flag lost-updates
Typecheck / desktop (pull_request) Failing after 22m0s
Typecheck / controller (pull_request) Successful in 13m37s
extensions.config is a single jsonb blob shared by every flag/hash the L1
onboarding wizards (capital, chairman) write to. Every write site did the
classic read-modify-write: findByName() the whole row, spread+mutate one key
in app memory, then update() the whole config back. Two DecisionTrackedEvent
handlers firing concurrently (two council decisions tracked near-simultaneously)
each read the same stale snapshot and each write their own flag back — whichever
write lands last wins and silently reverts the other one's flag to its old value.
Symptom hit live: onboarding_generator_program_template_done reverted to false
(with its hash still present, proving the decision really was tracked) after
onboarding_generation_contract_template_done raced it to true.

Added ExtensionDomainRepository.patchConfig(name, patch), implemented as a single
`UPDATE extensions SET config = config || patch::jsonb ... RETURNING *` — no
app-side read before the write, Postgres serializes concurrent UPDATEs on the
row so two different-key patches can no longer clobber each other. Replaced
every config read-modify-write in capital's and chairman's onboarding services
(the events handler's flag write, loadPlugin's init/expire timestamps, the
hash writes in completeStep/completeGeneralMeet, saveProgramDocDataHash) with
this atomic patch. update() is untouched for other callers.

Live data recovered manually: onboarding_generator_program_template_done was
patched back to true for voskhod/capital (hash was already present, confirming
the decision had in fact been tracked before the race reverted the flag).
2026-07-19 07:56:00 +00:00
coopops 024d46ff64 fix update 2026-07-19 07:55:49 +00:00
coopops 9030d28c6b fix(cooptypes): remove п.8.1 «Под Сайтом» from BlagorostProgramTemplate, renumber
Typecheck / desktop (pull_request) Failing after 9m37s
Typecheck / controller (pull_request) Successful in 14m3s
По решению: универсал-документ (ЦПП БЛАГОРОСТ_финал_универсал.docx.rtf,
~/blago/production/shared) — новый правильный эталон. В нём раздел 8 «Прочие условия»
содержит только 2 пункта (8.1 «Общество и Участник...», 8.2 «Изменения и дополнения...»),
без пункта про «Под Сайтом... прочие программные продукты» — ни в Положении, ни в
Оферте БЛАГОРОСТ (universal) такого пункта нет.

Убрал п.8.1 из rawContext, сдвинул 8.2→8.1, 8.3→8.2. Ссылка "(п. 7.1.)" в новом 8.2
не требует правки — форс-мажор остаётся разделом 7 независимо от сдвига в разделе 8.
Теперь 998 совпадает с универсалом точь-в-точь (сверено скриптом по номерам пунктов
и по тексту под каждым номером).
2026-07-19 07:31:02 +00:00
coopops 6e0e68c1e7 fix(cooptypes): correct cross-reference in GeneratorProgramTemplate п.12.3
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Systematic clause-numbering comparison against Положение_ЦПП_ГЕНЕРАТОР_универсал.docx.rtf
(~/blago/production/shared) found п.12.3 pointing at "(п. 12.1.)" — that's the "Под Сайтом"
definition clause, unrelated. The universal document (and the sentence's own subject —
"изменения условий ЦПП... возможность внесения которых прямо предусмотрена настоящим
Положением") both point to п.11.1, the форс-мажор/legislative-changes clause that actually
grants this right. Fixed the reference in rawContext only; left the orphaned translations
copy (994's dead legacy i18n block, unused by rendering) untouched.
2026-07-19 07:26:44 +00:00
coopops c079785019 fix(cooptypes): remove orphaned п.5.2 in BlagorostProgramTemplate, renumber section 5
Typecheck / desktop (pull_request) Failing after 9m2s
Typecheck / controller (pull_request) Successful in 13m50s
Сверил с ЦПП БЛАГОРОСТ_финал_универсал.docx.rtf (~/blago/production/shared) — в
актуальной версии Положения самостоятельный п.5.2 «Основной экономический эффект
функционирования Платформы достигается за счет членских взносов Участников при
расширении круга пользователей Платформы» отсутствует: его смысл слит в п.5.1
("...экономический эффект от функционирования которого заключается в следующем: ...").
Оферта (999/1000.Blagorost*, уже сверено) была такой всегда — 4.1 (источник) сразу
за которым 4.2 (дополнительный источник), без промежуточного пункта.

Убрал п.5.2 из rawContext, сдвинул 5.3–5.10 на 5.2–5.9, поправил внутреннюю ссылку
"указанными в п. 5.9." → "указанными в п. 5.8." в новом 5.9 (бывший 5.10). Дублирующий
текст в дохлом export const translations (не используется рендером, см. context) не трогал.
2026-07-18 18:55:46 +00:00
coopops bbc6453b24 fix(capital): await onSaved before advancing wizard past БЛАГОРОСТ step
Typecheck / desktop (pull_request) Failing after 8m49s
Typecheck / controller (pull_request) Successful in 13m45s
goNext() checked getFirstAvailableCouncilGroup() (which reads isDocParamsReady,
derived from onboardingState.capital_program_doc_data_hash) synchronously right
after saveParams() resolved. But saveParams() fired options.onSaved(hash) without
awaiting it, and the card's onSaved handler did `void handleDocParamsSaved()` —
an async refetch of onboarding state — so isDocParamsReady was still stale at the
moment goNext() decided which council group to jump to. getFirstAvailableCouncilGroup()
returned null, setWizardStepKey never ran, and the wizard just re-rendered the same
БЛАГОРОСТ step, looking like the just-saved params were never registered.

Fix: onSaved is now awaited inside saveParams(), and the card passes the
handleDocParamsSaved() promise through instead of firing it and forgetting it.
2026-07-18 18:27:57 +00:00
coopops b5eb0ea51f fix(capital): address PR #154 review — proper interface instead of as-cast, IInput['data'] canon
Typecheck / desktop (pull_request) Failing after 23m23s
Typecheck / controller (pull_request) Failing after 21m57s
document-domain.service.ts: replaced the inline `as Cooperative.Document.IGenerate & {...}`
hack with a dedicated GenerateDocumentWithPrivateDataDomainInterface for the doc_data
pre-generation branch (Cooperative.Document.IGenerate already carries a `[key: string]: any`
index signature, so the narrow interface is assignable without any cast at all).

Onboarding api/index.ts: saveProgramDocDataHash now takes
Mutations.Capital.SaveCapitalProgramDocDataHash.IInput['data'] and forwards it as-is to
variables.data, matching the SDK IInput['data'] canon used by completeStep right above it
instead of taking a bare string and rebuilding the payload inline. Updated both call sites.
2026-07-18 15:39:57 +00:00
coopops a072364d00 fix(capital): auto-generate CPP doc preview on step entry, restore section header formatting
Typecheck / desktop (pull_request) Failing after 20m30s
Typecheck / controller (pull_request) Successful in 12m40s
Preview for ГЕНЕРАТОР/БЛАГОРОСТ params no longer requires a manual button click on
first visit — onMounted now calls ensurePreview for the active wizard step. Also
fixes wizardStepOrder typing (was narrower than the runtime string values it holds).

Restores <h3> centered section-title markup in registry templates 994/995/996/998/999/1000
that was lost during the "parameterize CPP templates via private data" refactor, when
translations+context i18n structure was flattened into raw HTML.
2026-07-17 15:21:01 +00:00
coopops ed9b0879d5 chore(release): publish
Build bootstrap container / build (push) Successful in 2m42s
Release / release (push) Successful in 22m23s
Release / publish-packages (push) Successful in 14m53s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / trigger-mono-docs (push) Successful in 2s
2026-07-17 14:41:21 +00:00
coopops c94043e77f fix(docker): не скачивать бинарники electron/puppeteer в mono-base
Их postinstall тянет тяжёлые сетевые загрузки (Electron zip, Chromium),
не нужные ни для build (unbuild/tsc/vite), ни для runtime — периодически
валил release job TLS-таймаутом, из-за чего publish-packages (needs: release)
не запускался вообще.
2026-07-17 14:40:39 +00:00
coopops c1a8f3e637 fix(boot/Dockerfile): изолировать pnpm install от electron/puppeteer
Build bootstrap container / build (push) Successful in 2m37s
--filter не ограничивает lifecycle-скрипты общего lockfile store — тянулись
и падали по сети чужие тяжёлые постинсталлы (electron/puppeteer из
desktop/notifications), boot их не использует. --ignore-scripts + точечный
pnpm rebuild --filter решают.
2026-07-17 14:13:35 +00:00
coopops 15fdf35dd3 chore(release): publish
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
Build bootstrap container / build (push) Failing after 2m10s
Release / release (push) Failing after 15m13s
Release / publish-packages (push) Has been skipped
2026-07-17 14:02:54 +00:00
coopops 79b89695d1 chore(release): publish 2026-07-17 13:58:44 +00:00
coopops 7b27d0805c chore(release): publish
Release / release (push) Successful in 43m2s
Release / publish-packages (push) Has been cancelled
Release / trigger-coopenomics-docs (push) Has been cancelled
Release / trigger-mono-docs (push) Has been cancelled
Build bootstrap container / build (push) Successful in 46s
2026-07-17 12:54:54 +00:00
coopops e089dbd3fb ci(release): чинить publish-packages — генерировать controller/schema.gql перед publish
С 24 июня (минимум) publish-packages падает на каждом релизе main:
@coopenomics/sdk's prepublishOnly (pnpm run docs → generate-index-comments.ts)
читает components/controller/schema.gql, которого нет в чекауте и никто не
генерирует — ENOENT. lerna publish атомарный, поэтому НИ ОДИН пакет не
публикуется. Итог: main давно на v2026.7.9, а npm видел последний раз
2026.5.30-3 — полтора месяца фиксов (включая убранный kpp из
BankAccountDetailsInput, PR #81) не долетали до потребителей SDK.

Фикс: добавлен шаг generate-schema (изолированный, GraphQLSchemaBuilderModule,
без БД) ПОСЛЕ lerna run build — порядок важен, иначе ts-node падает на
устаревших dist соседних workspace-пакетов (проверено локально: cooptypes →
factory → inter → notifications, каждый был stale и валил компиляцию
controller'а по цепочке).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:54:33 +00:00
coopops 5230fab7c6 fix(capital): add blagorost_goal_reason field, stop reusing blagorost_goal_expansion in clause 2.1.1
Typecheck / desktop (pull_request) Failing after 9m28s
Typecheck / controller (pull_request) Successful in 14m6s
Clause 2.1.1 of БЛАГОРОСТ Положение/Оферта needs different text than the
intro paragraph, but both were wired to the same doc_data.blagorost_goal_expansion
variable (confirmed against the original "универсал" RTF sources), so 2.1.1
rendered with the wrong sentence in all three registries (998, 999, 1000).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 07:41:24 +00:00
Alex Ant 345aadbd6a update
Typecheck / desktop (pull_request) Failing after 8m25s
Typecheck / controller (pull_request) Successful in 14m12s
2026-07-14 22:31:19 +03:00
Alex Ant 33f1ac1a15 fix(capital): restore CPP templates from RTF sources and improve parameter constructor
Typecheck / desktop (pull_request) Failing after 18m32s
Typecheck / controller (pull_request) Failing after 6m52s
Align cooptypes/factory templates 994–1000 with original document wording,
correct doc_data substitution points, update voskhod backfill values, and
add onboarding UI with descriptive field labels and doc data hash saving.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 14:27:27 +03:00
Alex Ant 5de7e7c65b update 2026-07-13 20:53:21 +03:00
coopops bfbcd2d57f fix(desktop): починить «Продолжить» в оверлее выбора КУ
Кнопка снова submit формы из footer, BranchSelector синхронизирует selectedBranch, ошибки генерации документа показываются пользователю.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 16:02:19 +00:00
coopops ff5df68bce chore(release): publish
Release / publish-packages (push) Failing after 21m11s
Release / release (push) Successful in 1h35m7s
Release / trigger-coopenomics-docs (push) Successful in 3s
Release / trigger-mono-docs (push) Successful in 3s
Build bootstrap container / build (push) Successful in 10m29s
2026-07-09 05:49:14 +00:00
coopops a948f07a1b fix(desktop): закрепить кнопки оверлея выбора КУ на мобильных
Кнопки «Продолжить» и «подписать» вынесены в footer BaseDialog, чтобы они не уезжали за пределы экрана при прокрутке контента.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 05:48:16 +00:00
coopops 85bd044cd6 fix(desktop): перезагрузка списка КУ при показе оверлея выбора
Оверлей выбора кооперативного участка теперь подгружает список при переходе
в мажоритарный режим, показывает лоадер и блокирует «Продолжить» без выбора.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 20:46:55 +00:00
Alex Ant 45feef2676 fix(capital): correct CPP template mapping and onboarding widget canon
Typecheck / desktop (pull_request) Failing after 13m22s
Typecheck / controller (pull_request) Failing after 13m26s
Fix swapped doc_data placeholders in generator/blagorost templates, align the install widget with Base* components, and use one shared PrivateData hash in factory tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 21:49:30 +03:00
Alex Ant 02ce9400a2 fix(capital): remove unsafe any from CPP onboarding changes
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 11:05:20 +05:00
Alex Ant bfa2830650 feat(capital): collect CPP document parameters in onboarding
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 10:44:02 +05:00
Alex Ant c7824545cf feat(capital): parameterize CPP templates via private data 2026-06-28 09:50:03 +05:00
Alex Ant b9107e9ee9 chore(release): publish
Build bootstrap container / build (push) Successful in 1m45s
Release / release (push) Failing after 55m53s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-06-24 22:04:51 +05:00
Alex Ant 4d151befe0 fix(install): блокировать «Продолжить» до минимума совета, удаление без ограничений.
На prod требуется 3 человека (на dev — 1): кнопка и подсказка, состав редактируется свободно до перехода на следующий шаг.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 22:03:12 +05:00
Alex Ant eb16d85abc fix(install): разблокировать мастер при прерванной установке и валидировать состав совета по окружению.
Не показываем заглушку техобслуживания на /install, разрешаем повтор install из maintenance без vars, проверяем минимум членов совета до записи в цепь (3 на prod, 1 на dev).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:35:01 +05:00
ant ee5f7e1850 Merge pull request 'feat: выход из кооператива (выход пайщика, возврат паевого взноса)' (#137) from feat/membership-exit into dev
Reviewed-on: #137
2026-06-22 19:56:24 +00:00
Alex Ant 10bacaad13 refactor(membership-exit): обновление интерфейса и логики выхода из кооператива
Typecheck / desktop (pull_request) Failing after 25m29s
Typecheck / controller (pull_request) Successful in 27m23s
Изменены компоненты, связанные с выходом из кооператива. В меню заменены иконки и пути для поддержки и выхода. Обновлены метаданные и названия для кнопок и страниц, чтобы улучшить пользовательский интерфейс. В ExitButton добавлены параметры для настройки иконки и метки. Упрощена структура MembershipExitPage с акцентом на ключевые шаги выхода. Исправлены комментарии и типы для лучшего понимания кода.
2026-06-23 00:26:38 +05:00
coopops 1f68169b94 feat(exit): возврат при выходе — сет из 3 паевых кошельков (minshr+share+blago)
Typecheck / desktop (pull_request) Failing after 9m37s
Typecheck / controller (pull_request) Successful in 12m46s
confirmexit теперь обходит сет LEDGER2_EXIT_REFUND_WALLETS (w.reg.minshr +
w.wal.share + w.cap.blago): собирает доступные L3-балансы каждого (>0),
консолидирует на главный паевой (w.reg.minshr→o.reg.mvmin, w.cap.blago→
o.cap.wthcap) и ставит полную сумму на возврат единым платежом. Раньше
хардкодил только minshr+share — паевой в Благоросте оставался висеть.

Сет задан один раз в wallets.hpp (источник истины), генерируется gen:from-cpp
в cooptypes (LEDGER2_EXIT_REFUND_WALLETS / EXIT_REFUND_WALLET_NAMES) и обходится
backend-preview (getReturnPreview) — расчёт на фронте всегда совпадает с тем,
что реально вернёт контракт. Подпись «планируемая сумма / итог фиксирует Совет»
убрана из ExitButton и ExitOverlay (сумма теперь авторитетна).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:36:52 +00:00
coopops 95891b2319 fix(desktop): развёл иконки меню «Подключение» и «Реквизиты»
Typecheck / desktop (pull_request) Failing after 8m41s
Typecheck / controller (pull_request) Failing after 6m39s
Обе были 'fas fa-link' (одинаковые + FontAwesome запрещён каноном). На Material:
- Подключение → 'link'
- Реквизиты → 'account_balance' (банковские реквизиты)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:38:23 +00:00
coopops f41f1444b1 fix(desktop): реестр пайщиков — статус «Вышел из кооператива», дата вступления, без удаления
Typecheck / desktop (pull_request) Failing after 8m55s
Typecheck / controller (pull_request) Successful in 13m48s
- статус: вышедший пайщик (participant_account стёрт delpartcpnt + user_account
  blocked) больше не показывается «Активный пайщик» — добавлен терминал
  «Вышел из кооператива» по user_account.status
- дата вступления: фолбэк participant_account.created_at → user_account.registered_at
  (у вышедших пайщик-запись стёрта, дата больше не «отсутствует»)
- убрана кнопка удаления пайщика (таблица + карточка + диалог + машинерия):
  пайщика из реестра не удаляют

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 12:48:14 +00:00
coopops da450ec605 fix(desktop): «платёжка» → «чек об оплате» в UI реестра платежей
Typecheck / desktop (pull_request) Failing after 6m42s
Typecheck / controller (pull_request) Has been cancelled
Подсказки/лейблы переименованы под каноничную терминологию (чек об оплате —
подтверждающий документ, не «платёжка»):
- тултипы «Чек об оплате приложен / не приложен» (desktop + mobile)
- fileKindLabel PAYMENT_PROOF → «Чек об оплате»
- расчётный ПЛАТЁЖ возврата/доплаты (вместо «платёжка») в подсказках/алертах
  ReportExpenseAdvance + согласование рода
- комментарии приведены к новой терминологии (чек / расчётный платёж)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 12:16:31 +00:00
coopops 7fdfd8385a feat(gateway): ядровый «чек об оплате» исходящих платежей по payment_hash
Typecheck / desktop (pull_request) Failing after 8m31s
Typecheck / controller (pull_request) Successful in 14m26s
Культура денег: входящие — подтверждением, исходящие — чеком. Единый механизм
на уровне ядра (gateway), привязка по payment_hash — один на все исходящие
(возврат паевого/withdrawal/registration-refund/аванс расхода), переиспользуется
расширениями. Контроль мягкий: статус не трогаем, но зеркалим proof_count →
реестр рисует «чек приложен / не приложен».

Backend (core gateway):
- таблица payment_files + бакет gateway:files (@UseBucket), PaymentFilesService
  (upload/read-url/list/delete + зеркалирование proof_count в платёж)
- enum PaymentFileKind (PAYMENT_PROOF), DTO, резолвер uploadPaymentProof /
  paymentProofs / paymentFile; провайдеры в typeorm.module + gateway.module

SDK: selectors/mutations/queries gateway + zeus regen

Desktop:
- features/Payment/AttachPaymentProof — панель чека по payment-hash
- реестр: панель + индикатор «чек приложен» для ЛЮБОГО исходящего (PAID/COMPLETED)
- конвергенция expense: чек ушёл в ядро, AttachExpenseProof оставляет только
  закрывающие документы (DIRECT)

Терминология: «чек об оплате» (не «платёжка»/«первичка»).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 09:17:09 +00:00
coopops 39433753c2 fix(membership-exit): терминальный экран после выхода + гард повторного выхода
Typecheck / desktop (pull_request) Failing after 8m47s
Typecheck / controller (pull_request) Has been cancelled
После completexit строка registrator::exits стирается (терминал=erase) →
getMembershipExit возвращал null → оверлей пропадал, кабинет разблокировался,
пайщик видел стол и мог подать выход повторно (гарда не было).

- enum MembershipExitStatus += COMPLETED
- getMembershipExit: терминальная фаза по blocked-аккаунту + персистентному
  MEMBERSHIP_EXIT-платежу (сумма возврата + статус 'Оплачено')
- createMembershipExit: гард — заблокированный аккаунт не может выйти повторно
  (контракт уже блокирует через get_participant_or_fail; отсекаем раньше)
- ExitOverlay: терминальная фаза 'Вы вышли из кооператива' (сумма + 'Оплачено'
  + ожидайте поступления), она же UX-гард — перекрывает кабинет
- SDK Zeus regen под новый enum

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:57:54 +00:00
coopops 560321853c Merge remote-tracking branch 'origin/dev' into feat/membership-exit
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
2026-06-16 08:42:23 +00:00
ant f42b703098 Merge pull request 'ci(release): независимый деплой окружений (testnet/main разным темпом)' (#150) from ci/independent-env-deploy into dev
Reviewed-on: #150
2026-06-16 08:17:55 +00:00
ant d7e08785eb Merge pull request 'fix(capital): регистрация долей Благороста при создании проекта по событию (не ждать крон)' (#151) from fix/capital-share-onproject-event into dev
Reviewed-on: #151
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-16 08:15:29 +00:00
coopops 7efd19f1bd chore(release): publish
Build bootstrap container / build (push) Has been cancelled
Release / publish-packages (push) Failing after 8m37s
Release / release (push) Successful in 27m21s
Release / trigger-coopenomics-docs (push) Successful in 1s
Release / trigger-mono-docs (push) Successful in 2s
2026-06-16 08:11:23 +00:00
coopops cce099748a fix(capital/state): program_expense_* в хвост через binary_extension — фикс нечитаемой таблицы state на проде
Инцидент 2026-06-16: коммит d02c0ce (18 мая) добавил program_expense_pool и
program_expense_reserved в global_state ПЕРЕД полем config напрямую, без
binary_extension. После деплоя на прод таблица state перестала читаться:
запись сериализована старым layout (config сразу после
program_membership_cumulative_reward_per_share), а новый ABI ждёт два asset
перед config → unpack натыкается на байты config (get table → "Invalid
symbol ...Y@" = double 100.0 = config.expense_pool_percent). Любой action,
читающий global_state, падал.

Фикс: оба поля перенесены в ХВОСТ struct (после config) и обёрнуты в
eosio::binary_extension<asset>. Старая запись прода читается без изменений
(хвостовые extension опциональны → пусто = 0), новая логика программных
расходов сохранена. Поля в таблицу EOSIO дописываются только в конец и только
binary_extension'ом.

utility-функции State:: (topup/reserve/release/consume/spend) переведены на
optional-семантику через ext_or_zero(); инвариант — оба поля материализуются
вместе, чтобы порядок хвостовых extension был консистентным. Прямого доступа к
полям вне State:: нет. Собрано в CDT-докере: capital.wasm слинкован, в ABI
program_expense_* → asset$ после config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:06:16 +00:00
coopops 3d04f76446 fix(capital): заводить доли только в active-проектах, не в pending
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
По решению пользователя заход долей Благороста — только в активные проекты.
В pending больше не заводим: при переходе pending→active придёт новая дельта
capital::projects со status=active, на ней листенер и зарегистрирует.

- Листенер ProgramShareRegistrationOnProjectDeltaListener: гейт pending|active → только active.
- ProgramShareRegistrationService.findActiveProjects (путь крона и wallet-листенера): фильтр pending|active → только active.
- Тест: pending теперь НЕ реагирует; active — позитивный кейс.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:04:08 +00:00
coopops 532e34c53b ci(release): promote по умолчанию шлёт текущий HEAD (не последний тег)
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Дефолт promote.sh — текущий HEAD чекаута; второй аргумент по-прежнему задаёт явный
ref (ветка/тег/sha). Типичный путь: cut на dev → promote testnet → promote main,
все три от того же HEAD. testnet/main остаются независимыми FF-указателями.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 07:45:04 +00:00
coopops a0e0c9310c fix(desktop): confirm-выхода — auth-aware навигация вместо молчаливого бойнса на логин
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Ссылка из письма открывается часто без сессии кабинета (другой браузер/инкогнито/
после logout). Подтверждение работает по токену без входа, но wallet защищён —
гард молча редиректил на login-redirect, и пайщик не понимал, что произошло.

- есть сессия → 'Перейти в кабинет' → wallet (глобальный ExitOverlay сам покажет
  'на рассмотрении Совета')
- нет сессии → 'Войти в кабинет' → signin + пояснение, что войти нужно для слежения
  за статусом и суммой возврата

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 07:40:02 +00:00
coopops 0d51973a7b fix(capital): регистрировать доли Благороста при создании проекта по событию, без ожидания крона
Typecheck / desktop (pull_request) Successful in 13m55s
Typecheck / controller (pull_request) Has been cancelled
Доли пайщиков Благороста регистрировались в проект только периодическим
scheduler'ом (regshare) и точечным листенером на дельту кошелька. Новый
проект баланс не меняет → wallet-листенер молчит, и наполнение нового
проекта зависело ТОЛЬКО от крона. Если компонент быстро прогнали
pending→…→result до тика крона, пайщики в него не попадали, а откат
result→active контракт не допускает — доли в компоненте терялись
безвозвратно (voskhod, компонент 011bcd92…, 2026-06-16).

- Новый ProgramShareRegistrationOnProjectDeltaListener на
  delta::capital::projects: при появлении проекта в статусе pending|active
  сразу регистрирует доли всех активных пайщиков (неблокирующе, fire-and-forget,
  переиспользуя ядро syncContributor — тот же путь, что и крон).
- ProgramShareRegistrationService.syncProgramSharesForProject — обход пайщиков
  по одному проекту; syncContributor переведён с ProjectDomainEntity[] на
  projectHashes: string[].
- Крон ОСТАВЛЕН как reconciliation-бэкстоп: он ещё и ДОобновляет уже
  зарегистрированные доли при дрейфе баланса (контракт upsert_contributor_segment
  это допускает) и подбирает события, потерянные при downtime контроллера.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 07:30:10 +00:00
coopops 9c2729d243 ci(release): независимый деплой окружений — testnet/main как FF-указатели на теги dev
Typecheck / desktop (pull_request) Successful in 13m58s
Typecheck / controller (pull_request) Successful in 13m15s
Паровоз dev→testnet→main заменён независимыми указателями: каждое окружение —
самостоятельный fast-forward-указатель на выбранный релизный тег dev. Прод и тест
катятся разным темпом и на разные версии одновременно, без диверджа и конфликтов
(оба — FF-указатели на линейную историю dev).

- promote.sh <testnet|main> [ref]: FF выбранного тега (по умолчанию — последний на
  origin/dev) на ветку окружения; main больше не зависит от testnet; только вперёд.
- release.yaml: workflow_dispatch получил inputs environment+ref — откат на старую
  версию / редеплой без бампа / hotfix в один контур (то, что FF не умеет). Сборка
  идёт из вычекнутого ref, окружение — из inputs.environment. Авто-триггер по
  изменению lerna.json сохранён.
- RELEASE.md: модель и инварианты переписаны под независимые указатели.

Принципы деплоя сохранены: бамп версии один раз на dev, образы по версии,
BUILD_MODE по окружению, webhooks per env, npm publish/доки только на main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 06:49:12 +00:00
coopops 00e15be65e fix(desktop): убрал '*/' из SCSS-комментария — ломал sass-парсер
Typecheck / desktop (pull_request) Failing after 8m29s
Typecheck / controller (pull_request) Successful in 14m13s
'text-h*/inline' досрочно закрывал блочный комментарий → expected '{'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 06:31:10 +00:00
coopops a349cfcf6a fix(desktop): диалог выхода — придавил заголовок заявления к канону + soft-панель итога
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
- заголовок документа из DocumentHtmlReader (3rem) прижат к h2 по канону,
  :deep + !important как в ReadStatement.vue
- 'Сумма к возврату' оформлена soft-панелью (label+hint слева, mono-значение
  справа) вместо висящего снизу текста

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 06:26:03 +00:00
coopops 07ec6d9a5b chore(release): publish
Build bootstrap container / build (push) Failing after 1m27s
Release / release (push) Failing after 16m43s
Release / trigger-mono-docs (push) Has been skipped
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
2026-06-16 06:02:31 +00:00
coopops 102eeb319e fix(desktop): суммы выхода через formatAsset2Digits + одна сумма без разбивки
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
- Суммы к возврату форматируются канонной formatAsset2Digits ("300.0000 RUB"
  → "300,00 RUB") в диалоге заявления и в оверлее — вместо сырого asset.
- Убрана разбивка "целевой + минимальный" (оба паевые, суммируются): показываем
  одну строку "Сумма к возврату".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 05:59:14 +00:00
coopops 2f1e078fe2 fix(desktop): один лоадер в диалоге заявления о выходе
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Было две крутилки рядом («Формирование заявления…» + «Расчёт суммы…»).
Теперь заявление и сумма грузятся параллельно (Promise.allSettled) под одним
центрированным лоадером и показываются вместе. Тот же стиль лоадера — на фазе
проверки реквизитов. Сумма некритична: документ подписывается и без предрасчёта.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 05:55:22 +00:00
coopops 554798b74b fix(desktop): пункт «Выход из кооператива» в конец меню + надёжный гейт реквизитов
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
- Пункт меню «Выход из кооператива» перенесён в самый низ (после «Поддержки»),
  иконка group_remove вместо logout — чтобы не сливаться с кнопкой «Выйти»
  (выход из кабинета, дверь-logout) внизу панели.
- Гейт реквизитов сделан fail-open: блокируем выход ТОЛЬКО при достоверно
  пустом списке методов; null/непонятный ответ не блокирует. + реактивный
  фолбэк: если бэкенд отклонил подачу из-за отсутствия реквизитов — диалог
  переключается на баннер. Авторитетный источник — бэкенд (createMembershipExit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 05:44:34 +00:00
ant 0ae8702ade Merge pull request 'fix(registration): pending-счёт не показывать как «платёж принят»' (#149) from fix/registration-pending-display into dev
Build bootstrap container / build (push) Has been cancelled
Reviewed-on: #149
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-16 05:36:38 +00:00
coopops 2d71706840 fix(registration): pending-счёт не считать «оплачено» — вести на оплату, а не на ожидание
Typecheck / desktop (pull_request) Successful in 13m50s
Typecheck / controller (pull_request) Has been cancelled
Счёт регистрационного взноса создаётся со статусом pending сразу при заходе на
шаг оплаты («счёт выставлен», деньги ещё не получены). При возврате на страницу
(перезагрузка/повторный вход) роутинг считал сам факт наличия платежа за «этап
оплаты пройден» и вёл на экран ожидания, где дефолтная ветка показывала «Ваш
платеж принят» — хотя оплаты не было и статус не менялся.

- SignUp: на экран ожидания/отказа ведём только при PAID/COMPLETED либо
  терминальном статусе (отказ/отмена/возврат); pending → шаг оплаты (QR + поллинг).
- WaitingRegistration: для pending — отдельная ветка «Ожидаем поступление оплаты»
  + кнопка «Перейти к оплате»; «платёж принят» остаётся только для PAID/COMPLETED.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 05:29:50 +00:00
coopops b8568eff4c feat(desktop): статус платежа в оверлее выхода + страница заявления по канону
Typecheck / desktop (pull_request) Failing after 8m24s
Typecheck / controller (pull_request) Has been cancelled
- ExitOverlay: чип статуса исходящего платежа у суммы (ожидает оплаты/оплачивается/
  оплачено/ошибка) — берётся из payment_status, меняется по мере обработки кассиром.
- MembershipExitPage переверстана на AuthCard (иконка-предупреждение + заголовок +
  ключевые шаги процедуры + запуск подачи) — единый канон с оверлеем.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 05:28:16 +00:00
coopops 448eb1de80 feat(membership-exit): статус исходящего платежа в getMembershipExit
getMembershipExit подтягивает платёж возврата по hash=exit_hash и отдаёт его
payment_status (PaymentStatus enum, nullable). Фронт показывает реальный статус
платежа кассира (ожидает оплаты → оплачено), а не только статус процесса выхода.
Сумма к возврату — зафиксированная советом on-chain (= сумма платежа), не preview.

+ codegen (controller/sdk zeus, селектор MembershipExit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 05:28:16 +00:00
coopops 8fae69d979 feat(desktop): экран выхода — собран в карточку AuthCard
Typecheck / desktop (pull_request) Failing after 8m25s
Typecheck / controller (pull_request) Has been cancelled
Контент больше не плавает по пустому экрану: статус выхода — в центрированной
карточке AuthCard (accent-стрип + мягкая тень, канонный hero-контейнер).
Иконка статуса в soft-плитке (primary/pos), заголовок, пояснение, сумма к
возврату на surface-2, действия в футере с разделителем. Всё на токенах --p-*.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 05:12:55 +00:00
coopops 1b1b8b3246 feat(desktop): экран выхода по канону + кнопка выхода из личного кабинета
Typecheck / desktop (pull_request) Failing after 6m51s
Typecheck / controller (pull_request) Has been cancelled
- ExitOverlay переверстан на канонный EmptyState (иконка-плитка + заголовок +
  приглушённый текст + слот действий) вместо россыпи text-h5/64px-иконки.
- Добавлена кнопка «Выйти из личного кабинета» (logout) на оба экрана выхода:
  при активном выходе аккаунт заблокирован, и выйти из сессии было нечем.
  После logout gate обнуляется, редирект на signin; при повторном входе экран
  выхода снова показывается (статус живёт on-chain).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 05:05:26 +00:00
coopops 72a8a3d0c3 feat(desktop): гейт реквизитов в выходе из кооператива
Typecheck / desktop (pull_request) Failing after 8m35s
Typecheck / controller (pull_request) Successful in 13m35s
Перед подписанием заявления о выходе проверяем наличие реквизитов пайщика
(getPaymentMethods). Если их нет — вместо формы показываем баннер «установите
реквизиты для получения возврата паевого взноса» + кнопку перехода на страницу
реквизитов (payment-methods). Бэкенд проверяет то же при подаче (страховка).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:17:46 +00:00
coopops 56cd422440 feat(membership-exit): исходящий платёж возврата паевого + гейт реквизитов
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Закрываем пробел: при одобрении советом выхода (on-chain confirmexit) кассир
не видел платёж возврата — его никто не создавал в реестре gateway.

- Гейт реквизитов: createMembershipExit отклоняет подачу, если у пайщика нет
  ни одного платёжного метода («установите реквизиты для возврата паевого»).
- MembershipExitAuthorizationListener (@OnEvent action::registrator::confirmexit):
  по exit_hash берёт из таблицы exits username + сумму возврата, заводит
  исходящий платёж MEMBERSHIP_EXIT (PENDING — совет одобрил, сразу кассиру) по
  реквизитам пайщика (метод по умолчанию). hash платежа = exit_hash, поэтому
  подтверждение кассой через default-ветку processOutgoingPayment вызовет
  completeOutcome → registrator::completexit (списание + блокировка).
- getExitByHash в account-порт/адаптер: confirmexit отдаёт только coopname+exit_hash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:13:38 +00:00
coopops 949b5139af chore(codegen): regen Zeus после merge — membership-exit + expense в одной схеме
После merge dev zeus-клиенты были взяты из dev (без операций выхода).
Интроспекция живого coopback → schema.gql → generate-client вернула обе
группы операций (createMembershipExit/confirm/cancel + expense-шасси).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:13:38 +00:00
coopops 50d5d71c8d Merge branch 'dev' into feat/membership-exit
Втягиваем инфраструктуру исходящих платежей (шасси расходов C28) для
создания возврата паевого взноса в gateway при выходе пайщика.

Конфликты (объединены обе стороны):
- gateway payment-type.enum / payment-domain.entity: MEMBERSHIP_EXIT + EXPENSE*
- zeus-клиенты (controller, sdk): взята версия dev, регенерация следом

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:30:43 +00:00
coopops 8d9bc5f592 chore(codegen): controller Zeus-клиент — типы MembershipExit
Typecheck / desktop (pull_request) Successful in 13m43s
Typecheck / controller (pull_request) Successful in 13m28s
Регенерация Zeus-клиента контроллера под GraphQL-операции выхода
(CreateMembershipExitInput, MembershipExitStatus, signed-document inputs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:27:22 +00:00
coopops 82ea0a4c03 fix(desktop): страница подтверждения выхода — без requiresAuth
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Маршрут membership-exit/confirm авторизуется токеном из ссылки (мутация
confirmMembershipExit публичная), поэтому навигационный гард не должен
редиректить его на login-redirect. requiresAuth:false исключает страницу из
auth-гейта — ссылку из письма можно открыть без активной сессии.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:00:16 +00:00
coopops b728bc6295 fix(membership-exit): письмо подтверждения — неблокирующая отправка + лог ссылки
Typecheck / desktop (pull_request) Successful in 13m37s
Typecheck / controller (pull_request) Has been cancelled
Черновик заявления сохраняется до отправки письма; если провайдер писем
недоступен, приём заявления не падает (try/catch + error-лог), пайщик может
отменить. Ссылка подтверждения пишется в debug-лог для диагностики.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:20:39 +00:00
coopops 59585dc4ea feat(desktop): выход из кооператива с email-подтверждением — UI
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
- ExitButton: диалог теперь показывает сгенерированное заявление (DocumentHtmlReader)
  с предупреждением «читайте внимательно, необратимо» + сумма к возврату, затем
  «Подписать и подать» (generateApplication → показ → submitSignedApplication).
- ExitOverlay: новое состояние AWAITING_CONFIRMATION — «пройдите по ссылке из письма»
  + кнопка «Отменить выход»; после перехода/отмены overlay переключается на
  ончейн-статус (рассмотрение Совета → одобрено → выплаты).
- useExitGate: isAwaitingConfirmation + cancelExit (Mutations.MembershipExit.CancelMembershipExit).
- model: generateApplication / submitSignedApplication / confirmExit вместо единого processMembershipExit.
- MembershipExitConfirmPage + маршрут membership-exit/confirm (цель ссылки из письма):
  onMounted confirmExit(token) → loadExitStatus.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:19:30 +00:00
coopops 6bba3c7b76 feat(sdk): confirmMembershipExit/cancelMembershipExit + MembershipExitResult.status
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Регенерация Zeus-клиента (интроспекция живого coopback) + ручные обёртки:
- Mutations.MembershipExit.ConfirmMembershipExit (token → MembershipExitResult)
- Mutations.MembershipExit.CancelMembershipExit (coopname/username → Boolean)
- membershipExitResultSelector += status (enum += AWAITING_CONFIRMATION)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:12:59 +00:00
coopops 9ee57d1e05 feat(membership-exit): email-подтверждение выхода — backend + письмо
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Двойное подтверждение необратимого выхода (закрытия аккаунта): заявление
подписывается и принимается сразу, но в блокчейн уходит только после перехода
по ссылке из письма (по аналогии с verify-email/reset-key токенами).

- notifications: воркфлоу MembershipExitConfirmation (письмо со ссылкой confirmationUrl).
- token: тип CONFIRM_EXIT + generateConfirmExitToken (домен + application).
- entity membership_exit_requests (off-chain черновик подписанного заявления; uniq coopname+username; удаляется при confirm/cancel).
- MembershipExitStatus += AWAITING_CONFIRMATION; MembershipExitResult += status.
- service: createMembershipExit теперь сохраняет черновик + шлёт письмо (НЕ цепь);
  confirmMembershipExit(token) проверяет токен и шлёт exitcoop в цепь;
  cancelMembershipExit удаляет черновик+токен; getMembershipExit отдаёт off-chain фазу.
- resolver: + confirmMembershipExit (публичная, по токену) + cancelMembershipExit (auth+владелец).

Проверено вживую: мутации в схеме, enum/Result обновлены, таблица создана (synchronize).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:08:03 +00:00
coopops 0c995b1bca fix(membership-exit): заявление о выходе (200) — только цифровая подпись, без шапки утверждения
Typecheck / desktop (pull_request) Successful in 13m59s
Typecheck / controller (pull_request) Has been cancelled
Заявление о выходе из кооператива подписывается цифровой подписью (signatures[]),
поэтому из документа убрано поле собственноручной подписи (рукописная картинка):
- cooptypes 200: убраны Action.signature/Model.signature, <img src="{{ signature }}">,
  signature из exampleData;
- factory Actions/200: убрана логика поиска/сохранения подписи в mongo;
  Templates/200: убрано свойство signature из AJV-схемы;
- controller DTO: убрано поле signature (@IsString) из меты заявления.
  Это и вызывало "signature must be a string" — ValidationPipe (422, не логируется)
  рубил генерацию, т.к. фронт картинку не передаёт.

Также откат самодуманной шапки "ФОРМА УТВЕРЖДЕНА решением Собрания Совета (Протокол №)":
исходная форма заявления её не содержит. Убраны vars.participant_exit_application
(cooptypes IVars + factory VarsSchema + фикстура теста), проверка протокола в фабрике
и переводы APPROVED/approved_by_council/protocol.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 14:47:30 +00:00
ant 7ab0c31878 Merge pull request '[C28-28..C28-34] feat: шасси системы расходов (MVP-SINGLE) + интеграция Капитала через inter-порт' (#61) from feat/expense-chassis-mvp into dev
Build bootstrap container / build (push) Successful in 3m51s
Reviewed-on: #61
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-15 13:11:52 +00:00
coopops c200a834eb [C28-67][@ant] fix: spec адаптера расходов — убраны стейл-кейсы authExp/declineExp
Typecheck / desktop (pull_request) Successful in 14m23s
Typecheck / controller (pull_request) Successful in 13m43s
Контроллерный tsc --noEmit (CI) падал: spec тестировал adapter.authExp /
adapter.declineExp, которых нет в ExpensesBlockchainAdapter по дизайну.
authexp/declexp исполняет контракт soviet как callbacks решения совета —
backend-адаптер несёт только 6 прямых actions (createexp/payexp/reportexp/
returnexp/overspendexp/closeexp). Зафиксировано в port, mutations-service и
комментарии адаптера. Прав код — приводим тест к дизайну, удаляя два кейса.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:04:28 +00:00
coopops 56121c2346 [C28-66][@ant] fix: type-errors шасси расходов — vue-tsc в CI
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
CI впервые дошёл до vue-tsc по ветке и вскрыл накопленный долг типов
(локально vue-tsc не гоняем). Чиним точечно, без смены логики:

- uploaded_at — Zeus-скаляр даты типизирован как {}: new Date(String(...))
  и date:String(...) в ProgramExpensePage, ExpenseDetailPage и трёх
  Payment-panel (Attach/Settlement/Report).
- ExpenseDetailPage: loadPayments options требует sortOrder → 'DESC';
  :key платежа допускает null → ключ pay.hash ?? idx.
- PaymentsPage: routeUsername null → undefined под prop :username.
- expenses/model: снят реэкспорт несуществующего IAuthorizeProposalDraft.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 11:53:27 +00:00
ant cabc589ce8 Merge pull request 'fix(desktop): UTF-8 (bit-11) в именах файлов zip-пакета документов' (#147) from fix/zip-utf8-flag into dev
Reviewed-on: #147
2026-06-15 11:38:01 +00:00
coopops c3086277f4 fix(desktop): zip-пакет документов — флаг UTF-8 (bit-11) в именах файлов
Typecheck / desktop (pull_request) Successful in 13m56s
Typecheck / controller (pull_request) Successful in 13m22s
buildZipArchive писал имена в UTF-8, но general-purpose bit flag = 0 → читатели
(включая верификатор на fflate) декодировали кириллицу как latin1 (мохибейк), и
manifest.json (корректный UTF-8) не находил записи → верификатор «в пакете нет
файлов». Ставим bit-11 в local и central заголовках. Архив теперь корректно
открывается любым ZIP-ридером (Explorer/macOS/fflate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:58:55 +00:00
coopops 3064e4b7ae [C28-65][@ant] refactor: платежи расхода — один запрос по proposal_hash
Typecheck / desktop (pull_request) Failing after 8m27s
Typecheck / controller (pull_request) Failing after 8m22s
ExpenseDetailPage переведён со сбора платежей по реконструированным хэшам на
один запрос getPayments({ coopname, proposal_hash }) — фильтр из C28-64. Убрана
скопированная серверная формула settlementPaymentHash/sha256Hex из
shared/lib/expenses: фронт больше не дублирует деривацию хэшей расчётных
платёжек. Запрос type-agnostic — ловит все связанные платежи любого типа,
устойчив к добавлению новых видов. Сортировка: выдача/оплата раньше расчётных.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:17:16 +00:00
coopops 435634bfd9 [C28-64][@ant] feat: фильтр платежей по proposal_hash в gateway + regen SDK
Добавлен фильтр PaymentFiltersInput.proposal_hash — возвращает все платежи,
связанные с расходом (служебной запиской). Связь платёж→расход уже зашита
бэкендом в json-поле blockchain_data.proposal_hash (и у платежа выдачи аванса/
оплаты организации, и у расчётных платёжек возврата/доплаты); фильтр извлекает
его через json-оператор ->> в typeorm-репозитории. Так связанные платежи
достаются одним запросом по расходу, без реконструкции хэшей на фронте.

regen: schema → generate-client → sdk build (zeus-клиент controller+sdk).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:17:06 +00:00
coopops 0203bf2405 [C28-63][@ant] feat: движения платежей в «Истории состояний» расхода
Typecheck / desktop (pull_request) Failing after 8m34s
Typecheck / controller (pull_request) Has been cancelled
В историю состояний расхода добавлены события платежей: выдача аванса/оплата
организации, возврат недорасхода, доплата перерасхода, отклонения. Берутся из тех
же linkedPayments (C28-61), без отдельного запроса. Тип/иконка/текст по статусу
(исполнен / оплачен кассой / создан / отклонён), сумма и причина отказа — в
описании; сортируются в общую ленту по дате updated_at.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 07:54:19 +00:00
coopops 91cfc0f0af [C28-62][@ant] feat: кнопка «Назад» (к реестру расходов) в детали расхода
Из детали расхода нельзя было вернуться в реестр расходов. Добавлен canon
back-link под шапкой (как в MeetDetails/DocumentDetails): router.push на
expenses-registry. Виден всегда, даже пока расход грузится.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 07:52:28 +00:00
coopops 882646d6aa [C28-61][@ant] feat: платежи по расходу в деталях СЗ + переход в реестр платежей
Typecheck / desktop (pull_request) Failing after 8m23s
Typecheck / controller (pull_request) Has been cancelled
В детали расхода (стол совета → реестр расходов → расход) добавлена секция
«Платежи по расходу»: выдача аванса/оплата организации и, если был перерасчёт по
чекам, расчётная платёжка (возврат недорасхода / доплата перерасхода). Реквизиты
«куда уходил платёж», назначение и причина отклонения — внутри PaymentDetails.

Платежи собираются по ДЕТЕРМИНИРОВАННЫМ хэшам, а не по username: у позиции-
организации платёж принадлежит кооперативу, у аванса — пайщику-получателю, и у
каждой позиции свой получатель — единого владельца нет. Хэш выдачи = item_hash,
расчётные = sha256('expense-settlement:coop:item:kind') (новый settlementPaymentHash
в shared/lib/expenses, точная копия серверного generateHashFromString). Точечный
hash-фильтр gateway не требует листать общий реестр и нового бэкенд-поля.

Клик «Открыть в реестре платежей» → реестр кассира, отфильтрованный по владельцу
(:username? в PaymentsPage), с фокусом на платёж (виджет раскрывает его по ?focus=hash).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 07:25:26 +00:00
coopops 9d830e7b91 [C28-60][@ant] fix: показывать причину отклонения платежа (CANCELLED)
Кассир пишет причину в диалоге «Отклонить», бэкенд сохраняет её в payment.message
и ставит статус CANCELLED — но PaymentDetails показывал message только при FAILED,
поэтому причина нигде не отображалась. Добавлен canon-баннер (.banner--neg) с
причиной для CANCELLED: видно сразу при раскрытии платежа. Компонент общий —
работает и на столе совета, и на столе пайщика.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 07:25:06 +00:00
coopops 4453dd9cfd [C28-59][@ant] feat: кликабельная ссылка «Основание» → раскрыть платёж выдачи аванса
Typecheck / desktop (pull_request) Failing after 8m29s
Typecheck / controller (pull_request) Failing after 7m58s
В блоке «Основание расчёта» (C28-58) добавлена кнопка «Открыть платёж выдачи аванса»:
кассир жмёт → реестр раскрывает исходный платёж-аванс (его hash == item_hash расчётной
платёжки) и прокручивает к нему. Видно, сколько выдавалось и что в чеке, без поиска
строки того же пайщика вручную. id навешены на tr/pay-card для scrollIntoView; если
платёж аванса не на текущей странице — подсказка.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:37:59 +00:00
coopops e0202b49dd [C28-58][@ant] feat: блок «Основание расчёта» у settlement-платёжки в реестре
Typecheck / desktop (pull_request) Failing after 8m25s
Typecheck / controller (pull_request) Has been cancelled
Кассир (и пайщик) при раскрытии расчётной платёжки (возврат недорасхода / доплата
перерасхода) видит основание прямо в строке — без поиска исходного аванса среди
сотен строк реестра:
- ссылка на СЗ (№), что оплачивали, выдано авансом, заявлено по чекам, сумма расчёта
- список подтверждающих документов (чеки REPORT_FILE) со ссылками на открытие

Новый feature ExpenseSettlementBasis (self-contained: грузит позицию СЗ + файлы по
proposal_hash/item_hash из blockchain_data платёжки). Подключён в ListOfPaymentsWidget
для EXPENSE_RETURN/EXPENSE_OVERSPEND на обоих столах (desktop+mobile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:26:23 +00:00
coopops ca9fd8f8de [C28-57][@ant] fix: запретить повторный отчёт по авансу + показывать заявленный факт
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Корень: позиция on-chain остаётся PAID до подтверждения расчётной платёжки кассой
(reportexp отложен), поэтому отчёт можно было подать многократно на разные суммы.
Идемпотентность createSettlementPayment — лишь по виду (возврат/доплата), из-за чего
отчёт 400→200 плодил две платёжки, а 500 молча возвращал старую.

Backend (авторитетный фикс):
- reportExpenseItem: guard по report_state платежа аванса — SETTLEMENT_PENDING/CLOSED
  → BadRequestException «Отчёт по этой позиции уже подан»
- markAdvanceReportState сохраняет reported_amount (заявленный факт) рядом со статусом
- тест на guard + обновлён ассерт SETTLEMENT_PENDING

Frontend:
- ReportExpenseAdvancePanel: новые props report-state/reported-amount; форма
  (AmountInput+кнопка) доступна только пока отчёт не подан (canReport); после подачи —
  «Отчёт подан на N ₽, ждём расчёт», остаётся лишь загрузка доп.документов
- ListOfPaymentsWidget: прокидывает report_state/reported_amount в панели на обоих столах

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:21:50 +00:00
coopops f2489fc592 [C28-56][@ant] fix: сузить поле суммы в отчёте пайщика + поправить назначение возврата
Typecheck / desktop (pull_request) Failing after 8m18s
Typecheck / controller (pull_request) Failing after 7m58s
- ReportExpenseAdvancePanel: AmountInput в .report-advance__amount (max-width 240px),
  чтобы значение не висело во всю ширину панели с разрывом «лейбл … сумма»
- memo возврата: «выданных под аванс под отчёт» → «выданных авансом под отчёт»

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 05:45:21 +00:00
coopops 9ac810f34a [C28-55][@ant] fix: вернуть «Направление» в реестр (перспектива стола) + универсальные уведомления о платеже
Typecheck / desktop (pull_request) Failing after 8m33s
Typecheck / controller (pull_request) Failing after 8m0s
Направление платежа (ListOfPaymentsWidget):
- колонка «Направление» снова показывается на ОБОИХ столах;
- on-chain direction всегда относительно кооператива (INCOMING = в кооператив).
  Стол совета (!hideActions) так и видит; на личном столе пайщика (hideActions)
  перспектива обратная — direction инвертируется (исходящий из кооператива =
  поступление пайщику). Хелперы displayDirection/directionLabel/directionHint,
  tooltip с пояснением («В кооператив»/«Из кооператива» vs «Поступление вам»/
  «Списание с вас»). colspan/skeleton/min-width поправлены под доп. колонку.

Уведомления о платеже — без привязки к типу:
- workflow payment-refunded (любой исходящий PAID пайщику) больше не «Возврат
  взноса выполнен», а универсальное «Платёж выполнен» (тем же каналом идут
  аванс под отчёт, доплата по перерасходу и пр. — тип платежа неизвестен).
  payment-paid уже был универсальным («Платёж принят» для входящего).
- комментарий в payment-notification.service приведён в соответствие.

ESLint ✓, notifications build ✓ (id platyozh-vypolnen резолвится в каталоге).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 05:13:00 +00:00
coopops 5c14bdb0a4 [C28-54][@ant] fix(desktop): пул расходов = баланс реального кошелька + загрузка доп. документов после отчёта
Typecheck / desktop (pull_request) Failing after 8m26s
Typecheck / controller (pull_request) Failing after 7m59s
Q1 (расхождение 900 vs 50): на странице программных расходов «Доступно»
теперь = остаток кошелька w.cap.pgexp (source_wallet, с которого payexp
реально списывает оплату), а не счётчик state.program_expense_pool. Счётчик
двигался на резерве, кошелёк — на оплате, поэтому кассир видел одну сумму, а
оплата обламывалась на другой. Карточка «Зарезервировано» (счётчик) убрана,
чтобы не смешивать два контура учёта на одной карточке кошелька.

Q2 (нет поля для доп. документов после «Отчёт принят»): FileUploader в
ReportExpenseAdvancePanel вынесен из ветки isAwaitingReport — теперь доступен
и в состоянии REPORTED («Приложите дополнительный документ»), как и обещает
текст «дополнительные документы дополнят его автоматически».

ESLint ✓. Визуальная проверка — по скриншотам.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:54:16 +00:00
coopops ab9b8ddc66 [C28-53][@ant] fix(desktop): перевёрстка диалога пополнения пула + реестра платежей кассира
Typecheck / desktop (pull_request) Failing after 8m33s
Typecheck / controller (pull_request) Has been cancelled
Диалог «Пополнение пула программных расходов»:
- убрана тёмная плашка под кнопками (footer-bar на --p-canvas) — кнопки на
  поверхности диалога через штатный BaseDialog __foot;
- снят двойной паддинг (.form поверх body) и uppercase-eyebrow («прыгающие шрифты»);
- BaseInput → AmountInput с суффиксом валюты в узкой обёртке (не во всю ширину).

Реестр платежей (стол кассира), последовательная подача:
- AttachExpenseProofPanel и ReportExpenseAdvancePanel получили опциональный
  проп step (номер+заголовок) — Этап 1 «Подтвердите оплату», Этап 2 «Отчёт
  пайщика»; обёрнуты в .expense-flow с хайрлайн-разделителем. Пустых этапов у
  DIRECT нет (панель отчёта сама скрывается);
- в отчёте пайщика порядок изменён на «сумма → чек» (сначала AmountInput, видно
  недо/перерасход, затем приложить чек, потом кнопка);
- дружелюбные подписи, больше воздуха (gap --p-3/--p-4), снята плотность.

ESLint ✓. Визуальная проверка — по скриншотам пользователя.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:44:34 +00:00
coopops b21df2b017 [C28-52][@ant] chore(sdk): regen schema/client после doc_data-перестройки 2010/2011
Typecheck / desktop (pull_request) Failing after 8m25s
Typecheck / controller (pull_request) Failing after 8m49s
generate-schema + generate-client + sdk build после C28-51. Zeus-клиент
(controller/zeus + sdk/src/zeus) знает ExpenseProposalSignedItemInput +
doc_data_hash на signed-meta 2010; ExpenseProposalDecisionItemInput (2011)
без requisites/payment_purpose/recipient_name. schema.gql — gitignored-артефакт.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:49:23 +00:00
coopops 0fcb8abd39 [C28-51][@ant] feat(expenses): реквизиты СЗ-2010/2011 off-chain через doc_data (не публикуем в блокчейн)
Typecheck / desktop (pull_request) Failing after 8m23s
Typecheck / controller (pull_request) Has been cancelled
Реквизиты получателя, имя и назначение платежа больше не уходят в blockchain
meta документа. Перестроено на паттерн doc_data из marketplace2 (epic5):
приватный payload сохраняется off-chain в DocDataService фабрики, в meta едет
только doc_data_hash; при генерации/регенерации фабрика подмешивает приватную
часть в позиции по number — полнотекстовый документ не страдает.

2010 (СЗ-смета):
- cooptypes: IExpenseItem → публичный (number/description/amount/recipient_type/
  mechanics); новые IExpensePrivateItem/PrivateData/IExpenseRenderItem; Action
  extends IDocDataRef + doc_data_hash; Model.items = render-позиции.
- factory: Action подгружает PrivateData по doc_data_hash и склеивает по number;
  meta строится из публичной data; ExpenseRenderItemSchema для модели рендера.
- controller: DTO расщеплён — вход генерации «богатый» (приватные поля → сервер
  кладёт в doc_data), подписываемая meta = публичные позиции + doc_data_hash.
  Сервис: saveDocData → Action с публичными items. Десктоп не меняется (подпись
  идёт по meta, возвращённой сервером).

2011 (Решение совета): латентный канал закрыт структурно — item-DTO лишён
requisites/payment_purpose/recipient_name; шаблон протокола их и не рендерил.
Публичная ExpenseItemSchema (5 полей) для модели 2011.

cooptypes build ✓, factory build ✓, ESLint ✓, unit 11/11 ✓.
Codegen (schema/client/sdk) — отдельным шагом.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:35:57 +00:00
coopops c69f0f24d8 [C28-50][@ant] feat(factory): перенести инфру doc_data из marketplace2 (приватные данные документа off-chain)
Typecheck / desktop (pull_request) Failing after 8m40s
Typecheck / controller (pull_request) Failing after 8m0s
Выборочный перенос переиспользуемого паттерна doc_data из marketplace2
(исходный коммит 10158b5b33, 598-8) — БЕЗ marketplace-документов 1102/1103,
которых нет в dev (их cooptypes-реестры и controller-сервисы тянули конфликты
modify/delete и сломали бы сборку). 1102/1103 придут штатно при мерже marketplace2.

Что такое doc_data: приватные поля документа (реквизиты/ПДн) сохраняются в Mongo
(коллекция doc_private_data) через DocDataService.save(payload, registry_id) →
{hash}; on-chain в meta публикуется только doc_data_hash (sha256). При генерации
фабрика подгружает payload по хэшу и отдаёт шаблону под зарезервированной
переменной {{ doc_data.* }}. Так реквизиты в блокчейн не попадают, но документ
регенерируем и верифицируем по слепку.

Перенесено (идентично marketplace2 — будущий мерж без конфликтов):
- factory: Services/DocData + обвязка Factory/index.ts (saveDocData/getDocData) +
  реэкспорт в Services/Databazor + Generator.saveDocData/getDocData в src/index.ts;
- controller: GeneratorPort/GeneratorInfrastructureService/DocumentDomainService —
  проброс saveDocData/getDocData;
- cooptypes: IDocDataRef { doc_data_hash } в модели документа.

cooptypes build + factory typecheck/build зелёные. Контур: следующим шагом
перенастроить СЗ-2010 на doc_data для реквизитов (отдельной задачей).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:15:15 +00:00
coopops 245a8fbd1d [C28-49][@ant] feat(expense): расшить отчёт по авансу — статус отчёта, отчёт за пайщика, закрывающие документы
Typecheck / desktop (pull_request) Failing after 8m22s
Typecheck / controller (pull_request) Failing after 8m3s
Разводит три независимых акта вокруг аванса под отчёт, которые UI прежде смешивал:
- Платёж (кассир + платёжка PAYMENT_PROOF) — без изменений.
- Отчёт по авансу (факт + чек REPORT_FILE → reportexp) — пайщик ИЛИ кассир «за пайщика».
- Закрывающие документы организации (новый ExpenseFileKind.CLOSING_DOC) — только DIRECT.

Бэкенд:
- ExpenseReportState (AWAITING/SETTLEMENT_PENDING/CLOSED/NOT_REQUIRED) зеркалится в
  payment.blockchain_data.report_state: в reportExpenseItem (CLOSED/SETTLEMENT_PENDING)
  и в inter-адаптере reportItem в момент on-chain reportexp (CLOSED). AWAITING — дефолт.
- CLOSING_DOC + valuesMap-описания видов файлов; SDK regen.

Desktop:
- Личный стол: убран кассирский значок «Платёжка приложена» (чужая бухгалтерия),
  добавлен бейдж статуса отчёта рядом со статусом платежа.
- Стол совета: кассир может отчитаться за пайщика (ReportExpenseAdvancePanel onBehalf,
  самоскрытие для DIRECT) + отдельная секция закрывающих документов для оплат организациям.

Авторизация reportExpenseItem уже допускала совет по любой строке — путь «за пайщика»
работает без правок guard'ов. Unit 11/11, ESLint чист, SDK typecheck зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:47:02 +00:00
coopops a0005362e0 [C28-48][@ant] fix(expense): UX расчёта аванса — суммы 2 знака, реквизиты, направление
Typecheck / desktop (pull_request) Failing after 8m25s
Typecheck / controller (pull_request) Failing after 7m56s
Правки по тест-прогону недо-/перерасхода:

- Суммы в реестре платежей (таблица+карточки) и инпут факта — formatAsset2Digits
  (2 знака), а не сырой on-chain precision=4.
- «Направление» (входящий/исходящий — относительно кооператива) скрыто на личном
  столе пайщика (hideActions): на его столе семантика обратная и путала. На столе
  совета остаётся (там перспектива кооператива корректна).
- PaymentDetails: убран дамп «Данные блокчейна» (JSON); реквизиты показываются при
  любом направлении (входящий возврат — реквизиты кооператива, исходящая
  доплата — реквизиты пайщика); «Сумма к переводу» — 2 знака.
- Settlement-платёжка несёт payment_details с реквизитами: возврат → банк
  кооператива (куда платит пайщик), доплата → снимок реквизитов пайщика (куда
  платит кооператив). Назначение: «Возврат неиспользованных средств, выданных под
  аванс под отчёт» / «Доплата по перерасходу аванса под отчёт».
- После отчёта реестр перезагружается и сразу раскрывает заведённую платёжку
  расчёта (по settlement_payment_hash) — пайщик видит реквизиты, не догадываясь
  нажать «развернуть».

Юнит-тесты расходов зелёные (11/11). GraphQL-схема не менялась — SDK regen не нужен.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 08:04:05 +00:00
coopops 3ab773c4b6 [C28-47][@ant] feat(expense): SDK-regen + desktop — инпут факта и расчёт разницы
Typecheck / desktop (pull_request) Failing after 8m24s
Typecheck / controller (pull_request) Failing after 8m17s
Фронт-слой контура недо-/перерасхода аванса.

- payment-domain.entity: exhaustive typeLabels по 2 новым типам платежей.
- SDK regen (Zeus controller+sdk) под reportExpenseItem → ExpenseReportResult,
  optional actual_amount и enum ExpenseReportOutcome.
- SDK: селектор expenseReportResultSelector (outcome / settlement_amount /
  settlement_payment_hash / transaction); reportExpenseItem использует его,
  api панели возвращает результат.
- desktop ReportExpenseAdvancePanel: инпут «Фактически потрачено по чекам»
  (префилл выданным авансом), подсказка о недо-/перерасходе, кнопка «Отчитаться»
  вместо авто-сабмита по первому чеку. По исходу: CLOSED — «Отчёт принят»;
  RETURN_PENDING/OVERSPEND_PENDING — пояснение про платёжку расчёта.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 06:56:06 +00:00
coopops c739c4e10a [C28-46][@ant] feat(expense): расчёт недо-/перерасхода аванса через платёжки
Typecheck / desktop (pull_request) Failing after 8m33s
Typecheck / controller (pull_request) Has been cancelled
Замыкаю контур факт→возврат/доплата для аванса под отчёт. Контракт уже имел
returnexp/overspendexp с двусторонними ledger2-проводками, но фронт/бэкенд их не
звали: отчёт слепо закрывал позицию на actual=аванс без сверки факта (дыра
недорасхода — пайщик мог недоотчитаться и оставить разницу).

Бэкенд-слой:
- 2 типа платежей: EXPENSE_RETURN (входящий возврат недорасхода) и
  EXPENSE_OVERSPEND (исходящая доплата перерасхода) + labels + direction-списки.
- inter-порт шасси: returnItem/overspendItem/reportItem (зеркало payItem),
  адаптер реализует через returnExp/overspendExp/reportExp.
- gateway-ветки: подтверждение кассиром EXPENSE_RETURN (income) → returnexp +
  reportexp; EXPENSE_OVERSPEND (outcome) → overspendexp + reportexp. proposal_hash
  и item_hash берутся из blockchain_data (hash платёжки уникальный, не item_hash).
- reportExpenseItem(actual_amount?): факт==аванс → reportexp сразу (CLOSED);
  недорасход → входящая платёжка на |разницу| (RETURN_PENDING); перерасход →
  исходящая (OVERSPEND_PENDING); reportexp отложен до подтверждения кассиром
  (контракт принимает settlement только на PAID-позиции). Дельта в минорных
  единицах по precision. Идемпотентность платёжки по детерминированному хэшу.
- Новый дискриминированный результат ExpenseReportResultDTO + enum
  ExpenseReportOutcome. Юнит-тесты на все три исхода + идемпотентность.

Контракт expense не изменён. Авторитет на сумму факта — пайщик (по чекам).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 06:43:55 +00:00
coopops e70b368163 [C28-45][@ant] revert: вернуть MyAdvancesPage как каркас стола кассира (не удалять scaffold)
Typecheck / desktop (pull_request) Failing after 8m29s
Typecheck / controller (pull_request) Failing after 8m13s
Откат удаления из C28-44: страницы воркспейса expenses (MyAdvances/Cashier/AdminApprove) — намеренный рабочий КАРКАС будущего стола кассира, а не мёртвый код. Решено их не удалять, а сохранить: соберём стол кассира из них позже, тогда «Мои авансы» выведем туда. Сейчас воркспейс не привязан к столу → в меню их нет и ничего на них не ведёт (то, что и нужно).

— восстановлены MyAdvancesPage.vue + роут expenses-my-advances + экспорт из barrel.
— в install.ts добавлен комментарий-маркер: это каркас, не удалять, ничего на него не ведёт намеренно; рабочие ссылки — на /:coopname/user/payments.
— ссылку напоминателя об авансах НЕ откатываем: остаётся на личные «Платежи» (/user/payments), как просил пользователь — всё ведёт через страницу платежей/реестр платежей, не на каркасные страницы.
— README приведён в соответствие (каркас, а не «удалена»).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 05:10:40 +00:00
coopops 832ea5fd96 [C28-44][@ant] chore: удалить мёртвую страницу «Мои авансы» + перецелить ссылку напоминателя
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
MyAdvancesPage была смонтирована только в воркспейсе expenses, который ни к одному столу не привязан (меню столов приходит с бэкенда; setRoutes вешает пункт лишь если воркспейс есть в столе). Страница открывалась только по прямому URL — пользователь её не видел ни разу. Авансы пайщика-получателя и так видны на его личной странице «Платежи» (/:coopname/user/payments, ListOfPaymentsWidget + ReportExpenseAdvancePanel) — отдельная страница дублировала и была мёртвой.

— desktop: удалён MyAdvancesPage.vue, его роут expenses-my-advances и экспорт из barrel; импорт из install.ts.
— controller: напоминатель об авансах при нескольких авансах вёл на /expenses/my/advances → перецелен на /:coopname/user/payments (личные «Платежи»); один аванс по-прежнему ведёт на сам расход.
— notifications: комментарий схемы payload обновлён.
— README/E2E: ссылки и таблица страниц обновлены.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 05:06:28 +00:00
coopops 2fbf08c19a [C28-43][@ant] fix: исходящий платёж организации больше не висит в личном реестре инициатора СЗ
Typecheck / desktop (pull_request) Failing after 8m29s
Typecheck / controller (pull_request) Has been cancelled
Симптом: пайщик создаёт расход с получателем-организацией (не он сам) — и этот исходящий платёж появляется у него в личном кабинете на странице платежей, хотя он лишь инициатор, а деньги идут не к нему.

Корень: expense-payments.listener создавал gateway-платёж по позиции расхода с username = entity.username (создатель СЗ) для org-получателя. Личный реестр платежей фильтрует по username (loadPayments({username: me})), поэтому платёж кооператива организации оказывался на столе инициатора.

Фикс: владелец org-платежа = сам КООПЕРАТИВ (entity.coopname), а не инициатор. В личных реестрах пайщиков (фильтр по username) он больше не виден; кассир видит его в общем реестре платежей кооператива (loadPayments без username). Аванс под отчёт по-прежнему принадлежит пайщику-получателю (item.recipient) — он у него и отображается. На on-chain payexp правка не влияет: проводка берёт coopname/proposal_hash/item_hash, username не участвует (см. gateway.interactor.processOutgoingPayment).

Касается только НОВЫХ платежей; уже созданные org-платежи с username=создатель не переписываются.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 04:54:41 +00:00
coopops 7155551837 [C28-42][@ant] fix: суммы в карточках расходов программы Благорост — формат asset (1 000,00 RUB)
Typecheck / desktop (pull_request) Failing after 8m34s
Typecheck / controller (pull_request) Failing after 8m2s
Карточки списка показывали сырой total_planned «1000.0000 RUB». ExpenseProposalListRow.total_planned по контракту типа — уже отформатированная строка; страница подавала сырьё. Оборачиваем в formatAsset2Digits в listRows ProgramExpensesPage (как суммы в сводке/детали).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 04:32:04 +00:00
coopops 65b792e015 [C28-41][@ant] feat: история состояний в детали расхода Совета + страница расходов программы → «Расходы»
Typecheck / desktop (pull_request) Failing after 8m33s
Typecheck / controller (pull_request) Failing after 7m56s
— ExpenseDetailPage (стол Совета): добавлена секция «История состояний» (ActivityTimeline, group-by-date) — как на странице расхода программы Благорост. Лента собирается из фактов в данных: создание СЗ, подпись заявления, утверждение/отклонение советом, приложенные документы, подача отчёта, закрытие расхода; актор — ФИО создателя. Отдельной журнальной таблицы у шасси нет.
— capital/install.ts: страница расходов программы переименована «Расходы программы» → «Расходы» (title в meta). Доступ только совету — roles ['chairman','member'] уже стояли. Попутно заменена запрещённая каноном FontAwesome-иконка fa-solid fa-receipt → Material receipt_long на обоих роутах (список + деталь).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:54:04 +00:00
coopops 5aea77bdcd feat(desktop): отдельная страница выхода + глобальная блокировка кабинета на время процедуры
Typecheck / desktop (pull_request) Successful in 13m42s
Typecheck / controller (pull_request) Successful in 13m1s
По уточнению UX:
- кнопка выхода убрана из шапки кошелька; заведена отдельная страница стола
  пайщика «Выход из кооператива» (route membership-exit) с описанием процесса
  (добровольно по заявлению; аккаунт закрывается и удаляется; паевой возвращается
  в срок по Уставу кооператива — без хардкода срока) + кнопка с переподтверждением;
- глобальный gate: пока активен процесс выхода (registrator::exits через query
  membershipExit), ExitOverlay (maximized, без закрытия) блокирует весь кабинет
  и показывает только статус заявления (ожидание Совета / одобрено) и планируемую
  сумму возврата — элегантно, по канону. Эталон — SelectBranchOverlay;
- useExitGate (статус+сумма, опрос) + watch-exit-overlay (поллинг 15с) +
  монтаж в App.vue + регистрация в init-app; после подачи статус подтягивается
  сразу;
- текст переподтверждения переформулирован (необратимо, запуск возврата +
  решение Совета).

ESLint чисто. Type-check — в CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:45:34 +00:00
coopops 3ac1b168d7 feat(controller,sdk): запрос статуса выхода пайщика (membershipExit) для блокировки кабинета
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
- query membershipExit(coopname, username) → {exit_hash, status, quantity, created_at}
  или null; читает chain-таблицу registrator::exits; enum MembershipExitStatus
  (PENDING/AUTHORIZED); self-/operator-доступ;
- AccountBlockchainPort/adapter.getExit — чтение exits по username;
- SDK: Queries.MembershipExit.GetMembershipExit + селектор.

Нужен фронту для глобального gate: пока есть активный выход — кабинет блокируется,
показывается только статус заявления и сумма возврата. tsc контроллера + SDK чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:36:02 +00:00
coopops 04c0b1195a [C28-40][@ant] fix: таблицы расходов схлопывались и рвали текст посимвольно — !important на min-width (как в реестре платежей)
Typecheck / desktop (pull_request) Failing after 8m23s
Typecheck / controller (pull_request) Failing after 8m10s
— симптом: колонка «Пайщик» (ФИО) в реестре расходов рвалась по буквам в столбик.
— причина: глобальный канон components.css форсит `.table{min-width:0!important}`; мой `.table{min-width:…}` без !important проигрывал → колонки схлопывались уже контента и слова ломались посимвольно. Плюс `overflow-wrap: anywhere` на ячейках добивал.
— фикс (эталон — ListOfPaymentsWidget): `.table{table-layout:fixed!important; min-width:…!important}` + `overflow-wrap: break-word` (не anywhere) на текстовых ячейках. При нехватке ширины таблица скроллится в .table-scroll, а не ломает слова. Применено и к таблице позиций в детали (тот же латентный баг).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:54:57 +00:00
coopops 2e42cac14c feat(desktop): UI выхода пайщика из кооператива
Typecheck / desktop (pull_request) Successful in 13m52s
Typecheck / controller (pull_request) Successful in 13m19s
- feature Membership/ExitFromCoop: кнопка «Выход из кооператива» (BaseButton
  danger) на WalletPage в шапке рядом с возвратом паевого; диалог-предупреждение
  (BaseDialog+Form+BaseBanner) с предрасчётом суммы возврата
  (MembershipExitReturnPreview) и текстом «паевой будет возвращён, аккаунт
  заблокирован, возврат невозможен»; submit генерит заявление (200), подписывает
  и подаёт createMembershipExit (push exitcoop). Канон-компоненты, Material-иконки;
- feature Membership/GenerateMembershipExitDecision: генерация решения совета (201);
- process-decisions: handler `leavecoop` в реестре повесток — Совет генерирует
  решение по выходу так же, как по вступлению/возврату.

ESLint чисто. Type-check — в CI (полный vue-tsc локально не гоняем).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:48:16 +00:00
coopops c1a940d1cd feat(sdk): codegen + операции выхода из кооператива (Zeus-клиент + Mutations/Queries)
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
- регенерирован Zeus-клиент (src/zeus) из обновлённой схемы контроллера —
  типы MembershipExit*Input / MembershipExitResult / MembershipExitReturnPreview;
- Mutations.MembershipExit: GenerateMembershipExitApplication /
  GenerateMembershipExitDecision / CreateMembershipExit;
- Queries.MembershipExit.MembershipExitReturnPreview;
- селекторы membershipExitResult / membershipExitReturnPreview.

tsc + сборка SDK — чисто. schema.gql не коммитим (gitignored-артефакт).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:37:59 +00:00
coopops 90f3a892a6 feat(controller): backend выхода пайщика из кооператива (генерация 200/201 + подача + предрасчёт)
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
- PaymentTypeEnum.MEMBERSHIP_EXIT (исходящий) + лейблы;
- document-DTO заявления (200) и решения совета (201) — зеркало join-документов;
- ParticipantInteractor.generateMembershipExit{Application,Decision} (registry 200/201);
- AccountBlockchainPort/adapter.exitCoop — push registrator::exitcoop с подписанным
  заявлением (прямой transact кооперативным ключом, как reguser);
- модуль membership-exit: resolver+service с мутациями
  generateMembershipExitApplication / generateMembershipExitDecision /
  createMembershipExit и query membershipExitReturnPreview (сумма возврата по
  L3-балансам w.wal.share + w.reg.minshr; итог фиксирует контракт на confirmexit);
  self-service для пайщика + операторские роли (chairman/member);
- cooptypes Ledger2.SHARE_WALLET_NAME / MIN_SHARE_WALLET_NAME — именованные
  константы кошельков вместо магических строк.

tsc --noEmit контроллера — чисто. Codegen Zeus-клиента — отдельным коммитом.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:30:01 +00:00
coopops 22e30672a3 [C28-39][@ant] fix: реестр и деталь расхода — чистка шапок, ФИО вместо аккаунта, кликабельные чеки, мелочи вёрстки
Typecheck / desktop (pull_request) Failing after 8m27s
Typecheck / controller (pull_request) Failing after 7m59s
По скриншот-ревью пользователя.

Реестр расходов (ExpensesRegistryPage):
— убрана шапка PageHead («Шасси расходов» / «Реестр расходов» / подзаголовок) — сразу таблица.
— колонка «Пайщик»: ФИО первой строкой (из сертификата подписанта СЗ — её подписывает создатель, доп. запрос не нужен), ниже имя аккаунта мелким моно с копированием по клику (copyToClipboard).
— добавлена колонка-шеврон справа (chevron_right) — видно, что строка открывается; подсвечивается на hover.

Деталь расхода (ExpenseDetailPage):
— убрана шапка PageHead; статус — чипом вверху справа (канон detail-страниц). Переверстана одноколоночно секциями с заголовками-эйбрау (Сводка / Документы / Строки расходов / Чеки и подтверждения) — как на странице расхода программы.
— Сводка: «Пайщик» = ФИО (из сертификата подписанта), отдельная строка «Аккаунт» (моно, копируемая); «Хеш» (рус. ярлык вместо «Hash») переносится и копируется (DataRow mono, vertical).
— «Строки расходов»: счётчик с правильным русским множественным («1 строка / 2 строки / 5 строк») вместо «1 строк».
— «Чеки и подтверждения»: имя файла — гиперссылка, клик открывает документ в новой вкладке (read_url короткоживущий — запрашиваем свежий по id через новый getExpenseFileReadUrl, как в capital); кто приложил — ФИО (из карты подписей СЗ+протокола), а не имя аккаунта; убраны storage_key/размер.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:21:12 +00:00
coopops 941030f58f feat(cooptypes): TS-зеркала выхода из кооператива (registrator + soviet)
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
- interfaces/registrator: IExitcoop/IConfirmexit/ICompletexit/IDeclinexit + IExit
  (таблица exits) — синхронно с обновлённым ABI registrator;
- interfaces/soviet: IDelpartcpnt;
- action-зеркало registrator ExitCoop (член-инициируемое действие, как RegisterUser);
  внутренние коллбэки confirmexit/completexit/declinexit конструируются каскадом
  из soviet/gateway и в зеркалах не нуждаются (как confirmreg/addpartcpnt);
- table-зеркало registrator Exits (scope=coopname) для чтения статуса выхода и
  суммы возврата на бэкенде/фронте.

tsc --noEmit cooptypes — чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:08:07 +00:00
coopops dbda7ca5ba feat(contracts): оркестрация выхода пайщика из кооператива (registrator + soviet)
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Жизненный цикл выхода — зеркало вступления (reguser→confirmreg) и возврата
паевого (wallet::createwthd→authwthd→completewthd):

- registrator::exitcoop    — пайщик подаёт заявление о выходе (registry 200),
  создаётся реестр exits (status=pending) и повестка совета `leavecoop`;
- registrator::confirmexit  — совет одобрил: контракт сам считает сумму возврата
  по L3-балансам ledger2 (мин. + целевой паевой), консолидирует минимальный на
  главный (o.reg.mvmin), резервирует сумму (o.wal.wthreq) и шлёт исходящий
  платёж в gateway; нулевой паевой → финализация без платежа;
- registrator::completexit  — кассир подтвердил выплату: проводка Дт80/Кт51
  (o.wal.wthcpl), пайщик удаляется (soviet::delpartcpnt), аккаунт блокируется;
- registrator::declinexit   — отказ совета или платежа: снятие резерва
  (o.wal.wthdec, если был), пайщик остаётся в кооперативе.

Новое: таблица registrator::exits, soviet_action `leavecoop`, soviet::delpartcpnt
(стирание пайщика, зеркало addpartcpnt; уменьшает счётчик активных). Новых
ledger2-кодов не вводилось — переиспользованы o.wal.wthreq/wthcpl/wthdec и ранее
добавленный o.reg.mvmin. registrator уже в contracts_whitelist → вправе применять
wallet-операции. Обе сборки (soviet/registrator) проходят в docker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:02:56 +00:00
coopops ae80b7f00b [C28-38][@ant] feat: стол совета «Расходы» → реестр всех расходов кооператива (вместо страницы пулов)
Typecheck / desktop (pull_request) Failing after 8m41s
Typecheck / controller (pull_request) Failing after 8m1s
— зачем: на столе совета «Расходы» показывал карточки кошельков-пулов и проваливался с редиректом на страницу программы Благорост. Нужен реестр-наблюдение: единая таблица ВСЕХ расходов кооператива по всем пулам без фильтра, с колонкой кошелька-источника; клик по строке → деталь расхода (та же информация, что на странице расхода). Фильтр по конкретному пулу остаётся на странице расходов программы.

— soviet/install.ts: монтирует ExpensesRegistryPage (title «Реестр расходов», маршрут soviet-expenses-registry) вместо ExpenseWalletsPage; редиректа на программу больше нет. Воркспейс expenses активен → generic-роут expenses-detail резолвится глобально.
— ExpensesRegistryPage: + колонки «Назначение» (описание первой позиции) и «Кошелёк (пул)» (резолв кода source_wallet → человеческое имя через listExpenseWallets(), fallback — сам код); суммы план/факт через formatAsset2Digits (2 знака); убрана колонка «Хеш» (шум, есть в детали).
— ExpenseDetailPage (generic, куда ведёт реестр): поднят до канона страницы расхода — документы СЗ/протокол рендерятся каноном ExpenseProposalDocuments (раскрывающийся BaseDocument с подписями) вместо голых хешей; суммы сводки и позиций через formatAsset2Digits; источник средств показан как имя пула; убрано техническое поле «Действие» (operation_code).
— ExpenseWalletsPage остаётся в barrel'е (не смонтирован); listExpenseWallets/registerExpenseWallet теперь служат картой код-кошелька→имя для реестра. README обновлён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 12:45:44 +00:00
coopops 93e13d6a14 feat(contracts): ledger2-операция o.reg.mvmin (мин. паевой → главный при выходе)
Typecheck / desktop (pull_request) Successful in 13m38s
Typecheck / controller (pull_request) Has been cancelled
Операция-фундамент для консолидации при выходе из кооператива: переносит
минимальный паевой (w.reg.minshr) на главный паевой кошелёк (w.wal.share)
через TRANSFER без проводки (оба кошелька на счёте 80), чтобы вернуть его
вместе с основным паевым через wallet-withdraw (o.wal.wthcpl, Дт80/Кт51).

- operations.hpp: объявление + запись OPERATION_REGISTRY (static_assert OK,
  registrator скомпилирован в docker dicoop/blockchain).
- operations.ts: ручное TS-зеркало (синхронизация обеих сторон).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 12:28:50 +00:00
coopops 6778561228 [C28-37][@ant] fix: лист документа в диалоге — центрируем в col-md-8, не на всю ширину — зачем: A4 на широком экране растягивался во весь макси-диалог. Обёртка .row.justify-center > .col-12.col-md-8 (канон ComplexDocument/ExpandableDocument)
Typecheck / desktop (pull_request) Failing after 8m33s
Typecheck / controller (pull_request) Failing after 7m57s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 12:16:47 +00:00
coopops 311ce09c2e [C28-36][@ant] fix: документы расхода рендерим каноном BaseDocument, не голым DocumentPreview — зачем: статья «голый html» расползалась без стилей и без подписей. ExpenseProposalDocuments теперь во всплывашке (BaseDialog maximized) показывает BaseDocument — ShadowHtml со стилями документа/таблиц + раскрывающийся блок DocumentSignatures (кто подписал). Пропы статья/протокол типизированы IDocumentAggregate, statement_doc/decision_doc ложатся напрямую (как decisionAggregate в ExpandableDocument у собраний)
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 12:12:46 +00:00
coopops afeb728bec fix(factory): заявление о выходе (200) — паспорт по условию + ЭП-подпись
Typecheck / desktop (pull_request) Successful in 13m33s
Typecheck / controller (pull_request) Successful in 13m15s
- Паспортные данные физлица выводятся при vars.passport_request=='yes',
  как в заявлении на вступление (100).
- Внизу документа «Документ подписан электронной подписью.» вместо
  «личная подпись заявителя» (по правке пользователя).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 11:59:51 +00:00
coopops ee60ece571 feat(factory): документы выхода из кооператива (registry 200/201)
Typecheck / desktop (pull_request) Successful in 13m34s
Typecheck / controller (pull_request) Successful in 13m13s
Заявление о выходе из состава пайщиков (200) с условным рендером по трём
ролям (физлицо/ИП/юрлицо) — зеркало 100.ParticipantApplication, текст по
утверждённой форме. Решение совета о выходе (201) — зеркало 501.

- cooptypes: registry 200/201 (Model + context + переводы + exampleData),
  поле vars.participant_exit_application для шапки «ФОРМА УТВЕРЖДЕНА».
- factory: Templates/Actions 200/201, регистрация в индексах и factories-map.
- test: генерация 200 в трёх ролях + 201; сидинг participant_exit_application.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 10:24:19 +00:00
coopops 875ca6edee [C28-35][@ant] feat: документы расхода (СЗ + протокол) на странице расхода + правки лейблов/формата — зачем: со страницы расхода нужно открывать заявление и протокол решения совета, единообразно везде. Доменный канон ExpenseProposalDocuments (shared/ui/domain): клик по строке СЗ/протокола открывает документ во всплывашке (BaseDialog maximized + DocumentPreview, html уже приходит в statement_doc/decision_doc.rawDocument). На ProgramExpensePage: секция «Документы» после сводки; суммы (по смете/фактически/план/факт позиций) — через formatAsset2Digits (2 знака); «Первичные документы» → «Подтверждающие документы»; в таймлайне «Платёжка: приложен документ» → «Приложен документ». Канон статусов: REPORTED «Отчитана» → «Отчёт получен» (shared/lib/expenses)
Typecheck / desktop (pull_request) Failing after 8m29s
Typecheck / controller (pull_request) Failing after 7m56s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 09:48:33 +00:00
coopops e6090d9200 [C28-34][@ant] feat: фоновый напоминатель об отчёте по авансу под отчёт — зачем: закрыть момент, когда пайщик получил аванс и не отчитался. @Interval-воркер в extensions/expenses сканирует зеркало расходов, находит ADVANCE-позиции в PAID без отчёта, шлёт пайщику-получателю недельный агрегированный дайджест (workflow ExpenseAdvanceReportReminder) со ссылкой на сам расход (/:coopname/expenses/:hash) или список «Мои авансы»; только получателю, кассиру/совету ничего, ORG пропускаются. Троттлинг раз в неделю — через идемпотентность Центра уведомлений (поле period=ISO-неделя в payload), без своей таблицы. Грейс до первого письма и интервал тика — env (EXPENSE_ADVANCE_REMINDER_*, дефолты 3 дня / 6 ч). Точка отсчёта — proposal.updated_at; deadline из формы пока не персистится в цепи — отмечено follow-up в README
Typecheck / desktop (pull_request) Failing after 8m33s
Typecheck / controller (pull_request) Failing after 8m2s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 05:52:04 +00:00
coopops 999eee2d84 [C28-33][@ant] docs: README по UI-виджетам шасси расходов и рецепту подключения пула — зачем: стол КУ собирается по документу без возврата в контекст Благороста
Typecheck / desktop (pull_request) Failing after 8m31s
Typecheck / controller (pull_request) Failing after 8m5s
2026-06-12 18:10:33 +00:00
coopops ed52abe86c [C28-33][@ant] refactor: виджеты шасси расходов — в shared/ui/domain — зачем: стол КУ должен собираться из готовых компонентов без возврата в контекст Благороста. ExpenseCreateDialog (форма + черновик в localStorage + генерация и подпись СЗ; пул-источник, ключ черновика и подача на чейн — пропы) и ExpenseProposalList (список СЗ пула, данные подаёт страница) — общие доменные виджеты; подписи статусов/механик/файлов — src/shared/lib/expenses; capital переведён на них первым потребителем (тонкая обёртка CreateProgramExpenseDialog + listRows на ProgramExpensesPage), старые labels — реэкспорт
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
2026-06-12 18:05:32 +00:00
coopops 06c922abf1 [C28-33][@ant] feat: фабричная переиспользуемость шасси расходов + кошельки расходов на столе совета + закрытие расхода + UX отчёта — зачем: шасси должно настраиваться на любой пул без правок контракта. EXPENSE_OPERATION_SETS в ledger2 (source_wallet → 5 кодов операций, compile-time валидация) — в expense.cpp нет захардкоженных кодов; реестр registerExpenseWallet: capital регистрирует пул из install.ts, стол совета рендерит страницу «Расходы» (WalletCard + проваливание в реестр пула); кнопка «Закрыть расход» (совет, REPORT_SUBMITTED → closeexp), роль мутации chairman/member; файлы расхода: регистронезависимый маппинг item_hash (чейн UPPER vs storage lower — документы «исчезали»), секция «Прочие документы»; отчёт: выбор файла = мгновенная загрузка + автоподача reportexp, дозагрузка дополняет отчёт, у кассира платёжка грузится без кнопки; source_wallet в СЗ-документе — w.cap.pgexp
Typecheck / desktop (pull_request) Failing after 8m30s
Typecheck / controller (pull_request) Failing after 7m54s
2026-06-12 17:15:46 +00:00
coopops 8346d97326 [C28-33][@ant] fix: локаторы процессов p.cap.pgexp и p.exp.expns в process-hash-locator — зачем: fail-fast валидатор контроллера падал на старте после добавления операций пула расходов в cooptypes
Typecheck / desktop (pull_request) Failing after 8m38s
Typecheck / controller (pull_request) Failing after 8m6s
2026-06-12 13:40:45 +00:00
coopops 23bb2ad51f [C28-33][@ant] fix!: оплата СЗ — из кооперативного пула расходов w.cap.pgexp, не с личного кошелька пайщика — зачем: payexp списывал w.cap.blago (USER_SHARED) с L3 инициатора, уменьшая его паевые взносы; topupprogexp теперь наполняет кошелёк пула (o.cap.pgtop), подотчёт w.exp.adv числится на получателе аванса; имена o.exp.*/p.exp.expns в cooptypes-словари (названия операций в UI); форма: пайщик-получатель только физлицо, поле ФИО по канону; страница расхода: кнопка «К расходам», без дубля суммы, позиции отклонённой СЗ не «ожидают оплаты»
Typecheck / desktop (pull_request) Failing after 8m39s
Typecheck / controller (pull_request) Failing after 8m12s
2026-06-12 13:19:20 +00:00
coopops 23cd31861a [C28-33][@ant] feat: страница расхода (позиции+реквизиты+файлы+история состояний по канону), выбор пайщика-получателя по ФИО, черновик формы в localStorage, отчёт пайщика по авансу чеком из реестра платежей, создание СЗ только совету — зачем: наполнение карточки расхода и полный цикл аванса под отчёт
Typecheck / desktop (pull_request) Failing after 8m30s
Typecheck / controller (pull_request) Failing after 8m5s
2026-06-12 12:15:01 +00:00
coopops 918f30073a [C28-33][@ant] fix: реестр платежей — компактные DataRow с копированием вместо растянутых CopyableInput, оригинальное имя файла платёжки + рабочее открытие по свежему read_url, отметка «платёжка приложена» у исходящих расходных платежей (proof_count в blockchain_data)
Typecheck / desktop (pull_request) Failing after 8m36s
Typecheck / controller (pull_request) Failing after 8m1s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 08:59:02 +00:00
coopops f9d09a74cc [C28-33][@ant] feat: исходящие платежи расходов в реестре (тип EXPENSE: листенер на authexp + payexp при подтверждении кассой, реквизиты и назначение из снимка), протокол 2011 по канону решений совета (кворум/голоса/повестка через decision_id), подтверждение оплаты файлом в реестре, карточка расхода без механики и с № СЗ
Typecheck / desktop (pull_request) Failing after 8m28s
Typecheck / controller (pull_request) Failing after 8m0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 07:58:28 +00:00
coopops cb0056f716 [C28-33][@ant] fix: кнопки расходов в топбар через canon Teleport (#header-actions-host), фикс генерации решения 2011 (payment_purpose/deadline/fund_name в DTO), СЗ 2010 — место и дата под заголовком, подпись с отступом, фон футера диалога убран
Typecheck / desktop (pull_request) Failing after 8m22s
Typecheck / controller (pull_request) Failing after 8m0s
2026-06-12 07:15:06 +00:00
coopops 7a7e5a04f0 [C28-33][@ant] feat: кнопки расходов в PageHead (создать → пополнить), короткий № СЗ и formatAsset по канону фабрики, автоназначение платежа «Аванс под отчёт» для пайщика — описание позиции переназвано «Что оплачиваем», пункты документа без списка
Typecheck / desktop (pull_request) Failing after 8m32s
Typecheck / controller (pull_request) Failing after 8m7s
2026-06-12 06:45:07 +00:00
coopops c8a15511ee [C28-33][@ant] feat: СЗ 2010 по канону вёрстки реестра + назначение платежа отдельным полем после реквизитов — фиксируется в снимке для кассира; «оплата по счету», «Организация/ИП», алерт «передано в совет», хинт механики убран
Typecheck / desktop (pull_request) Failing after 8m25s
Typecheck / controller (pull_request) Failing after 7m57s
2026-06-12 06:07:44 +00:00
coopops 61038a6f8c [C28-33][@ant] feat: срок «в срок до» и фонд списания в СЗ 2010 + инвариант механики оплаты — пайщику только аванс под отчёт, организации только прямая оплата (форма/бэкенд/контракт); фикс 422 на пустом recipient организации
Typecheck / desktop (pull_request) Failing after 8m28s
Typecheck / controller (pull_request) Failing after 8m1s
2026-06-11 20:28:35 +00:00
coopops dfe73601e8 [C28-33][@ant] feat: реквизиты получателей по payment_method со снимком в БД + СЗ 2010 по бумажному образцу — полные реквизиты в документ подставляет сервер, фронт видит сокращённые; вычищены осиротевшие authorizeExpenseReport/declineExpenseReport
Typecheck / desktop (pull_request) Failing after 8m31s
Typecheck / controller (pull_request) Has been cancelled
2026-06-11 20:01:43 +00:00
coopops 1f3e9d93c7 [C28-33][@ant] feat: утверждение программного расхода через решение совета — createexp ставит вопрос в повестку (soviet::createagenda), authexp/declexp стали callback'ами совета; кнопка председателя убрана
Typecheck / desktop (pull_request) Failing after 9m36s
Typecheck / controller (pull_request) Successful in 13m38s
2026-06-11 19:20:18 +00:00
coopops 67dde7cf45 [C28-33][@ant] feat(capital): рассмотрение программного расхода председателем
Typecheck / desktop (pull_request) Failing after 8m22s
Typecheck / controller (pull_request) Successful in 13m19s
На карточке расхода в статусе «Создан» председатель видит кнопку «Рассмотреть» —
открывается диалог авторизации СЗ (утвердить с протоколом 2011 / отклонить с
причиной). Фиксы диалога: отклонение теперь идёт через declineExpenseReport →
declexp (раньше ошибочно через authexp, который безусловно ставил AUTHORIZED);
убраны ссылки на удалённый operation_code (назначение = перечень позиций).
Инициатор в карточке — ФИО (creator_name). Тексты карточек пулов укорочены.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:26:19 +00:00
coopops 06a5cf2464 [C28-33][@ant] feat(capital): creator_name в выводе программного расхода
В карточке показывался username (ant) — канон требует ФИО/название организации.
Бэкенд резолвит имя через ACCOUNT_DATA_PORT.getDisplayName (батчем по уникальным
creators, при ошибке остаётся username). Codegen: schema + zeus + sdk selector.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:26:19 +00:00
coopops 8073eb8642 [C28-33][@ant] fix(desktop): WalletCard — перенос заголовков вместо marquee
Бегущая строка убрана полностью (раздражала и читалась плохо): заголовок и
подпись переносятся максимум на две строки с многоточием, полный текст — в
title-тултипе. Выпилены measure/ResizeObserver/tabindex и keyframes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:26:18 +00:00
coopops 986b5e0cf2 [C28-33][@ant] fix(expenses): callback.data нормализуется в hex-строку при синке зеркала
Typecheck / desktop (pull_request) Failing after 8m25s
Typecheck / controller (pull_request) Has been cancelled
On-chain callback.data — vector<char>; парсер десериализует в Uint8Array, который
в jsonb-зеркале становится {} (пустой) или {"0":..}. GraphQL-поле data — String,
сериализация ответа capitalProgramExpenses падала «String cannot represent value: {}»
и UI показывал пустой список при живой строке в зеркале. Нормализуем в hex ещё
в дельта-маппере (на записи), существующая строка поправлена UPDATE'ом.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:02:38 +00:00
coopops 8c09d9f2ae [C28-33][@ant] feat(boot): pnpm run reboot:blago — перезапуск dev-среды Благороста одной командой
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
extra_reboot (чистая цепь + совет + dev-shortcut онбординга capital — без него
после reboot снова 5 решений совета руками) → ожидание готовности controller →
seed-capital --up-to=04-contributor (программы, проекты, регистрация ant).
Глубина сида настраивается: SEED_UP_TO=08-investments pnpm run reboot:blago.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:57:00 +00:00
coopops 3532245012 [C28-33][@ant] fix(desktop): укорочен locked-label на странице программных расходов
Typecheck / desktop (pull_request) Failing after 8m31s
Typecheck / controller (pull_request) Successful in 13m27s
Канон-класс .wallet__locked-line — white-space:nowrap + inline-flex, рассчитан
на короткий лейбл; длинный «Зарезервировано под активные расходы» не переносился
и вылезал за карточку. Лейбл → «Зарезервировано» (смысл ясен из контекста
страницы), общий компонент не трогаем.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:28:28 +00:00
coopops a08cb7b2b6 [C28-33][@ant] fix(controller): идемпотентное сохранение blockchain-action
global_sequence — глобально-уникальный монотонный id действия в истории цепи.
При re-scan/replay redis-стрима то же действие доставляется повторно → INSERT
падал на unique-индексе (23505) → consumer не ACK'ал сообщение и зацикливал
recoverOwnPending/reclaimStalePending (дикий спам duplicate-key в логах). Теперь
дубль трактуется как «уже сохранено»: возвращаем существующую запись, не бросаем.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:28:28 +00:00
coopops 8b2b9893df [C28-33][@ant] fix(expense): программные расходы зеркалятся в БД и видны в списке
Парсер не был подписан на контракт `expense` (нет в subscribedContracts) →
дельты `expense::proposals` не эмитились → postgres-зеркало `expense_proposals`
оставалось пустым → список «Программных расходов пока нет», хотя на цепи СЗ и
резерв создавались. Добавил `expense` в subscribedContracts парсера.

Плюс: `coopname` добавлен явным полем в таблицу `proposals` контракта (struct +
EOSLIB_SERIALIZE + проставление в createexp) и в cooptypes IProposal. Таблица
scoped по coopname, но зеркало требует coopname в строке — дублируем полем по
канону (как capital/soviet), а не выводим из scope дельты.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:28:16 +00:00
coopops 0a3e1c97b7 [C28-33][@ant] fix(expense): proposal_hash в meta документа 2010 — приведено к канону 2011 (поле в Action/Model cooptypes + Base-meta DTO + JSONSchema шаблона + фабрика кладёт в combinedData + фронт передаёт при генерации); раньше proposal_hash был только в signed-DTO → подписанный документ не проходил валидацию «Field proposal_hash not provided»
Typecheck / desktop (pull_request) Failing after 8m40s
Typecheck / controller (pull_request) Successful in 13m25s
2026-06-11 16:57:50 +00:00
coopops dd5d61b0b4 [C28-33][@ant] fix(cooptypes): шаблон 2010 — nunjucks-тег {% elif %} вместо liquid-овского {% elsif %} (движок рендера nunjucks, как в шаблонах 100/101); рендер падал «unknown block tag: elsif»
Typecheck / desktop (pull_request) Failing after 8m24s
Typecheck / controller (pull_request) Has been cancelled
2026-06-11 16:40:05 +00:00
coopops 44e9bb24a9 [C28-33][@ant] fix(factory): фабрики 2010/2011 сами тянут coop/vars/user из хранилища (resolveParallel-канон) — раньше ждали их в data и валидация падала «должно иметь обязательное поле coop»
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
2026-06-11 16:34:13 +00:00
coopops 1fcf8523df [C28-33][@ant] feat(expense): per-позиционная механика оплаты — operation_code удалён из proposal/createexp/createpgexp, ledger2-код выводится из item.mechanics при payexp; в одном СЗ смешиваются аванс и прямая оплата; UI — селект способа на каждой позиции
Typecheck / desktop (pull_request) Failing after 8m39s
Typecheck / controller (pull_request) Successful in 13m15s
2026-06-11 13:22:34 +00:00
coopops 42c179d5d0 [C28-33][@ant] fix(desktop): marquee WalletCard — бежит только строка под курсором/фокусом (не вся карточка), скорость одинаковая у всех строк (длительность цикла из величины переполнения, linear)
Typecheck / desktop (pull_request) Successful in 14m17s
Typecheck / controller (pull_request) Has been cancelled
2026-06-11 12:48:47 +00:00
coopops 794cb43de6 [C28-33][@ant] fix(factory): зарегистрировать фабрики 2010/2011 (ExpenseProposalStatement/Decision) в Registry и карте factories — шаблоны были написаны и экспортированы, но генератор их не находил («Фабрика для документа #2010 не найдена»)
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
2026-06-11 12:42:17 +00:00
coopops bae8bdb52f [C28-33][@ant] fix(desktop): marquee кошельков — бег обеих строк только на hover/фокус (в покое «…», tabindex лишь при переполнении для тапа), иконка удаления позиции расхода — в слот #icon-left (icon-only не рендерит дефолтный слот, иконка была невидима)
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
2026-06-11 12:19:55 +00:00
coopops 1759c2ac4e [C28-33][@ant] feat(capital): балансы пулов на странице программных расходов + чистка диалога — карточки «Глобальный инвест-пул» и «Пул программных расходов» (чтение живьём с цепи через capitalState: добавлены поля program_expense_pool/reserved в GraphQL DTO и interactor, без участия БД), из диалога пополнения убраны технические идентификаторы пулов
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
2026-06-11 12:13:11 +00:00
coopops 7bddb72fa1 [C28-33][@ant] fix(desktop-capital): заголовок карточки расхода — описание первой позиции; поля description на уровне расхода нет ни в шасси, ни в inter-порте, vue-tsc валил CI после regen
Typecheck / desktop (pull_request) Successful in 13m53s
Typecheck / controller (pull_request) Successful in 14m1s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 04:51:02 +00:00
coopops 7f35ef3710 [C28-29][@ant] merge: актуальный dev в ветку шасси — синхронизация перед объединённым PR
Typecheck / desktop (pull_request) Failing after 8m32s
Typecheck / controller (pull_request) Has been cancelled
# Conflicts:
#	components/contracts/cpp/lib/core/ledger2/wallets.hpp
#	components/controller/schema.gql
#	components/controller/zeus/const.ts
#	components/controller/zeus/index.ts
#	components/cooptypes/src/ledger2/wallets.generated.ts
#	components/sdk/src/zeus/const.ts
#	components/sdk/src/zeus/index.ts
2026-06-11 04:30:51 +00:00
coopops fe915bd1d5 [C28-33][@ant] merge: интеграция Капитал → шасси расходов (PR #67) в зонтичную ветку шасси — объединение двух PR в один 2026-06-11 04:20:20 +00:00
claude 75cd0976e2 fix(desktop): PropType<BaseButtonSize> в DeletePaymentMethodButton — разблокировка desktop-CI всех PR в dev (#128) 2026-06-10 22:30:58 +00:00
coopops d50b619f75 [C28-32][@ant] fix(desktop-expenses): String(uploaded_at) для formatDate — zeus-скаляр Date типизирован как {}, vue-tsc валил CI
Typecheck / desktop (pull_request) Successful in 13m38s
Typecheck / controller (pull_request) Successful in 13m13s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:24:32 +00:00
coopops 714d618320 [C28-32][@ant] fix(desktop-expenses): String(uploaded_at) для formatDate — zeus-скаляр Date типизирован как {}, vue-tsc валил CI
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:24:26 +00:00
coopops dd0097b56c [C28-31][@ant] fix(expenses-spec): спека trigger-сервиса отстала от конструктора с EventEmitter2 — TS2554 валил CI controller; тест проверяет эмит EXPENSE_CAPITALIZATION_REQUIRED вместо снесённого TODO-лога
Typecheck / desktop (pull_request) Failing after 8m25s
Typecheck / controller (pull_request) Successful in 13m11s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:12:03 +00:00
coopops 773ecd89c2 [C28-31][@ant] fix(expenses-spec): спека trigger-сервиса отстала от конструктора с EventEmitter2 — TS2554 валил CI controller; тест проверяет эмит EXPENSE_CAPITALIZATION_REQUIRED вместо снесённого TODO-лога
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:11:58 +00:00
coopops aeadf60239 [C28-32][@ant] fix(desktop-expenses): SuccessAlert принимает одну строку + Number(size_bytes) — vue-tsc валил CI desktop
Typecheck / desktop (pull_request) Failing after 8m55s
Typecheck / controller (pull_request) Failing after 8m4s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 18:50:06 +00:00
coopops e678d62e82 [C28-33][@ant] fix(desktop-capital): SuccessAlert принимает одну строку — те же сигнатурные ошибки vue-tsc в диалогах программных расходов
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 18:49:52 +00:00
coopops 5cb8ef3175 [C28-32][@ant] fix(desktop-expenses): SuccessAlert принимает одну строку + Number(size_bytes) — vue-tsc валил CI desktop
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 18:49:48 +00:00
coopops 4f9df9012a [C28-33][@ant] fix(desktop-capital): механика следует из способа оплаты СЗ + явный резолв получателя по типу — смешение ADVANCE/DIRECT в одном СЗ режется контрактом, а recipient уходил пустой строкой из-за ?? при заполненном recipient_name
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 18:42:46 +00:00
coopops 73fc79fa88 [C28-33][@ant] fix(boot): деплой контракта expense — аккаунт + code permissions; без него шасси не поднимается на свежем стенде
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 18:42:45 +00:00
coopops e073a6d9bc [C28-33][@ant] fix(expense+capital): авторизация inline-связки и state machine шасси — e2e-цепочка не проходила ни по одному сценарию
- expense::createexp принимает контракты-инициаторы по contracts_whitelist
  (capital@eosio.code не имеет coopname@active — inline падал по authority);
  capital::createpgexp шлёт inline от _capital@active.
- capital::onpgexpdone: require_auth(_expense) вместо _capital — callback
  шасси шёл с authority expense@active и всегда падал.
- payexp: cap actual<=plan + DIRECT-item сразу REPORTED с пересчётом статуса
  proposal — DIRECT-only СЗ навсегда зависал в PARTIALLY_PAID.
- returnexp/overspendexp: settlement-семантика по PRD — статус item остаётся
  PAID, отчёт закрывает item штатным reportexp (раньше RETURNED/OVERSPENT
  были терминальными тупиками: перерасход блокировал closeexp навсегда).
- declexp: только CREATED/AUTHORIZED — decline после оплат разъезжался
  с учётом пула в capital (возвращал весь резерв при ушедших деньгах).
- onpgexpdone CLOSED: перерасход сверх резерва списывается из
  program_expense_pool (spend_program_expense_pool).
- names.hpp: redefinition CREATE_PROGRAM_EXPENSE + несуществующий
  Names::Capital::Callbacks — контракты не компилировались вовсе.
- verify_document_or_fail(statement, {creator}) в createpgexp; снос
  мёртвых set_program_approved/set_program_authorized.

Оба контракта собраны: expense.wasm + capital.wasm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 18:42:28 +00:00
coopops ec5b5ee403 fix(desktop): SSR-гидратация ломала ВСЕ маршруты — пустые страницы до F5
Найдена реальная причина «белого экрана» на первом SSR-заходе (доказано
DOM-диагностикой + __INITIAL_STATE__ прода: 87 сериализованных "component"
в desktops.workspaces[].routes):

SSR-сервер кладёт RouteRecordRaw вместе с component в Pinia-стор; Quasar
сериализует стейт в __INITIAL_STATE__; Vue-компонент не переживает JSON
(render-функция выпадает). Клиент гидратируется мёртвыми маршрутами,
registerWorkspaceMenus регистрирует их в router (routes=108 сразу),
initExtensions живые не добавляет («маршрут уже есть») → каждая страница
рендерится пустой при работающем layout/меню. F5 «лечит», потому что SW
отдаёт SPA-shell без гидратации (Config.js-путь, routes=13 → живые 108).
Локально не воспроизводилось: dev-режим SPA, без SSR.

Фикс: на клиенте перед loadDesktop зачищаем гидратированные routes из
workspaces — живые добавит useInitExtensionsProcess, ровно как при
SPA-заходе. Диагностика [BOOTRACE] остаётся до подтверждения на проде.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 18:34:29 +00:00
coopops 787b2ca786 [C28-33][@ant] merge: база feat/expense-chassis-mvp (zeus fix) в ветку интеграции 2026-06-10 18:22:51 +00:00
coopops 4eb72309d4 [C28-31][@ant] fix(sdk): Zeus regen с ExpenseProposal*GenerateDocumentInput — sdk tsc падал на ручных обёртках без типов
Typecheck / desktop (pull_request) Failing after 8m19s
Typecheck / controller (pull_request) Failing after 8m14s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 18:22:27 +00:00
coopops 93fda756aa Revert "feat(desktop): сайт кооператива в scope_extensions динамического PWA-манифеста"
This reverts commit adea7038d3.
2026-06-10 12:09:51 +00:00
coopops adea7038d3 feat(desktop): сайт кооператива в scope_extensions динамического PWA-манифеста
Переходы из установленного PWA на сайт кооператива (vars.website из
публичного getSystemInfo, кэш 5 мин) остаются в окне приложения вместо
выброса в браузер. Заодно явные id и scope для стабильной identity.
Работает в Chrome/Edge 138+ на десктопе; сайт должен отдать
/.well-known/web-app-origin-association с {"https://<домен-лк>/": {"scope": "/"}}.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 10:57:15 +00:00
coopops 10cc7bdb9f fix(desktop): реестр платежей — кнопки действий не наезжают на статус
Глобальный канон .table{min-width:0!important} снимал локальный min-width
таблицы — колонки сжимались уже контента и кнопки «Подтвердить/Отклонить»
ложились поверх бейджа статуса. Возвращён min-width (860/980px) с
!important: при нехватке ширины таблица скроллится в .table-scroll, как
журнал уведомлений, а не схлопывает колонки.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:26:46 +00:00
coopops 6c4b92805e fix(desktop): отступ между карточками пайщиков — margin-bottom на .participant-card
padding/margin на .q-table__grid-item virtual-scroll игнорирует, поэтому
зазор не появлялся. Отступ повешен на саму .participant-card (margin-bottom
внутри grid-item) — его virtual-scroll учитывает в измеряемой высоте.
Grid-item padding возвращён в 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:02:36 +00:00
coopops 7830713d7c chore(release): publish
Build bootstrap container / build (push) Successful in 46s
Release / publish-packages (push) Failing after 8m19s
Release / release (push) Successful in 24m57s
Release / trigger-coopenomics-docs (push) Successful in 1s
Release / trigger-mono-docs (push) Successful in 2s
2026-06-09 18:41:34 +00:00
ant 4b99780ab8 Merge pull request 'ci(release): FF-промоушн dev→testnet→main вместо merge -X theirs' (#119) from feat/ff-release-flow into dev
Reviewed-on: #119
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-09 18:41:01 +00:00
coopops 21d523d18b fix(desktop): карточка пайщика — отступ, наезд бейджа, иконки аккаунта/email
- Отступ между карточками: margin-bottom игнорился virtual-scroll, а
  padding:0 убрал дефолтный зазор. Зазор задан через padding grid-item
  (входит в измеряемую высоту элемента virtual-scroll).
- Наезд бейджа: длинный бейдж («Ожидает решения совета») переполнял свой
  контейнер и налезал на дату/удаление. Мета-строка переведена на flex-wrap
  — дата+удаление переносятся на след. строку, когда бейдж не помещается.
- Аккаунт: иконка badge с подсказкой «Имя аккаунта» в начале + кнопка
  копирования username. Email: иконка mail в начале. Текст с ellipsis.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:36:37 +00:00
coopops eb025b6105 fix(desktop): реестр пайщиков — подложка, бейдж и отступы на мобиле
- ListOfParticipantsPage: на <768px (grid-режим карточек) убрана внешняя
  обрамлённая поверхность .participants-page__card — карточки уже сами в
  рамках, обёртка давала «подложку»/двойное обрамление. На десктопе рамка
  остаётся (там таблица).
- ParticipantCard: широкий бейдж статуса («Ожидает решения совета») сжимал
  имя/аккаунт/email до пары букв. Перестроено: идентификация на всю ширину
  в верхней строке, бейдж + дата + удаление — отдельной строкой снизу.
- ParticipantsTable: на мобиле карточки во всю ширину с понятным
  вертикальным отступом (убран дефолтный 4px-padding grid-item, задан gap).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:30:49 +00:00
coopops 44e45dfb00 [@dark-sun] ci(release): FF-промоушн dev→testnet→main вместо merge -X theirs
Typecheck / desktop (pull_request) Failing after 8m19s
Typecheck / controller (pull_request) Successful in 12m30s
Релизный флоу переведён на линейную fast-forward модель — устраняет
регулярные конфликты на 20 package.json при релизе.

Корень проблемы: publish-alpha.sh/publish-prod.sh бампали версию НА КАЖДОЙ
ветке (alpha-N на testnet, чистую на main) через `git merge -X theirs` +
back-merge. Два независимых bump-коммита за цикл + merge'и плодили
расхождение веток → конфликты (особенно при гонке push/pull).

Новая модель:
- Версию бампает lerna ОДИН раз на dev (scripts/cut-release.sh).
- Тот же коммит едет вверх по FF: scripts/promote.sh testnet|main
  (server-side fast-forward push, рабочее дерево не трогается).
- testnet/main не несут своих коммитов → ветки не диверджатся → конфликты
  структурно невозможны.

release.yaml:
- Триггер: push в testnet/main с изменением lerna.json (вместо тега v*).
  Окружение определяет ВЕТКА (main→prod, testnet→staging), не суффикс -alpha.
- Версия читается из закоммиченного lerna.json (едет с коммитом по FF).
- Гейты npm-publish/доки: branch == main (вместо !contains '-alpha').
- Образы/webhook по-прежнему версия-тегированы → playbooks/приёмник деплоя
  не затрагиваются.

Удалены publish-alpha.sh/publish-prod.sh и мёртвые дубли тех же merge-X-theirs
скриптов (root sync:main, components/contracts production/testnet/docs-publish).
Документация — scripts/RELEASE.md + CLAUDE.md PR-flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:29:09 +00:00
coopops f64c9ae0f9 fix(desktop): контакты — заголовок организации внутрь карточки
Eyebrow + название кооператива висели отдельным блоком над карточкой и
выглядели оторванно. Перенесены в head карточки (отделены линией от сетки
реквизитов); размер h1→h2 под карточный контекст; field-значения
overflow-wrap anywhere→break-word (телефон не рвётся посреди цифр).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:22:15 +00:00
coopops e3f399c0e8 fix(desktop): минимальный остаток — через WalletCard, чтобы тоже бежал
Карточка «Минимальный неснижаемый остаток» рисовалась вручную сырыми
canon-классами .wallet в обход WalletCard, поэтому marquee на неё не
распространялся (бежали только программные кошельки). Переведена на
<WalletCard neutral> — DRY + бегущая строка заголовка/подписи бесплатно.
WalletCard: program стал опциональным, добавлен neutral-вариант подсветки
иконки (--p-canvas-2/--p-ink-2) вместо акцента программы.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:21:01 +00:00
coopops 8d5ff83c05 fix(desktop): мобайл-карточки журнала, бегущие строки кошелька, канон-удаление реквизитов
Build bootstrap container / build (push) Successful in 2m40s
- Журнал уведомлений: на телефоне вместо горизонтального скролла/посимвольного
  переноса — компактные карточки, поля по два в ряд (col-xs-6); таблица скрыта
  на ≤599px (скрытие через двойной селектор .nj-cards.nj-mobile).
- WalletCard: заголовок и подпись при переполнении становятся «бегущей строкой»
  (marquee) вместо обрезки «…» — JS-детект overflow + CSS-анимация, обе строки
  бегут одинаково (одна длительность, синхронные паузы). Уважает
  prefers-reduced-motion.
- Реквизиты (PaymentMethods): кнопка удаления переведена на канон — icon-only
  BaseButton (delete_outline, danger), без текста «удалить»; убраны лишние
  fallthrough-атрибуты flat/color.
- BaseCard head: align-items center → flex-start, чтобы угловое действие при
  2-строчном заголовке не проваливалось к центру.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:08:32 +00:00
coopops 1d56c8d9fd chore(release): publish
Build bootstrap container / build (push) Successful in 3m17s
Release / release (push) Successful in 25m58s
Release / publish-packages (push) Failing after 8m23s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / trigger-mono-docs (push) Successful in 2s
2026-06-09 17:47:20 +00:00
coopops 3f8522507c chore(release): publish
Build bootstrap container / build (push) Successful in 3m12s
Release / release (push) Successful in 25m50s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-06-09 17:46:58 +00:00
coopops b9634c400d fix(desktop): перезагружать стол после входа в той же вкладке (белый экран)
Реальная причина белого экрана (предыдущий коммит чинил не то — гонка
чанков по логам не подтвердилась, router.onError не срабатывал):

Вход в той же вкладке (incognito-логин или выход→вход без F5) НЕ
переинициализирует приложение. LoginForm после login() звал только
selectDefaultWorkspace(true) + goToDefaultPage(), но НЕ loadDesktop().
Поэтому currentDesktop оставался АНОНИМНЫМ (загруженным до входа), целевой
стол (chairman/connect) резолвился, но рендерился пустым — данные/гранты
DesktopWorkspace были анонимные. Ручной reload делал init начисто как
авторизованный → стол подгружался → всё рисовалось.

Фикс: добавить `await desktops.loadDesktop()` в LoginForm после ожидания
loadComplete, перед навигацией — зеркало проверенного паттерна в
SignUp.vue / init-app / EnableButton (там стол перезагружают, вход забыли).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 17:42:47 +00:00
coopops 5b8d252d6d chore(release): publish
Release / release (push) Successful in 25m49s
Build bootstrap container / build (push) Successful in 40s
Release / publish-packages (push) Failing after 8m19s
Release / trigger-coopenomics-docs (push) Successful in 1s
Release / trigger-mono-docs (push) Successful in 2s
2026-06-09 16:55:44 +00:00
coopops 7bb4f18097 chore(release): publish
Build bootstrap container / build (push) Successful in 3m16s
Release / release (push) Failing after 16m17s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-06-09 16:55:28 +00:00
coopops cba0d41020 fix(desktop): устранить белый экран на первом холодном заходе (PWA boot race)
Гонка на первом заходе в проде: ленивый чанк маршрута падал на import()
из-за конкуренции за сеть с фоновым precache только что установленного SW,
а перехватить ошибку было некому → пустой router-view до ручной перезагрузки.

- FIX-1: router.onError ловит провал загрузки чанка и делает авто-reload на
  целевой путь (с защитой от reload-цикла через sessionStorage).
- FIX-2: регистрацию Service Worker откладываем до window 'load', чтобы
  precache-шторм не конкурировал с первой отрисовкой и догрузкой чанков.
- Диагностика: таймстемп-логи [BOOTRACE] по всей boot-цепочке (initApp,
  router beforeEach/afterEach/onError, App.onMounted/isLoaded, SW register,
  safety-timeout) для грепа порядка инициализации на проде.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:48:51 +00:00
coopops 7ca3c21079 chore(release): publish
Build bootstrap container / build (push) Successful in 3m6s
Release / release (push) Successful in 26m46s
Release / publish-packages (push) Failing after 8m13s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / trigger-mono-docs (push) Successful in 2s
2026-06-09 15:37:07 +00:00
coopops 045204754b chore(release): publish
Build bootstrap container / build (push) Has been cancelled
Release / release (push) Has been cancelled
Release / publish-packages (push) Has been cancelled
Release / trigger-coopenomics-docs (push) Has been cancelled
Release / trigger-mono-docs (push) Has been cancelled
2026-06-09 15:36:50 +00:00
coopops b7c56bd948 fix(payment): добавить legacy-поле kpp в BankAccountDetails
Старые клиенты селектят kpp на BankAccountDetails → GraphQL-валидация
падала (Cannot query field "kpp"). Добавлено nullable-поле в DTO + во все
места Zeus-селектора SDK (иначе сборка десктопа падает на MakeAllFieldsRequired).
Помечено TODO «удалить после 1 августа».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:33:59 +00:00
coopops 3bfd5d219a fix(desktop): карточки платежей не лезут на десктоп + DataRow стек на мобиле
- Реестр платежей: мобильные карточки (.payments-cards.pmt-mobile) показывались
  и на десктопе поверх таблицы. Причина — .payments-cards{display:flex} имел ту
  же специфичность, что одиночный .pmt-mobile{display:none}, и перебивал его по
  порядку источника. Скрытие/показ переведены на двойной селектор
  .payments-cards.pmt-mobile (специфичнее) — на десктопе только таблица.
- Удостоверение/DataRow: горизонтальная пара label|value на телефоне (≤599px)
  стекается в одну колонку — длинный публичный ключ занимает всю ширину
  карточки вместо узких ~150px и больше не рвётся в столбик по буквам.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:27:01 +00:00
coopops 7c01011a51 [@ant] fix(push): обработчик push в генерируемом SW + видимые логи доставки
Корень «push отправлен, но не виден»: workboxMode GenerateSW игнорирует
sourceFiles.serviceWorker, поэтому push/notificationclick из
custom-service-worker.ts в прод не попадали — браузер push получал, но SW его
не показывал (бэкенд честно «sent to N subscription(s)»). Доказано: прод
/service-worker.js содержит 0 push-обработчиков.

- desktop: public/push-sw.js (push → showNotification, notificationclick →
  focus/openWindow) + importScripts('push-sw.js') в extendGenerateSWOptions
- controller: «Доставлено»/«Канал пропущен» debug→info + сводка
  «web-push → user: N подписок» — чтобы доставка была видна в логах без БД
2026-06-09 15:25:31 +00:00
coopops 31b6f5805a fix(desktop): убрать посимвольный перенос и наезд бейджей на мобиле
Корень: overflow-wrap:anywhere рвал имена/email по буквам в столбик, а
бейджи статуса воровали ширину у имени рядом.

- BaseBadge: white-space:nowrap + flex-shrink:0 — бейдж не сжимается/не
  переносится (фикс глобально для всех экранов).
- components.css мобильный .table: anywhere → break-word (перенос по словам).
- Реестр платежей (pay-card): имя обрезается «…», сумма flex-shrink:0,
  тип переносится по словам.
- Реестр пайщиков (ParticipantCard): имя/аккаунт/email обрезаются «…»;
  попутно FontAwesome-иконка аватара → Material person.
- Журнал уведомлений: восстановлен задуманный горизонтальный скролл
  таблицы на мобиле (перебит глобальный min-width:0!important), 7 колонок
  с кнопкой действия больше не схлопываются в буквы-в-столбик.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:16:32 +00:00
coopops 5cf6c20ff2 chore(release): publish
Release / publish-packages (push) Has been cancelled
Release / trigger-coopenomics-docs (push) Has been cancelled
Release / trigger-mono-docs (push) Has been cancelled
Release / release (push) Has been cancelled
Build bootstrap container / build (push) Has been cancelled
2026-06-09 14:28:38 +00:00
coopops ef3fffecda chore(release): publish
Build bootstrap container / build (push) Successful in 3m9s
Release / release (push) Successful in 24m4s
Release / trigger-coopenomics-docs (push) Has been cancelled
Release / trigger-mono-docs (push) Has been cancelled
Release / publish-packages (push) Has been cancelled
2026-06-09 14:28:02 +00:00
coopops fd9b14a98b chore(release): publish
Build bootstrap container / build (push) Successful in 3m26s
Release / release (push) Successful in 23m22s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-06-09 14:27:46 +00:00
coopops 4a39c976a1 fix(desktop): ужать зазор между «Выйти» и версией в LeftDrawerMenu
Нижний padding rail__signout 12→4px, version padding-top 4→0px,
шрифт версии 11→10px — версия идёт сразу под кнопкой, не висит крупной.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:11:01 +00:00
coopops 7df88f48b7 [@ant] fix(desktop/notifications): панель уведомлений во всю ширину на мобиле
Было: q-menu прижат справа фиксированной шириной 360px → правый край резался
(«Прочитать в…», тексты заявок), снизу горизонтальный скролл. На мобиле (≤600px)
раскрываем меню во всю ширину вьюпорта (left/right 8px), панель width:100%.
2026-06-09 13:42:21 +00:00
coopops 354e2d2be0 [@ant] fix(desktop/notifications): чистка HTML в теле, гейт push-кнопки по реальному SW, нерасползающаяся strip
- тело уведомления приходит с <br>/HTML (шарится с email/push-шаблонами), а
  in-app панель рендерит как текст с переносом \n — нормализую HTML→plain в toItem
- push-strip и кнопку «Включить на устройстве» показываем только когда SW реально
  активен (hasActiveServiceWorker), иначе клик умирал на «Service Worker не активен»;
  в dev/без-PWA strip не рисуем (не путать с «браузер не поддерживает»)
- strip не переносит текст/кнопку на узких экранах (nowrap + flex 0 0 auto)
2026-06-09 13:32:52 +00:00
coopops cb0d6365ba chore(release): publish
Release / release (push) Successful in 27m4s
Build bootstrap container / build (push) Successful in 39s
Release / publish-packages (push) Failing after 8m11s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / trigger-mono-docs (push) Successful in 1s
2026-06-09 13:07:28 +00:00
coopops f608119278 Merge branch 'main' of ssh://git.coopenomics.world:222/C9S/mono
# Conflicts:
#	components/blago-cli/package.json
#	components/boot/package.json
#	components/cleos/package.json
#	components/contracts/package.json
#	components/controller/package-lock.json
#	components/controller/package.json
#	components/cooptypes/package.json
#	components/desktop/package.json
#	components/docs-harness/package-lock.json
#	components/docs-harness/package.json
#	components/docs/package.json
#	components/email-relay/package.json
#	components/factory/package.json
#	components/inter/package.json
#	components/migrator/package.json
#	components/notifications/package.json
#	components/parser/package-lock.json
#	components/parser/package.json
#	components/sdk/package.json
#	components/setup/package.json
#	lerna.json
2026-06-09 13:07:14 +00:00
coopops 3c3b6a76c4 chore(release): publish
Build bootstrap container / build (push) Successful in 3m9s
Release / release (push) Successful in 25m21s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-06-09 13:02:21 +00:00
coopops 8f3fdd49a1 Merge branch 'testnet' of ssh://git.coopenomics.world:222/C9S/mono into testnet
# Conflicts:
#	components/email-relay/package.json
2026-06-09 12:58:16 +00:00
coopops 9ffe184062 [@dark-sun] fix(desktop): реестр платежей — карточки на мобиле вместо ломаной таблицы
На телефоне (≤599px) таблица из 7–8 колонок схлопывалась в нечитаемые
буквы-в-столбик: глобальный канон components.css форсит
.table{min-width:0!important} + overflow-wrap:anywhere на ячейках, что
перебивало scoped min-width/горизонтальный скролл виджета. Канон запрещает
горизонтальный скролл таблиц.

Решение как в реестре документов: десктоп — канон-таблица, мобайл —
карточная раскладка (имя+статус, сумма+направление, тип·дата, действия,
раскрытие деталей). Переключение чисто на CSS @media (SSR-safe).
Заодно иконки направления fa-* → Material (канон запрещает FontAwesome).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:52:42 +00:00
coopops 9ae811dae3 [@ant] fix(desktop/push): device-aware подписка + кнопка переподписки в колокольчике
Корень бага «push приходит только на одно устройство»: autoSubscribe
гейтился store.isSubscribed (= у аккаунта есть подписка на ЛЮБОМ устройстве),
поэтому второе устройство никогда не подписывалось. Введён endpoint текущего
браузера + computed isThisDeviceSubscribed; autoSubscribe теперь идемпотентно
регистрирует каждое устройство. Добавлены resubscribe()/refreshDeviceState()
и строка статуса push с кнопкой «Включить/Переподписать» в виджете колокольчика.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:51:26 +00:00
coopops a77fa945a6 [C28-28][@ant] chore(email-relay): вынести релей из mono в C9S/email-relay
Релей переехал в отдельный репозиторий C9S/email-relay, собирается локально
плейбуком (playbooks/email-relay) — релиз mono его больше не пересобирает.
Прод-образ mono-base (prune --prod) не подходил для standalone-сервиса.

Удалено: components/email-relay, build_service в release.yaml, dev-сервис в
docker-compose. Контроллерный relay-режим (EMAIL_RELAY_URL/TOKEN в
email-channel.adapter + config) ОСТАЁТСЯ — он и активирует отправку через релей.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:47:59 +00:00
Alex Ant fd24efb63a chore(release): publish
Build bootstrap container / build (push) Successful in 3m19s
Release / release (push) Successful in 28m31s
Release / publish-packages (push) Has been cancelled
Release / trigger-coopenomics-docs (push) Has been cancelled
Release / trigger-mono-docs (push) Has been cancelled
2026-06-09 15:49:05 +05:00
Alex Ant 01fc0872ae chore(release): publish
Build bootstrap container / build (push) Has been cancelled
Release / publish-packages (push) Has been cancelled
Release / trigger-coopenomics-docs (push) Has been cancelled
Release / trigger-mono-docs (push) Has been cancelled
Release / release (push) Has been cancelled
2026-06-09 15:48:49 +05:00
Alex Ant 271de60831 Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev 2026-06-09 15:48:32 +05:00
coopops be7e1585b0 [C28-28][@ant] fix(email-relay): ts-node+typescript в dependencies (переживают prune --prod)
Прод-образ mono-base делает `pnpm prune --prod` (Dockerfile:74) — devDeps
вырезаются. ts-node был в devDependencies → в проде `sh: ts-node: not found`,
контейнер в restart-loop, /health не отвечал. controller держит ts-node в
dependencies — повторяем паттерн. (Коммит из закрытого PR #115 не попал в dev,
вношу прямо.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 10:47:15 +00:00
Alex Ant addb527f3c chore(release): publish
Build bootstrap container / build (push) Successful in 3m16s
Release / release (push) Successful in 26m2s
Release / publish-packages (push) Failing after 8m18s
Release / trigger-coopenomics-docs (push) Successful in 3s
Release / trigger-mono-docs (push) Successful in 1s
2026-06-09 14:10:52 +05:00
Alex Ant 9e271a6ce9 chore(release): publish
Build bootstrap container / build (push) Has been cancelled
Release / release (push) Has been cancelled
Release / publish-packages (push) Has been cancelled
Release / trigger-coopenomics-docs (push) Has been cancelled
Release / trigger-mono-docs (push) Has been cancelled
2026-06-09 14:10:33 +05:00
Alex Ant f37605b343 migrations 2026-06-09 14:10:09 +05:00
ant f728563bdb Merge pull request '[C28-28] feat(email-relay): HTTP→SMTP релей для коопов с закрытым SMTP' (#115) from feature/C28-28-email-relay into dev
Reviewed-on: #115
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-09 07:54:56 +00:00
coopops 468c946610 [C28-28][@ant] fix(email-relay): убрать mailpit из compose, env_file как coopback
Typecheck / desktop (pull_request) Successful in 13m51s
Typecheck / controller (pull_request) Has been cancelled
mailpit был только локальным SMTP-перехватчиком для теста — в поставке не нужен.
email-relay сам ходит в реальный SMTP через nodemailer; конфиг (RELAY_TOKEN,
SMTP_*) выносим в components/email-relay/.env по образцу coopback, без отдельного
контейнера и inline-параметров.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 07:51:58 +00:00
coopops 7e2b26a02e [C28-28][@ant] feat(email-relay): HTTP→SMTP релей для коопов с закрытым SMTP
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Хостинги коопов режут исходящий SMTP → письма controller'а уходят в timeout
(раньше хабом был Novu, его убрали). Новый компонент @coopenomics/email-relay
принимает письма по HTTPS с Bearer-токеном и форвардит по SMTP. Ставится на
сервер с открытыми портами (плейбук в playbooks/email-relay).

- components/email-relay: express + nodemailer, POST /send + GET /health,
  Bearer-токен (constant-time), опц. IP-allowlist, конфиг из ENV
- controller: email-channel.adapter получил relay-режим — если задан
  EMAIL_RELAY_URL, письмо уходит POST'ом на релей; иначе прямой SMTP как
  раньше (opt-in, без регрессии)
- docker-compose: дев-сервисы email-relay + mailpit (SMTP-перехватчик для E2E)
- release.yaml: сборка образа dicoop/email-relay

E2E проверен локально: controller-формат письма → релей → SMTP → mailpit,
тело с <br> и заголовки целы; 401 на отсутствие/неверный токен, 400 на пустое тело.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 07:42:15 +00:00
coopops a76f875725 [C28-27][@ant] fix(desktop): in-app уведомления видны на тёмной теме
Build bootstrap container / build (push) Successful in 3m2s
В колоколе уведомлений на тёмной теме элементы сливались с фоном панели:
разделители на --p-line (0.06 white) почти невидимы, а непрочитанные
отличались лишь цветом текста + точкой — всё тонуло в --p-surface. На светлой
контраст тёмного текста на белом спасал.

Канон-токенами (обе темы, светлую не ухудшает): панели — рамка --p-line-1
для контура; разделители между уведомлениями — --p-line-1 (заметнее); у
непрочитанных подложка --p-primary-soft + левый акцент-бар --p-primary,
hover/focus — --p-primary-line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 07:22:39 +00:00
coopops 1d235e5caf chore(release): publish
Build bootstrap container / build (push) Successful in 3m5s
Release / release (push) Successful in 24m54s
Release / publish-packages (push) Failing after 8m23s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / trigger-mono-docs (push) Successful in 2s
2026-06-09 07:18:39 +00:00
coopops 98b9c03977 chore(release): publish
Build bootstrap container / build (push) Successful in 3m5s
Release / release (push) Successful in 26m40s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-06-09 07:09:56 +00:00
ant c05daad8fd Merge pull request 'feat(pwa): надёжное оповещение об обновлении через version-watch' (#114) from feat/pwa-version-watch into dev
Reviewed-on: #114
2026-06-09 07:06:53 +00:00
ant c3ff628dac Merge pull request 'feat(desktop): статусы пайщиков бейджами + удаление непринятых аккаунтов из реестра' (#112) from fix/participants-registry-status-badges into dev
Build bootstrap container / build (push) Successful in 3m7s
Reviewed-on: #112
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-09 06:52:52 +00:00
coopops 3ba3a867ad feat(pwa): надёжное оповещение об обновлении через version-watch
Typecheck / desktop (pull_request) Successful in 15m5s
Typecheck / controller (pull_request) Successful in 13m12s
Заменяет ненадёжный триггер от lifecycle service worker'а (не срабатывал
на iOS standalone PWA, при первой установке и когда бинарь SW не менялся)
на сравнение версий: запечённая в бандл package-версия (process.env.APP_VERSION)
сверяется с self-report SSR-ноды (/version). При расхождении — существующий тост.

- quasar.config: APP_VERSION из package.json в build.env; middleware 'version'
- src-ssr/middlewares/version.ts: self-report /version (JSON, no-store)
- entities/AppVersion: стор useUpdateWatch (реестр компонентов, сейчас shell),
  опрос /version раз в 5 мин + при возврате на вкладку; SPA-safe (нет ноды -> no-op)
- init-app: старт version-watch рядом с systemMonitoring
- register-service-worker: убран SW-триггер; applyUpdate устойчив (waiting -> SKIP_WAITING, иначе reload)
- LeftDrawerMenu: версия приложения в футере рейла

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 06:46:04 +00:00
coopops 15813d91ef [@ant] fix(desktop): правка текста предупреждения при удалении registered-аккаунта
Typecheck / desktop (pull_request) Successful in 15m1s
Typecheck / controller (pull_request) Successful in 13m41s
«исчезнет» → «исчезает»; убрано предложение про отзыв заявки на повестке —
кнопка доступна только когда аккаунт уже в цепи (after оплаты), решение совета
тут неприменимо.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 06:44:31 +00:00
coopops 955142cf84 [@ant] feat: удаление любого непринятого аккаунта из реестра (гард только participant_account)
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Прежний allow-list статусов исключал Registered — но именно туда попадают
отклонённые советом и оплаченные брошенные регистрации (отказ/возврат
оставляют статус registered; failed/refunded users.status никем не
выставляются). То есть чистка не работала для своей же цели «убрать
незавершённые/отклонённые регистрации (тестеры)».

- backend deleteAccount: убран allow-list по статусу, остаётся единственный
  гард — НЕ принят в кооператив (нет participant_account).
- frontend isDeletable: !participant_account. Кнопка удаления теперь и у
  Registered (оплачен/на рассмотрении совета/отклонён/возврат).
- confirm-диалог: для blockchain-registered (Registered) предупреждение, что
  on-chain аккаунт неудаляем (имя в цепи занято навсегда) + про отзыв заявки
  на повестке, если ещё на рассмотрении совета.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 06:32:10 +00:00
coopops 7efbf90c9b [C28-27][@ant] fix(desktop): таблицы переносят содержимое, не скроллят по горизонтали
На мобиле (и в узком окне десктопа) canon-таблицы выходили за ширину: левая
колонка обрезалась, появлялся внутренний горизонтальный скролл — из-за
фиксированной min-width (780–1000px), которую виджеты ставят локально, а
скелетон inline-стилем.

Централизовал в каноне components.css: `.table { min-width: 0 !important }`
снимает форс-ширину на всех экранах (!important — иначе scoped/inline
перебивают канон), таблица всегда влезает (width:100%) и переносит содержимое
на следующую строку вместо скролла. На узких экранах (≤599px) дополнительно
table-layout:auto + перенос в ячейках + плотнее паддинги, чтобы многоколоночная
таблица читалась переносом, а не клиппингом.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 06:29:07 +00:00
coopops 0e9542fff4 [@ant] feat(desktop): статусы пайщиков бейджами в реестре + enrich registration_payment
Typecheck / desktop (pull_request) Successful in 13m49s
Typecheck / controller (pull_request) Has been cancelled
- ParticipantsTable/ParticipantCard: галочка «Активен» → BaseBadge с
  человекочитаемым статусом; колонка «Активен» → «Статус».
- entities/Account/lib/accountStatus.ts: getAccountStatusBadge — сводит
  participant_account (accepted|blocked) + provider_account.status +
  отказ совета (Registered + возврат взноса) → «Отклонён советом».
- controller getAccounts: enrich registration_payment через this.getAccount,
  иначе на списке реестра отказ совета неотличим от «ожидает совет».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 06:14:46 +00:00
ant 8ffe4bea94 Merge pull request 'feat: Центр уведомлений DC v3 (замена Novu) [C28-22…27] — код-комплит, 6/6 эпиков' (#60) from feat/notification-center-dc-v3 into dev
Reviewed-on: #60
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-08 08:37:29 +00:00
coopops 70bd1323c7 chore(release): publish
Build bootstrap container / build (push) Successful in 3m8s
Release / release (push) Successful in 25m6s
Release / publish-packages (push) Failing after 8m25s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / trigger-mono-docs (push) Successful in 1s
2026-06-08 07:03:58 +00:00
coopops 765af3cbef chore(release): publish
Build bootstrap container / build (push) Successful in 2m57s
Release / release (push) Successful in 25m7s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-06-08 07:03:41 +00:00
ant 9cdee035b9 Merge pull request 'fix(controller): приложения-оферты ЦПП к заявлению на повестке (wallet::signagree)' (#108) from fix/agenda-program-offer-links into dev
Reviewed-on: #108
2026-06-08 07:03:13 +00:00
ant 9553697f2b Merge pull request 'fix(desktop): чистить технические детали ассертов цепи в тостах ошибок' (#110) from fix/sanitize-blockchain-error-toast into dev
Reviewed-on: #110
2026-06-08 07:02:47 +00:00
ant 124782a8d9 Merge pull request 'fix(controller): не фиксировать возврат паевого взноса в БД при ошибке блокчейна' (#109) from fix/wallet-withdraw-no-phantom-payment into dev
Reviewed-on: #109
2026-06-08 07:02:39 +00:00
coopops f10b49ac8f [@ant] fix(desktop): чистить технические детали ассертов цепи в тостах ошибок
Typecheck / desktop (pull_request) Successful in 13m28s
Typecheck / controller (pull_request) Successful in 12m29s
Пользователю в тост летел сырой ассерт контракта, напр.
`walletop TRANSFER: недостаточно средств на кошельке w.wal.share`.
Ему не нужны ни scope-префикс операции, ни служебное имя кошелька — он
нажал конкретную кнопку и так понимает, о каком кошельке речь.

Добавлен универсальный санитайзер sanitizeBlockchainError, встроенный в
единую точку показа ошибок FailAlert:
- снимает ведущий технический scope-префикс (`walletop TRANSFER:`,
  `ledger2::apply:`, `o.mkt.supply:`) — распознаёт идентификатор с
  `.`/`::` либо слово+ALLCAPS-опкод, кириллицу не трогает;
- убирает служебные имена ledger2-кошельков (`w.wal.share` и т.п.);
- нормализует пробелы и ставит заглавную первую букву.

Результат для примера выше — «Недостаточно средств на кошельке».
Обычные (нетехнические) сообщения проходят без изменений.

Полный технический текст остаётся разработчикам: FailAlert дублирует
сырой message в console.error, бэкенд пишет его в логи контроллера.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 06:42:22 +00:00
coopops 75548036f4 chore(release): publish
Release / release (push) Successful in 24m41s
Build bootstrap container / build (push) Successful in 38s
Release / publish-packages (push) Failing after 8m22s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / trigger-mono-docs (push) Successful in 1s
2026-06-08 06:27:19 +00:00
coopops ec0427c778 chore(release): publish
Build bootstrap container / build (push) Successful in 3m8s
Release / release (push) Failing after 14m44s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-06-08 06:27:02 +00:00
coopops 29417dd425 [@ant] fix(controller): не фиксировать возврат паевого взноса в БД при ошибке блокчейна
Typecheck / desktop (pull_request) Successful in 13m22s
Typecheck / controller (pull_request) Successful in 15m49s
Баг: при «Получить возврат» с суммой больше L3-баланса платёж писался в
gateway-БД (AWAITING_AUTHORIZATION) ДО on-chain транзакции. Контракт wallet
отклонял заявку (недостаточно L3-средств), а в разделе «Платежи» оставался
висеть фантомный исходящий платёж со статусом FAILED.

Фикс — порядок операций в WalletInteractor.createWithdraw:
1. prepareWithdraw — все валидации (символ, дубликат hash, платёжный метод)
   и сборка записи, БЕЗ записи в БД;
2. on-chain wallet::createwthd — при недостатке средств падает здесь,
   ни одной записи о платеже не создаётся;
3. persistWithdraw — фиксация платежа в БД только после успешной заявки.

GatewayInteractor.createWithdraw разбит на prepareWithdraw/persistWithdraw
(старый combined-метод сохранён как обёртка). Порт и оба адаптера обновлены.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 06:00:02 +00:00
coopops ca1d1ec1a2 [@ant] fix(controller): приложения-оферты ЦПП к заявлению на повестке (wallet::signagree)
Typecheck / desktop (pull_request) Successful in 14m13s
Typecheck / controller (pull_request) Successful in 13m2s
Программные оферты (program_id > 0) с Эпика 2 уходят на цепь через
wallet::signagree (commit 21e94ce0ed), а не soviet::sndagreement —
soviet::newagreement по ним не создаётся. Агрегатор пакета документов
(document-package-v1) искал приложения к заявлению только по
soviet::newagreement, поэтому подписанная оферта молча выпадала из links
и не отображалась как приложение к заявлению на повестке совета (при этом
сам документ виден в реестре).

Резолв каждого linkHash вынесен в общий хелпер buildLinkedAggregate;
носитель подписи ищется сначала в soviet::newagreement, затем фолбэком в
wallet::signagree. Унифицированы три дублировавшихся цикла (statement /
decision / acts). Добавлен регрессионный unit-тест.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 05:49:07 +00:00
coopops c28bb81c25 [C28-27][@ant] fix: in-app уведомления — перенос строки вместо литерального <br>
In-app тело инбокса рендерится как текст ({{ n.description }}, без v-html —
payload несёт ФИО/заголовки, v-html = XSS-вектор), поэтому HTML-тег <br> из
шаблона показывался строкой «№C53E<br>От: …». Перевёл 2 in-app шаблона
(new-agenda-item, approval-request) с <br> на \n и добавил white-space:
pre-line на .notification-center__item-desc (line-clamp:2 сохранён).

Push не трогаю — его тела уже plain-text без тегов, отображается верно.
Email оставлен на HTML (<br>/<strong>) — рендерится почтовым клиентом.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 05:28:04 +00:00
coopops 4df811d40f chore(desktop): пометить stripLegacyBankKpp на удаление после 01.09.2026
Build bootstrap container / build (push) Successful in 2m18s
Временный shim очистки persisted-localStorage от устаревшего «КПП банка».
К 01.09.2026 старые незавершённые черновики регистрации истекут — утилиту
и оба вызова (CreateUser/AddUser model) можно удалить.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 05:23:53 +00:00
coopops 54e27df7f8 chore(release): publish
Build bootstrap container / build (push) Successful in 3m27s
Release / release (push) Successful in 26m49s
Release / publish-packages (push) Failing after 28m24s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / trigger-mono-docs (push) Successful in 1s
2026-06-08 05:13:30 +00:00
coopops b7da867198 chore(release): publish
Build bootstrap container / build (push) Successful in 3m19s
Release / publish-packages (push) Has been cancelled
Release / trigger-coopenomics-docs (push) Has been cancelled
Release / trigger-mono-docs (push) Has been cancelled
Release / release (push) Has been cancelled
2026-06-08 05:13:14 +00:00
coopops 6906d3fad0 fix(desktop): не отправлять устаревший «КПП банка» из persisted-localStorage
Build bootstrap container / build (push) Successful in 3m26s
Пайщики, начавшие регистрацию ИП/юрлица ДО удаления поля «КПП банка»
(commit 93b94326), имеют это значение в persisted-localStorage стора
регистратора (useRegistratorStore, persist:true). При построении мутации
RegisterAccount/AddParticipant org/entrepreneur объект уходил целиком,
включая bank_account.details.kpp → BankAccountDetailsInput его больше не
принимает → GRAPHQL_VALIDATION_FAILED, форму не отправить.

Защитный strip stripLegacyBankKpp() срезает kpp из bank_account.details
на входе в мутацию (createUser/signup и addUser/председатель). КПП самой
организации (details.kpp) не трогаем — легитимный реквизит юрлица.

Read-путь (старый фронт запрашивает details{kpp}) лечится перезагрузкой
страницы — селекторы SDK уже без kpp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 05:09:07 +00:00
coopops a4e5abe4fa chore(release): publish
Build bootstrap container / build (push) Successful in 3m36s
Release / release (push) Successful in 25m54s
Release / publish-packages (push) Failing after 8m23s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / trigger-mono-docs (push) Successful in 2s
2026-06-07 15:29:25 +00:00
coopops ef393e9724 chore(release): publish
Build bootstrap container / build (push) Successful in 3m34s
Release / release (push) Successful in 25m2s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-06-07 15:28:58 +00:00
coopops f2e3165b54 Merge branch 'testnet' of ssh://git.coopenomics.world:222/C9S/mono into testnet
# Conflicts:
#	components/notifications/package.json
2026-06-07 15:27:59 +00:00
coopops 5b7e1015cd [C28-27][@ant] fix(desktop): 4 ошибки типов после де-Novu — тайпчек зелёный
Typecheck / desktop (pull_request) Successful in 13m26s
Typecheck / controller (pull_request) Successful in 13m11s
CI vue-tsc (PR #60) после фикса lockfile дошёл до тайпчека и поймал хвосты:
- src-ssr/middlewares/{injectEnv,generateConfig}.ts: NOVU_APP_ID/BACKEND_URL/
  SOCKET_URL слались в объект EnvVars, откуда поля удалены де-Novu → TS2353.
  Убрал мёртвые строки.
- NotificationJournalWidget.vue: getName() возвращает string|undefined
  (organization_data?.short_name) → .replace на possibly-undefined (TS2532).
  Обернул в (getName(account) ?? '').
- NotificationCenter/model/index.ts: n.createdAt — DateTime-скаляр SDK типа
  unknown, NotificationItem.date ждёт string|Date (TS2322). Каст as string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:58:24 +00:00
coopops 4ffc3b4616 [C28-27][@ant] fix(desktop): 4 ошибки типов после де-Novu — тайпчек зелёный
CI vue-tsc (PR #60) после фикса lockfile дошёл до тайпчека и поймал хвосты:
- src-ssr/middlewares/{injectEnv,generateConfig}.ts: NOVU_APP_ID/BACKEND_URL/
  SOCKET_URL слались в объект EnvVars, откуда поля удалены де-Novu → TS2353.
  Убрал мёртвые строки.
- NotificationJournalWidget.vue: getName() возвращает string|undefined
  (organization_data?.short_name) → .replace на possibly-undefined (TS2532).
  Обернул в (getName(account) ?? '').
- NotificationCenter/model/index.ts: n.createdAt — DateTime-скаляр SDK типа
  unknown, NotificationItem.date ждёт string|Date (TS2322). Каст as string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:58:13 +00:00
coopops e199eb4913 [C28-27][@ant] fix(lockfile): пересборка после удаления @novu/js — CI frozen-lockfile
Typecheck / desktop (pull_request) Failing after 8m22s
Typecheck / controller (pull_request) Has been cancelled
Удаление @novu/js из desktop/package.json (b500789dd6) не сопровождалось
пересборкой pnpm-lock.yaml → CI install --frozen-lockfile падал
ERR_PNPM_OUTDATED_LOCKFILE до шага typecheck. Пересобрал lockfile-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:45:05 +00:00
coopops e763adae48 Merge feat/notification-center-dc-v3 into dev — Центр уведомлений (C28-22…27)
DC v3: каталог @coopenomics/notifications, роутер outbox + worker доставки
(email/in-app/web-push), стол председателя (журнал + колокол), удаление Novu.

Чиню рассинхрон lockfile: feat-ветка убрала @novu/js из desktop/package.json,
но pnpm-lock.yaml не пересобрала → CI --frozen-lockfile падал
ERR_PNPM_OUTDATED_LOCKFILE. pnpm install --lockfile-only — @novu/js удалён,
lockfile согласован с package.json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:40:52 +00:00
coopops 4f67231b3f [C28-27][@ant] fix: web-push без подписки — мягкий пропуск, не ошибка
Typecheck / desktop (pull_request) Failing after 4m49s
Typecheck / controller (pull_request) Failing after 4m47s
Получатель без активной push-подписки (не дал разрешения / нет PWA / 410)
давал {delivered:false,error} → worker ретраил 5× → терминальный FAILED:
ложные «Ошибка» в журнале и warn в логах для всех, у кого push не настроен
(в dev SW не регистрируется без ENABLE_PWA_DEV → у всех).

Ввёл исход skipped в ChannelDeliveryResult: канал неприменим к получателю.
Адаптер web-push возвращает skipped при 0 подписок / отсутствии username.
Worker гасит такую строку в canceled — без ретраев, без записи попытки в
журнал, без error-лога (debug); причину кладёт в lastError → тултип
«Отменено» на столе председателя. Гейт на слое отправки (состояние подписки
авторитетно в момент доставки), роутер outbox остаётся чистым.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:26:25 +00:00
coopops a2d5750449 [C28-27][@ant] feat: журнал — получатель ФИО + видимость ошибок доставки
Typecheck / desktop (pull_request) Failing after 4m49s
Typecheck / controller (pull_request) Failing after 4m46s
Получатель в журнале:
- две строки — ФИО (резолв username→account через getName на фронте, кэш
  на компонент) + AccountBadge с username и копированием. DTO журнала несёт
  только username/subscriber_id, поэтому ФИО тянем getAccount'ом. Нет ФИО
  (приватные данные недоступны) → одна строка с username.

Видимость ошибки доставки:
- worker раньше логировал только исключение тика; провалы отправки шли лишь
  в БД → при падающем канале в логах пусто и не видно, что worker жив.
  Добавлено: лог старта (onModuleInit), debug-heartbeat тика с числом строк,
  warn на ретрай и error на терминальный провал — с контекстом
  (workflow/канал/получатель/попытка) и текстом ошибки.
- в журнале на ячейке статуса «Ошибка» — q-tooltip с lastError (поле уже
  отдаётся DTO/селектором), курсор help.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:15:18 +00:00
coopops 423c1d0984 [C28-27][@ant] fix: убрать «Показать все» из колокола + бейдж не наезжает
Typecheck / desktop (pull_request) Failing after 4m48s
Typecheck / controller (pull_request) Has been cancelled
- Футер «Показать все» удалён из личного колокола: журнал — админский
  (стол председателя), пайщику туда не нужно; председатель открывает
  журнал через пункт меню. Колокол = свежие из инбокса + «Прочитать все».
- Бейдж счётчика вытолкнут за угол кнопки через transform translate(45%,-45%)
  — больше не «наезжает» на глиф колокола.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:08:43 +00:00
coopops f997c77315 [C28-27][@ant] fix: журнал в меню председателя + колокол не прыгает (скелетон/фикс-высота)
Typecheck / desktop (pull_request) Failing after 4m49s
Typecheck / controller (pull_request) Failing after 4m46s
- Журнал уведомлений зарегистрирован в реальном меню стола председателя
  (extensions/chairman/install routes[0].children) — был воткнут в мёртвый
  src/desktops/Chairman/model (не в реестре расширений), потому страница
  была недостижима. Дубль из мёртвого манифеста убран.
- Колокол: спиннер заменён канон-скелетоном (.skel), тело фиксированной
  высоты (~4 уведомления, max 60vh) — панель больше не «прыгает» при
  скелетон → меньше элементов → пусто.
- Футер: вместо неясной пагинации «Все загружены/Показать ещё» —
  навигация «Показать все →» в журнал (только для председателя; q-menu
  закрывается при переходе).
- Бейдж счётчика смещён в угол колокола (-4/-4) — убран «наезд» на иконку.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:53:12 +00:00
coopops b653a3cbd5 Merge remote-tracking branch 'origin/dev' into feat/notification-center-dc-v3
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
# Conflicts:
#	components/notifications/package.json
2026-06-07 12:24:50 +00:00
Alex Ant aa9b677d9c chore(release): publish
Build bootstrap container / build (push) Successful in 3m25s
Release / release (push) Successful in 26m36s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-06-07 17:09:57 +05:00
ant d7fda5cca6 Merge pull request 'feat(account,desktop): удаление пайщика в незавершённом регистрационном статусе' (#106) from feat/delete-inactive-participant into dev
Reviewed-on: #106
2026-06-07 12:03:44 +00:00
ant dc564941d3 Merge pull request 'fix(chatcoop): новый формат имени LiveKit-комнаты Element Call — секретарь не входил в звонки' (#107) from fix/chatcoop-livekit-room-name into dev
Reviewed-on: #107
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-07 12:03:33 +00:00
coopops e98baeccc2 [chatcoop][@ant] fix(secretary): новый формат имени LiveKit-комнаты Element Call
Typecheck / desktop (pull_request) Successful in 13m37s
Typecheck / controller (pull_request) Successful in 13m8s
Публичный call.element.io перешёл на слот-протокол MatrixRTC: имя LiveKit-комнаты
теперь считает lk-jwt-service как unpaddedBase64(sha256(JSON.stringify([roomId,
"m.call#ROOM"]))), а не старое sha256(roomId+"|m.call#ROOM"). Старая формула
перестала матчить webhook'и → секретарь молча игнорировал все звонки
(«Комната ... не в реестре транскрипции»).

Матчинг теперь сверяет оба формата (текущий + legacy для старых клиентов Element).
Тест на живой паре с прода (комната «Ферма на Паях», созвон 07.06).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 11:54:31 +00:00
coopops fc9a05fe24 [del-participant][@ant] fix(account,desktop): гейт удаления по статусу, не по blockchain_account
Typecheck / desktop (pull_request) Successful in 13m26s
Typecheck / controller (pull_request) Successful in 13m6s
Кнопка удаления не показывалась НИ у кого, и сервер отверг бы все удаления.
Причина: гейт опирался на blockchain_account, но on-chain аккаунт заводится рано
и присутствует уже у статуса Created — то есть у всех. Плюс на фронте сравнение
шло со строками в нижнем регистре, а GraphQL отдаёт enum UserStatus с большой
буквы (Created/Joined/...).

- interactor.deleteAccount: убран blockchain_account из guard; различитель —
  participant_account (принят) + статус-allow-list (домен lowercase).
- ParticipantsTable.isDeletable: убран blockchain_account; allow-list через
  Zeus.UserStatus (Created/Joined/Payed/Failed/Refunded). Registered (после приёма
  взноса), Active, Blocked — не удаляются.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 09:06:56 +00:00
ant a57763a535 Merge pull request 'feat(ledger2,registrator): двухфазный учёт регистрационного взноса через счёт 76' (#105) from feat/registration-ledger2-suspense into dev
Reviewed-on: #105
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-07 08:33:11 +00:00
coopops 3faf9049f4 fix(registration): возврат заводится на каждом цикле отказа, а не только на первом
Typecheck / desktop (pull_request) Successful in 13m32s
Typecheck / controller (pull_request) Successful in 13m7s
Idempotency-guard listener'а пропускал создание возврата при наличии ЛЮБОГО
REGISTRATION_REFUND. После первого цикла отказа возврат cycle1 оставался в БД
(его hash = registration_hash = sha256(username) детерминирован и совпадает с
новым), поэтому на втором цикле возврат не создавался, latest refund оставался
старше нового платежа, и фронт зависал на «платёж принят».

Guard теперь пропускает только возврат ТЕКУЩЕГО цикла — свежее текущего
вступительного платежа. Детекция цикла по дате — как в account.interactor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 08:30:16 +00:00
coopops a46d177242 fix(registration): повторная оплата генерит новый платёж + единое назначение с НДС-оговоркой
Typecheck / desktop (pull_request) Successful in 14m15s
Typecheck / controller (pull_request) Has been cancelled
Повторная подача после отказа совета:
- createRegistrationPayment больше не переиспользует старый COMPLETED рег-платёж,
  если возврат свежее его (цикл закрыт) — заводит новый ордер со свежим QR,
  иначе пайщик видел исполненный QR, а в реестре совета платёж не появлялся.
- ReadStatement сбрасывает согласие с Уставом при входе в раздел (как doc-галочки).
- GenerateAccount сбрасывает «Я сохранил ключ» при входе в шаг (q-step не размонтируется).

Назначение платежа (memo) — единый полный источник:
- оговорка «НДС не облагается.» вынесена в константу VAT_EXEMPT_NOTE и добавлена во
  все 4 назначения (вступительный, паевой, возврат паевого, возврат рег-взноса);
- qrpay/sberpoll больше НЕ дописывают «. Без НДС.» к Purpose — назначение целиком
  в memo, без задвоения.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 08:07:35 +00:00
coopops f666007647 fix(registration): кнопка «Подать заявку заново» только после завершения возврата
Typecheck / desktop (pull_request) Successful in 13m31s
Typecheck / controller (pull_request) Successful in 13m6s
Два явных шага возврата при отказе совета (по просьбе): пока касса не подтвердила
исходящий платёж — показываем «возврат выполняется» без кнопки; после подтверждения —
«взнос возвращён» + кнопка. Иначе ранний клик упирался в reguser (карточка ещё не
снята) и падал.

- getAccount: registration_payment.status = PROCESSING пока возврат идёт,
  REFUNDED по завершении (refundpay снял карточку — повторная подача возможна).
- WaitingRegistration: ветка isCouncilRefundPending (PROCESSING) — баннер + песочные
  часы, без кнопки; isCouncilDeclined (REFUNDED) — баннер + «Подать заявку заново».
- fixData: ошибка через extractGraphQLErrorMessages вместо e.message
  (раньше показывало «undefined» — у GraphQL-ошибки SDK message лежит глубже).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:33:18 +00:00
coopops 3af96ab8f8 feat(registrator): повторная подача заявления на том же аккаунте после отказа совета
Typecheck / desktop (pull_request) Successful in 13m36s
Typecheck / controller (pull_request) Has been cancelled
Вариант 1: после отказа совета и возврата взноса несостоявшийся пайщик может
подать заявку заново на том же аккаунте (а не упереться в тупик).

Контракт:
- reset_account_card: refundpay (и миграц.ветка declinereg) при закрытии кандидата
  снимают карточку участника (type="", storages) — иначе повторный reguser падает
  «повторное получение невозможно». Аккаунт и ключи сохраняются.

Бэкенд:
- registerBlockchainAccount: CreateAccount шлётся только если eosio-аккаунта ещё нет
  (при повторной подаче он уже существует — иначе двойное создание падало бы).
- resetRegistration: ветка «отказ совета» — пропускает pre-payment гарды (аккаунт в
  цепи и взнос принят здесь нормальны), требует возврат COMPLETED (карточку снял
  refundpay), сбрасывает кандидата + user.status=created. Старые платежи не удаляет.
- getAccount: сигнал REFUNDED только если возврат свежее последнего рег-платежа —
  после повторной подачи новый REGISTRATION перекрывает старый отказ.

Повторная подпись соглашений идемпотентна (signagree перезаписывает программу,
sndagreement падает лишь на confirmed — при отказе совета соглашение не confirmed).

Стандарт p.reg.refund: отмечено освобождение аккаунта для повторной подачи.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:16:32 +00:00
coopops 6755c04bc0 feat(registrator): возврат рег-взноса по подтверждению кассы (callback), а не на decline совета
Typecheck / desktop (pull_request) Successful in 13m25s
Typecheck / controller (pull_request) Successful in 13m2s
Деньги держатся на счёте 76 (w.reg.pend) до фактического исходящего возврата
кассой — симметрично возврату паевого в wallet (createoutpay → outcomplete).

Контракт:
- declinereg: вместо немедленной проводки 76/51 ставит кандидата в refunding и
  создаёт исходящий платёж gateway::createoutpay (callback refundpay/declinerfnd);
  миграц.ветка (нет баланса на 76) — старое поведение, erase сразу.
- refundpay (новый, callback gateway): обратная проводка Дт 76 / Кт 51 (o.reg.refund,
  BURN w.reg.pend) + закрытие кандидата — на подтверждении кассой.
- declinerfnd (новый): страж check(false) — отмена возврата невозможна.
- фикс инвертированного eosio::check в declinepay (всегда падал «Кандидат не найден»).

Стандарты: p.reg.refund — двухшаговый граф payed→refunding→refunded
(o.reg.refund triggered_by refundpay); p.reg.accept — ветка отказа без своей проводки.

Бэкенд:
- статус refunding в userStatus + MonoAccountStatusDomainInterface (без рассинхрона).
- RegistrationDeclineListener: hash платежа-возврата = registration_hash (мост к
  on-chain outcome).
- gateway.interactor.processOutgoingPayment: REGISTRATION_REFUND зовёт completeOutcome
  (→ outcomplete → refundpay) с миграц-fallback (нет on-chain объекта → off-chain).
- getAccount: при наличии возврата отдаёт registration_payment.status=REFUNDED.

Фронт: WaitingRegistration реагирует на отказ совета (REFUNDED) — баннер
«Совет отказал, взнос возвращается» + «Подать заявку заново». union статуса +refunding/refunded.

Миграц.код помечен «снять условие после 30.07.2026».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 08:26:09 +00:00
coopops 4c0938bdd8 feat(registrator): миграционный fallback для зависших кандидатов (снять после 30.07.2026)
Typecheck / desktop (pull_request) Successful in 13m34s
Typecheck / controller (pull_request) Has been cancelled
На момент релиза двухфазного учёта будут кандидаты в статусе payed, чей платёж
принят по СТАРОМУ пути — без o.reg.inpay, то есть без баланса на w.reg.pend (76).
Новые confirmreg/declinereg делали бы TRANSFER/BURN с пустого суспенса → падение
«недостаточно средств».

Fallback по наличию баланса кандидата на w.reg.pend (helper
get_registration_pending_balance читает ledger2::userwallets cross-contract):
  - есть баланс (новый путь): confirmreg → o.reg.setmin/setent (76→80/86),
    declinereg → o.reg.refund (BURN 76, Dr 76/Cr 51);
  - нет баланса (принят до релиза): confirmreg → o.reg.putmin/payent (прямой
    ISSUE Dr 51/Cr 80|86, как раньше), declinereg → без on-chain проводки
    (возврат банковским переводом из бэкенда, как было).

Весь блок помечен «снять условие после 30.07.2026»: к этому сроку все зависшие
на момент релиза кандидаты гарантированно получат решение совета.

registrator.wasm пересобран.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 07:33:47 +00:00
coopops 890c386118 [del-participant][@ant] feat(account,desktop): удаление пайщика в незавершённом регистрационном статусе
Typecheck / desktop (pull_request) Successful in 13m28s
Typecheck / controller (pull_request) Successful in 13m11s
Реестр пайщиков накапливает брошенные/отклонённые регистрации (тестеры,
повторные попытки) и держит занятым e-mail, мешая перерегистрации. Включаем
кнопку удаления, но строго ограничиваем безопасными случаями.

Backend:
- account.interactor.deleteAccount: guard — запрет, если есть blockchain_account
  или participant_account (любой on-chain след), и allow-list off-chain статусов
  {created, joined, payed, failed, refunded}. deleteUserByUsername удаляет только
  PG-строку users, не цепь, поэтому on-chain аккаунты осиротеют — их не трогаем.
  Граница совпадает с resetRegistration. fail-closed: blocked/active/registered и
  любой будущий не-регистрационный статус — удалить нельзя.
- account.resolver: активирована мутация deleteAccount (роль chairman).

SDK: активирован Mutations.Accounts.DeleteAccount (Boolean), регенерирован zeus.

Desktop (Реестр пайщиков):
- entities/Account: api.deleteAccount + store.deleteAccount (локальное удаление
  строки без перезапроса).
- ParticipantsTable/ParticipantCard: деструктивная кнопка рядом со статусом
  «Активен», видна только для удаляемых аккаунтов (isDeletable зеркалит серверный
  guard) + BaseDialog-подтверждение необратимого действия.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 07:26:42 +00:00
coopops 01ac09599e feat(ledger2,registrator): двухфазный учёт регистрационного взноса через счёт 76
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Раньше регистрационный взнос вставал на учёт одношагово только на одобрении
совета (confirmreg: Dr 51/Cr 80 + Dr 51/Cr 86), а приём денег кассой
(confirmpay) и отказ совета (declinereg) не делали проводок вообще.

Вводит суспенс-фазу через новый счёт 76 «Расчёты с пайщиками»:
  - confirmpay (касса приняла деньги) → o.reg.inpay: Dr 51 / Cr 76, ISSUE w.reg.pend
  - confirmreg (совет одобрил)        → o.reg.setmin (Dr 76/Cr 80) + o.reg.setent (Dr 76/Cr 86),
                                        TRANSFER w.reg.pend → w.reg.minshr / w.reg.entry
  - declinereg (совет отказал)        → o.reg.refund: Dr 76 / Cr 51, BURN w.reg.pend
                                        (отдельный процесс p.reg.refund) + удаление кандидата

Новые сущности ledger2:
  - счёт 76 PARTICIPANT_SETTLEMENTS (ACTIVE_PASSIVE)
  - кошелёк w.reg.pend REGISTRATION_PENDING (USER_SHARED, program_id=0)
  - операции o.reg.inpay/setmin/setent (p.reg.accept) + o.reg.refund (p.reg.refund)

confirmreg: проводки под гардами amount>0 (чинит латентный креш при initial==0).
declinereg: добавлен require_auth(_soviet) + возврат + чистка кандидата
(устраняет залипание записи кандидата, блокировавшее повторную подачу).

Синхронизированы оба зеркала (cpp registries + cooptypes TS), обновлён
snapshot реестра кошельков, добавлен p.reg.refund в process-hash-locator
(иначе integrity-check валит controller на старте). Стандарт p.reg.accept
обновлён под двухфазную схему, заведён p.reg.refund.standard.yaml.

Контракты registrator.wasm и ledger2.wasm собраны (static_assert'ы реестров прошли).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 07:21:24 +00:00
coopops 867a87f8db Merge remote-tracking branch 'origin/feat/registration-refund' into dev 2026-06-06 06:54:55 +00:00
ant 61570457db Merge pull request 'fix(gateway): не плодить повторный регистрационный платёж после приёма' (#98) from feat/registration-payment-dedup into feat/registration-refund
Reviewed-on: #98
2026-06-06 06:49:14 +00:00
ant 41c77b3b0f Merge pull request 'feat(gateway,account): возврат вступительного взноса при отказе совета' (#97) from feat/registration-refund into dev
Reviewed-on: #97
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-06 06:49:02 +00:00
coopops 9e64238682 fix(gateway,notifications): возврат взноса слал письмо «платёж принят»
Typecheck / desktop (pull_request) Successful in 13m24s
Typecheck / controller (pull_request) Successful in 13m15s
notifyPaymentStatus выбирал workflow только по статусу (PAID → PaymentPaid
«Платёж принят»), игнорируя direction. Исходящий PAID — это возврат взноса
пайщику, и он получал письмо как на приём. Баг общий для всех возвратов
(регистрационный + паевой — оба идут через gateway.setPaymentStatus).

- Новый workflow notifications/payment-refunded «Возврат взноса выполнен»
  (зеркало payment-paid, тот же payload).
- payment-notification.service: OUTGOING+PAID → PaymentRefunded, INCOMING+PAID →
  PaymentPaid, остальное → PaymentCancelled.

Workflow засинкан в Novu (pnpm sync, 24 workflow, 0 ошибок).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 06:38:51 +00:00
coopops 75a8068970 feat(account,desktop): назначение возврата взноса — зеркало входящего + показ на развороте
Typecheck / desktop (pull_request) Successful in 13m31s
Typecheck / controller (pull_request) Has been cancelled
Backend: memo возвратного платежа теперь зеркалит назначение входящего взноса —
«Возврат вступительного и минимального паевого взносов №XXXX. Без НДС.» с тем же
номером (original.hash.slice(0,8), как в QR при приёме) и НДС-оговоркой, что
qrpay/sberpoll добавляют к Purpose. Кассир копирует as-is в платёжку.

Desktop: PaymentDetails показывал «Назначение платежа» с кнопкой копирования
только в ветке OUTGOING+payment_details. Возврат идёт без payment_details и падал
в «нет дополнительной информации». Добавлена ветка: если есть payment.memo —
показываем его через CopyableInput (с копированием).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 06:22:38 +00:00
coopops e2cf85dd6b fix(account): declinereg не находил кандидата из-за регистра registration_hash
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
В БД registration_hash хранится lowercase (sha256().digest('hex')), а из
цепи checksum256 в declinereg.registration_hash приходит UPPERCASE — exact-match
findOneBy не находил кандидата, возврат взноса пропускался с warn.
Нормализуем входящий хэш .toLowerCase() в findByRegistrationHash.

Проверено на voskhod: candidate czeudubzzuql имеет registration_hash
f25ca396... (lowercase), warn пришёл с F25CA396... (uppercase) — тот же хэш.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 06:09:29 +00:00
coopops 2821cc729e fix(desktop): ожидание решения совета — статичная иконка + сброс согласий при возврате
1. WaitingRegistration: крутящийся спиннер заменён статичной иконкой
   hourglass_top с подписью «Ожидаем решение совета». Анимация загрузки
   на процессе до 30 дней сбивала с толку — казалось, страница не дозагрузилась.
2. fixData (возврат к редактированию после отклонённого платежа) теперь зовёт
   новый store.resetConsents(): сбрасывает agreements (устав/ПД/…) + подписи +
   документы. Раньше галочка «прочитал устав» оставалась проставленной на
   повторном проходе ReadStatement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 05:50:48 +00:00
coopops 6def1156ed fix(desktop): BaseRadioCard hover не белит карту на светлой теме
hover ставил background: var(--p-surface) (#fff на light) — карта на
--p-canvas (#f4f4f5) осветлялась до белого, выглядело как засвет выбора.
На dark было ок (canvas #0a0a0a → surface #141416 — чуть светлее).
Замена на --p-canvas-2: на light темнее канвы, на dark светлее — hover
идёт в одну сторону с темой в обеих.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 05:39:06 +00:00
coopops 03976c8be9 Merge remote-tracking branch 'origin/feat/registration-refund' into feat/registration-payment-dedup 2026-06-05 19:41:55 +00:00
coopops e3983c3d01 merge dev into feat/registration-refund
Typecheck / desktop (pull_request) Successful in 13m36s
Typecheck / controller (pull_request) Successful in 13m16s
Конфликт только в сгенерированном Zeus-клиенте (controller/zeus + sdk/zeus) — разрешён
перегенерацией codegen, не ручной разводкой:
- пересобран cooptypes dist (протух: DeclineRegistration был в src, не в dist → TS2339)
- generate-schema (56 резолверов) + generate-client
- zeus теперь надмножество: dev + fsm-recovery (resetRegistration) + refund (REGISTRATION_REFUND)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:36:42 +00:00
ant 0966a97dde Merge pull request 'perf(factory): параллелить независимые чтения сборки документа (resolveParallel)' (#103) from perf/factory-parallel-fetch into dev
Reviewed-on: #103
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-05 19:05:25 +00:00
claude 26d4774307 docs(factory): зафиксировать паттерн параллелизации resolveParallel в CLAUDE.md
Typecheck / desktop (pull_request) Successful in 13m29s
Typecheck / controller (pull_request) Successful in 13m14s
Конвенция для Actions/*: независимые от data чтения — в resolveParallel,
зависимые (getMeta/getDecision/getMeetQuestions) — после батча; ловушка TDZ;
системный шрифт вместо base64.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:01:46 +00:00
coopops 2496755c4e fix(factory): arial.ttf как binary — controller/.gitattributes нормализовал EOL
Typecheck / desktop (pull_request) Successful in 13m29s
Typecheck / controller (pull_request) Has been cancelled
В предыдущем коммите arial.ttf закоммитился битым: components/controller/.gitattributes
содержит `* text eol=lf`, что применилось и к бинарному шрифту → срезало 105 байт
\r (275572 → 275467), шрифт невалиден. Добавил `*.ttf/otf/woff binary` в
controller/.gitattributes (перекрывает правило выше) и перезаписал blob корректно.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 18:45:23 +00:00
coopops ddf050a02f perf(factory): системный Arial вместо base64 в html — рендер weasyprint ×10
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Главное горло генерации PDF: в каждый html инжектился @font-face с Arial через
data:base64 (~270 КБ TTF). weasyprint декодировал и парсил весь шрифт на КАЖДЫЙ
рендер — ~1.3 сек на документ в 754 байта (замер: тот же текст со встроенным
шрифтом 1282–1715 мс, без него 120–289 мс).

Ставим Arial СИСТЕМНО в образ controller (fontconfig) — парсится один раз и
кешируется. Шрифт тот же файл (декод прежнего ArialBase64 → arial.ttf), имя
семейства "Arial", глифы/вёрстка идентичны.

- controller/assets/fonts/arial.ttf — наш Arial (275 КБ)
- controller/Dockerfile — fontconfig + COPY шрифта + fc-cache
- factory Generator — убран @font-face data:base64, оставлен font-family: Arial
- удалён src/Fonts/arial.ts (367 КБ base64, больше не нужен; bundle 1.48 → 0.75 МБ)

Итог по всей серии (warm-pool + parallel-fetch + системный шрифт):
generateProjectOfFreeDecision ~5 сек → ~400 мс (×10).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 18:43:04 +00:00
coopops 0030f5a362 perf(factory): параллелить независимые чтения сборки документа через resolveParallel
Typecheck / desktop (pull_request) Successful in 12m54s
Typecheck / controller (pull_request) Successful in 12m31s
Сборка данных документа в каждом Action делала серию строго последовательных
await: getTemplate (2 сетевых запроса в explorer) + getMeta(getCurrentBlock) +
mongo-чтения (getUser/getCooperative/getProject/getVars) + udata/request/meet.
Сетевые запросы складывались в задержку ~1.5с поверх рендера PDF.

Добавлен хелпер DocFactory.resolveParallel({...}) — резолвит ВЗАИМНО
НЕЗАВИСИМЫЕ источники через Promise.all. В каждом из 46 Action-факторов
независимый префикс (template + mongo-чтения + независимые meet/request/udata)
сведён в один resolveParallel; зависимые вызовы оставлены последовательными
после батча, с сохранением порядка и аргументов:
  - getMeta({title: template.title}) — зависит от template (TDZ в Promise.all);
  - getFullName/getCommonUser — от user;
  - getDecision/getApprovedDecision — от coop (+meta.created_at);
  - getMeetQuestions/getGeneralMeetingDecision — от meet;
  - getProgram — от request.

Поведение не меняется — только параллелизуются независимые чтения. Условные
ветки (bank_account по типу юзера, branch по is_branched, signature) и гварды
сохранены на местах. Файлы, где параллелить нечего (только template+meta,
meta зависит от template) — не тронуты.

tsc + eslint + unbuild — зелёные.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 17:15:50 +00:00
ant 39ec558877 Merge pull request 'fix(decision): vue-tsc TS2719 в Decision/CreateProject — publish result ?? null' (#101) from fix/decision-createproject-tsc into dev
Reviewed-on: #101
2026-06-05 15:10:11 +00:00
ant 3a372fe801 Merge pull request 'perf(factory): тёплый пул процессов WeasyPrint вместо exec-per-doc' (#100) from perf/factory-weasyprint-warm-pool into dev
Reviewed-on: #100
2026-06-05 15:06:47 +00:00
coopops a0ce722952 fix(decision): publish result undefined → null для IAgenda | null
Typecheck / desktop (pull_request) Successful in 12m49s
Typecheck / controller (pull_request) Successful in 12m24s
vue-tsc TS2719 в Decision/CreateProject/model: publishProjectOfFreeDecision
объявлен Promise<IAgenda | null>, но поле мутации nullable в схеме —
result включает undefined и не присваивается IAgenda | null.

Fix: return result ?? null.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:36:40 +00:00
coopops a2cf1d7bdf merge dev into perf/factory-weasyprint-warm-pool (PR #99 publish + др.)
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
2026-06-05 14:29:48 +00:00
ant ca1dc4fa69 Merge pull request 'fix(soviet): возвращать созданный вопрос повестки из publish — мгновенный показ' (#99) from fix/agenda-pending-new-question into dev
Reviewed-on: #99
2026-06-05 14:22:34 +00:00
coopops 2244c4e95c perf(factory): тёплый пул процессов WeasyPrint вместо exec-per-doc
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Генерация PDF спавнила новый процесс `weasyprint` на каждый документ через
`exec`. Медленные 3-5 сек — это холодный старт Python: import всего стека
(cffi/pydyf/tinycss2/Pango/cairo/HarfBuzz) + скан fontconfig. Сам рендер
быстрый (<0.5 сек), но старт платился каждый раз, т.к. процесс умирал сразу.

Держим пул из N долгоживущих Python-процессов горячими (WeasyPool/WeasyWorker):
import платится один раз за жизнь бэкенда, далее каждый документ = чистый
рендер. Размер пула = env WEASY_POOL_SIZE (дефолт min(4, CPU)) — даёт реальный
параллелизм, одновременные генерации не сериализуются к одному процессу.
Воркер живёт внутри процесса controller'а: без докера, миграций и отдельного
сервиса. Питон берётся из /venv (controller/Dockerfile), фоллбэк python3.

Детерминизм сохранён: SOURCE_DATE_EPOCH=0 в env воркера. Заодно выровнял
weasyPrintVersion 62.3 → 67 (синхрон с pip install в controller/Dockerfile —
метадата producer'а документов больше не врёт).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:15:13 +00:00
coopops f7951a985e fix(soviet): publish матчил вопрос по doc_hash вместо hash — ускорение + рабочий показ
Typecheck / desktop (pull_request) Failing after 8m22s
Typecheck / controller (pull_request) Successful in 12m41s
getAgendaItemByHash сравнивал decision.hash с data.document.doc_hash (хэш только
содержимого), тогда как decision.hash в блокчейне == data.document.hash («общий
хэш» = doc_hash + meta_hash). Из-за этого матч НИКОГДА не срабатывал: publish
впустую опрашивал повестку весь таймаут (~4.5 c) и возвращал null — мгновенный
показ не работал ни разу, вопрос всегда дожидался поллинга страницы.

- Матч по data.document.hash (= decision.hash on-chain).
- Поллинг: проверка сразу (без слепой стартовой паузы), тик 400 мс — ловим
  индексацию парсером action'а (~2 c) минимальной задержкой; publish ~2.2 c.

По таймингам: decision на чейне мгновенно, aggregate ~0.4 c; доминирует лаг
индексации парсером (~2 c) — единственный способ убрать его — синтез action,
отвергнут (требует подделки ~12 explorer/SHiP-полей BlockchainActionDTO).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:48:15 +00:00
coopops cd45697623 fix(soviet): не блокировать кнопки у созданного вопроса — голос/утверждение сразу
Typecheck / desktop (pull_request) Failing after 8m56s
Typecheck / controller (pull_request) Successful in 13m10s
Бэкенд возвращает пункт повестки только когда он полностью собран (decision из
блокчейна + проиндексированные парсером действие и документ-заявление) — то есть
он уже идентичен тому, что отдаёт getAgenda. Значит блокировать голос/утверждение
до следующего поллинга незачем: пользователь может действовать немедленно.

Убрана блокировка: pendingConfirmationIds (store/process-decisions) и
disabledDecisions (страница). Буфер-мердж в agendaStore оставлен как страховка от
«мигания» (вставленный вопрос держится, пока поллинг не вернёт его authoritative).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:12:26 +00:00
coopops c2c5aa1d84 fix(soviet): возвращать созданный вопрос повестки из publish — мгновенный показ
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
После публикации свободного решения повестка показывала «Нет вопросов на
повестке» до следующего поллинга (10+ сек). getAgenda собирается join'ом таблицы
decisions из блокчейна с проиндексированными парсером действием newsubmitted и
документом-заявлением, поэтому только что созданный вопрос появляется не сразу.

Решение: мутация publishProjectOfFreeDecision теперь возвращает созданный пункт
повестки. Бэкенд после публикации делает settle-паузу (parser-индексация) и
извлекает готовый вопрос с повестки по хэшу документа; фронт вставляет его в
таблицу немедленно. Кнопки голосования/утверждения/правки на нём заблокированы,
пока ближайший getAgenda-поллинг не подтвердит вопрос authoritative-данными.

Backend:
- AgendaInteractor.getAgendaItemByHash / AgendaService.getAgendaItemByHash —
  сборка одного пункта повестки по хэшу заявления (null, пока не проиндексирован).
- FreeDecisionService.publishProjectOfFreeDecision — settle-пауза + страховочная
  петля, возвращает AgendaWithDocuments | null. Резолвер: тип возврата сменён с
  Boolean на AgendaWithDocuments (nullable).
- AgendaModule экспортирует AgendaService; DecisionModule импортирует AgendaModule.

SDK: publish-селектор → форма agenda-item (rawAgendaSelector); regen zeus-клиента.

Frontend:
- agendaStore: буфер оптимистично вставленных вопросов + computed-мердж с
  authoritative-списком (без «мигания»); pendingConfirmationIds для блокировки.
- CreateProjectButton: вставляет вернувшийся вопрос через insertCreated.
- process-decisions/страница: блокируют кнопки неподтверждённых вопросов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:07:45 +00:00
ant 5c13c4ac53 Merge pull request 'feat(soviet): отрицательный консенсус — voteagainst отклоняет решение до срока' (#94) from feat/soviet-negative-consensus into dev
Reviewed-on: #94
2026-06-05 12:21:51 +00:00
coopops 3e2bf3dbc6 docs(council): снятие вопроса с повестки — отказ председателем или по сроку
Typecheck / desktop (pull_request) Successful in 12m49s
Typecheck / controller (pull_request) Successful in 12m30s
Дополнена страница «Повестка совета» бизнес-языком: если решение не принято
или истёк его срок, вопрос снимается с повестки — председателем вручную
(отрицательный консенсус) или автоматически по истечении срока. Обновлён
mermaid-граф (ветки «большинство против» и «срок истёк»), добавлен раздел
«Снятие вопроса с повестки», в блок голосования добавлено описание индикатора
отклонения. Без скриншотов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:14:22 +00:00
coopops aea114b01a feat(soviet): развязка отказа решения — авто по сроку vs явный declinedec председателем
Typecheck / desktop (pull_request) Successful in 13m13s
Typecheck / controller (pull_request) Successful in 12m38s
Голос «против» больше не затирает решение автоматически. Развязаны два пути
отказа:
- по истечению срока — авто (cancelexprd кроном chairman, ключом кооператива);
- по отрицательному консенсусу (большинство «против») — явным действием
  председателя declinedec, до истечения срока.

Контракт (soviet):
- voteagainst: только фиксирует голос, без авто decline_and_erase_decision.
- Новый action declinedec (в cancelexprd.cpp): require_auth(coopname), проверяет
  votes_against*100 > members*50 (зеркало votefor), переиспользует общую ветку
  decline_and_erase_decision. Собран в CDT, declinedec в ABI.
- Стандарты sov.authpkg / sov.decision приведены к развязке.

cooptypes: action Declinedec + интерфейс IDeclinedec.

Контроллер:
- SovietBlockchainPort/adapter: declineDecision + getBoards.
- Мутация declineDecision (resolver/service/interactor + DeclineDecisionInput),
  роль chairman.
- Повестка обогащается council_members_count (состав совета `soviet`, дёргается
  один раз) — фронту для порога принятия/отклонения.
- chairman: убран флаг cancelApprovedDecisions и кривая подпись; крон по сроку
  гасит ВСЕ истёкшие решения.

SDK: селектор council_members_count, мутация DeclineDecision; zeus regen (обе копии).

Desktop:
- BaseButton: сплошной вариант negative.
- VotingButtons: красный крестик при отрицательном консенсусе.
- QuestionCard: у председателя красная «Отклонить» (declinedec) вместо «Утвердить»
  когда решение отклонено советом; фича Decision/DeclineDecision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:44:11 +00:00
coopops b010cbe248 fix(gateway): не плодить повторный регистрационный платёж после приёма
Баг: при перезаходе/перезагрузке страницы оплаты в момент приёма взноса
заводился второй регистрационный платёж. Дедуп findActivePendingPayment ловит
только status=PENDING — а когда первый платёж уже PAID/COMPLETED (принят,
аккаунт регистрируется), дедуп промахивался и createInitialPayment создавал
платёж-дубль. Его нельзя ни подтвердить (processIncomingPayment →
registerBlockchainAccount → «аккаунт уже зарегистрирован»), ни осмысленно
отклонить — мусорный сирота в реестре.

Фикс: регистрационный платёж одноразовый. Берём последний рег-платёж пайщика
и, если он в живом статусе (PENDING/PROCESSING/PAID/COMPLETED), возвращаем его
вместо создания нового. Новый ордер заводим только если прошлого нет либо он
FAILED/EXPIRED/CANCELLED (легитимная повторная попытка, в т.ч. после
resetRegistration). Авторитетная защита на бэке, не зависит от гонок на фронте.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 11:36:13 +00:00
coopops 89e042a8ef review(#97): подписка на declinereg через константы cooptypes
По ревью @ant: не хардкодить 'action::registrator::declinereg' строкой.
Добавил action-константу RegistratorContract.Actions.DeclineRegistration
(actionName='declinereg', тип IDeclinereg) в cooptypes — её не было.
Листенер подписывается через
action::${RegistratorContract.contractName.production}::${...DeclineRegistration.actionName}
по образцу signed-document-ingestion (SovietContract).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 11:35:47 +00:00
coopops 39d02f8592 feat(gateway,account): возврат вступительного взноса при отказе совета
При отказе совета в приёме пайщика (отрицательный консенсус >50% — PR #94 —
или истечение срока повестки) on-chain срабатывает registrator::declinereg.
Взнос по фондам на этот момент НЕ разносился (confirmreg не было), но реальные
деньги пайщика уже у кооператива на расчётном счёте — их нужно вернуть.

- Новый PaymentTypeEnum.REGISTRATION_REFUND (исходящий), отдельный от WITHDRAWAL
  (возврат паевого), чтобы не путать термины. Лейблы + OUTGOING_PAYMENT_TYPES.
- RegistrationDeclineListener на action::registrator::declinereg: резолвит
  кандидата по registration_hash, по исходному REGISTRATION-платежу берёт сумму
  и номер (hash8 = тот же № из QR), заводит исходящий платёж PENDING с
  назначением «Возврат вступительного и мин.паевого взноса №<hash8>».
  Идемпотентно (повторный приход события не плодит возвраты).
- processOutgoingPayment: для REGISTRATION_REFUND подтверждение кассиром —
  off-chain no-op (статус → COMPLETED, без completeOutcome). Взнос on-chain не
  двигался, откатывать нечего; обратная проводка по расчётному счёту появится
  здесь же, когда введём проводки на приёме платежа.
- Реестр платежей: для возврата спрятана кнопка «Отклонить» (отказать в
  возврате нельзя — совет уже отказал, деньги обязаны вернуть).
- Codegen: PaymentType.REGISTRATION_REFUND проброшен в Zeus (sdk + controller).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 11:15:55 +00:00
ant fc9df153ea Merge pull request 'feat(reg): обработка отклонения вступительного платежа (decline до создания аккаунта)' (#91) from feat/registration-payment-decline into dev
Reviewed-on: #91
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-05 11:05:08 +00:00
coopops 50a0550a6a feat(registration): поллинг приёма оплаты на шаге PayInitial
Провайдер QR (Bank) не эмитит success-колбэк, деньги подтверждаются вебхуком
на бэке асинхронно — без поллинга экран висел на форме оплаты до перезагрузки.
Каждые 10с подтягиваем аккаунт; как только вступительный платёж покинул PENDING
(принят/отклонён) — уходим на шаг ожидания решения совета, который сам
показывает «платёж принят» либо причину отказа.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:54:20 +00:00
ant eb9ff9d3b6 Merge pull request 'feat(pwa): надёжное обновление + пер-кооп имя/иконка установочника' (#93) from feat/pwa-reliable-update into dev
Reviewed-on: #93
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-05 09:27:06 +00:00
coopops de974d336d feat(pwa): стандартная иконка приложения (SVG + PNG-набор)
Typecheck / desktop (pull_request) Successful in 12m56s
Typecheck / controller (pull_request) Successful in 12m31s
PNG-иконки manifest'а физически отсутствовали (на проде /icons/*.png отдавали
HTML-фоллбэк) → на домашнем экране дефолтная иконка. Генерим полный набор из
logo.svg (квадрат, белый фон, лого по центру, maskable-safe) + в динамическом
manifest основная иконка — масштабируемый /logo.svg (purpose any maskable).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:21:17 +00:00
coopops 14063859c6 feat(pwa): имя установочника = имя кооператива (динамический manifest)
Образ desktop generic, кооп задаётся env контейнера (COOP_SHORT_NAME), а
manifest пёкся на сборке и коопа не знал → на домашнем экране всегда дефолтное
'Цифровой Кооператив'. Отдаём manifest динамически SSR-middleware из env по
пути /manifest.webmanifest (статический /manifest.json перехватывается
serveStatic ДО user-middleware, поэтому новый путь + переписываем <link
rel=manifest> в HTML). Плюс рантайм apple-mobile-web-app-title для iOS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:21:17 +00:00
coopops 7105c053fb feat(pwa): надёжное пользовательское обновление вместо авто-reload
Новый SW больше не активируется в фоне сам (skipWaiting/clientsClaim=false) —
ждёт в waiting, убирая гонку «подвисание между релизами» (старая страница +
новый SW). updated() шлёт window-событие 'sw:update-available'; boot 'pwa-update'
ловит его и показывает канон-тост UpdateAlert в левом нижнем углу (timeout 0,
кнопки «Обновить»/«Позже»). По «Обновить» → window.applyUpdate() → SKIP_WAITING
→ controllerchange → reload. Детерминированно, без фонового свопа версий.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:21:17 +00:00
coopops ffdf6fe8e3 [reg-fsm][@ant] feat(account,desktop): откат незавершённой регистрации + server-truth шаг
Plan B (recovery + источник истины о шаге на сервере), поверх гарда A.

Backend:
- resetRegistration (self-scoped по токену): откат незавершённой регистрации
  к редактированию до отправки в блокчейн. Снимает заморозку профиля/e-mail
  (users.status → created), чистит документы кандидата и отпечаток заявления,
  удаляет непринятую попытку вступительного платежа. Принятые средства
  (PAID/COMPLETED) или уже созданный on-chain аккаунт → отказ (нужна
  перерегистрация/возврат — отдельная ветка).
- PaymentRepository.delete.

Frontend:
- SignUp реконструирует шаг с сервера: Joined → шаг оплаты (переживает
  перезагрузку и работу в другой вкладке), приоритет у registration_payment.
- WaitingRegistration «Исправить данные» дёргает resetRegistration перед
  возвратом к данным (после подписи профиль заморожен, локального отката мало).
- Фича Account/ResetRegistration + Zeus-клиент (codegen).

Существующие статусы (joined/registered/active) не меняем, новых не вводим:
фронтовые active-гейты висят на on-chain user_account.status — другая ось,
не пересекается с mono users.status, который трогает recovery.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:18:58 +00:00
coopops f72cf529a9 feat(soviet): отрицательный консенсус — voteagainst отклоняет решение до срока
Typecheck / desktop (pull_request) Successful in 12m50s
Typecheck / controller (pull_request) Successful in 12m35s
На контракте совета теперь возможно не только заблокировать решение
голосованием, но и принять отрицательное решение: как только против
проголосовало больше половины состава совета, решение отклоняется
немедленно (не дожидаясь истечения срока) — той же веткой отказа, что и
просрочка: decline-callback в исходный процесс + удаление решения.

- Общая ветка отказа вынесена в decline_and_erase_decision (cancelexprd.cpp),
  переиспользована в cancelexprd (просрочка) и voteagainst (негативный консенсус).
- Порог зеркален votefor: votes_against*100 > total_members*50, строгое «больше».
- Стандарты sov.authpkg / sov.decision приведены к новому поведению; заодно
  убран cancelvote (в коде запрещён: eosio::check(false)).

Контракт soviet собран в CDT — зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 08:48:41 +00:00
coopops 2b44fe6022 [reg-decline][@ant] feat(account): гард консистентности профиля перед reguser
Typecheck / desktop (pull_request) Successful in 14m24s
Typecheck / controller (pull_request) Successful in 13m58s
Гард A: на цепь не уходят данные, отличные от подписанных в заявлении.
Каноничный отпечаток удостоверяющих личность полей фиксируется при
подписании заявления (registerParticipant → candidate.meta), сверяется
перед registerBlockchainAccount. Расхождение → BAD_REQUEST, платёж уходит
в FAILED с причиной (видна пайщику через registration_payment из #91).

Пропуск уже идущих регистраций: кандидаты без отпечатка в meta (заявление
подписано до внедрения гарда) не проверяются — grandfather.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 07:40:08 +00:00
coopops 5e333bdcc6 [reg-decline][@ant] feat(desktop): экран отклонения вступительного платежа + повтор
Typecheck / desktop (pull_request) Successful in 14m5s
Typecheck / controller (pull_request) Successful in 13m29s
Обработка отклонения платежа на этапе регистрации в SignUp:

- Session store: геттер registrationPayment из аккаунта.
- SignUp.vue onMounted: если есть незавершённый вступительный платёж —
  восстанавливаем шаг WaitingRegistration после перезагрузки/в любой вкладке
  (процесс может тянуться днями).
- WaitingRegistration.vue: ветка «Платёж не принят» с причиной из
  registration_payment.message и кнопками «Повторить оплату» / «Исправить
  данные» (откат назад по процессу — только по нажатию пайщика). Live-опрос
  аккаунта раз в 10с, чтобы экран сам переключился на отклонение/приём.
- Payment/SetStatus: setCancelledStatus(id, message); кнопка «Отклонить»
  теперь ставит CANCELLED (триггерит уведомление) с полем причины.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 06:53:55 +00:00
coopops c2f7eac7c2 [reg-decline][@ant] feat(gateway,account): причина отклонения платежа + сводка вступительного платежа в аккаунте
Backend под обработку отклонения регистрационного платежа (этап до создания
аккаунта в блокчейне):

- SetPaymentStatusInput: опциональное поле message — причина смены статуса;
  сохраняется в payment.message и показывается пайщику.
- gateway.interactor.setPaymentStatus: персистит message при смене статуса
  (уведомление о CANCELLED уже шлётся через PaymentNotificationService).
- PaymentRepository.findLatestByUsernameAndType — последний платёж типа.
- AccountDTO.registration_payment {status,message,hash,quantity,symbol}:
  сводка вступительного платежа в getAccount. Источник истины для фронта,
  чтобы восстановить шаг регистрации (ожидание/отклонение) после
  перезагрузки и в любой вкладке. Заполняется пока нет participant_account.
- Регенерация Zeus-клиента (schema.gql gitignored).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 06:53:43 +00:00
Alex Ant 8272fe6951 chore(release): publish
Release / release (push) Successful in 25m0s
Build bootstrap container / build (push) Successful in 39s
Release / publish-packages (push) Failing after 8m42s
Release / trigger-coopenomics-docs (push) Successful in 1s
Release / trigger-mono-docs (push) Successful in 2s
2026-06-05 11:42:28 +05:00
Alex Ant 793572ae4b Merge branch 'testnet' 2026-06-05 11:42:13 +05:00
Alex Ant 4cf2ab3c64 chore(release): publish
Release / release (push) Successful in 53m16s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
Build bootstrap container / build (push) Successful in 1m20s
2026-06-05 01:13:18 +05:00
ant fc3ae1b512 Merge pull request 'fix(install,contracts): чистка фантомных пайщиков fgrtejiwnynn + лок install от дублей' (#89) from fix/gorozhane-phantom-cleanup into dev
Reviewed-on: #89
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-04 20:06:42 +00:00
claude f09e267c28 fix(install): не включать maintenance во время нормальной установки
Typecheck / desktop (pull_request) Successful in 14m33s
Typecheck / controller (pull_request) Successful in 13m59s
'maintenance' на фронте включает полноэкранную заглушку «Техническое
обслуживание» (watch-desktop-health), которая накрывала бы саму страницу
установки. Поэтому статус остаётся 'initialized' на всё время install
(фронт показывает страницу установки, как и прежде), а в 'maintenance'
переводим ТОЛЬКО в catch при падении ПОСЛЕ записи в цепь — это блокирует
повторный запуск (guard требует 'initialized') и корректно сигналит, что
коопа в полу-установленном состоянии и требует ручного разбора.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:59:48 +00:00
claude cccec9f940 fix(registrator): пересчёт active_participants_count с явным исключением фантомов
Typecheck / desktop (pull_request) Successful in 14m25s
Typecheck / controller (pull_request) Has been cancelled
Счётчик сходится к верному значению (5) за один деплой независимо от
порядка выполнения migrate registrator vs soviet. Сверено с цепью:
11 participants = 6 фантомов + 5 реальных (3 founders + 2 новых пайщика
Белоус Нина/Сергей, вступивших после инцидента).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:41:37 +00:00
coopops 0354b391f5 fix(install,contracts): чистка фантомных пайщиков fgrtejiwnynn + лок install от дублей
Typecheck / desktop (pull_request) Successful in 14m51s
Typecheck / controller (pull_request) Has been cancelled
Причина (инцидент 2026-06-04, gorozhane): InstallInteractor.install прогнан 3×
(2 провала + успех). Каждый прогон генерил случайный username и необратимо
регистрировал совет на цепи (adduser), а откат в catch чистил только off-chain.
На цепи осели 6 дублей-аккаунтов 3 учредителей + 1800 RUB фантома в паевом фонде.

Багфикс (install.interactor.ts):
- лок: статус → 'maintenance' ДО любых on-chain операций; повторный install
  блокируется guard'ом (требует 'initialized'), что и плодило дубли;
- при ошибке ПОСЛЕ первого adduser off-chain rollback не делаем (сохраняем
  привязку личность↔username для разбора), статус остаётся 'maintenance';
- при ошибке ДО on-chain — чистый откат + возврат 'initialized'.

On-chain очистка (вызовется автоматически на деплое, идемпотентна, scope=coopname):
- registrator::migrate — erase 6 фантом-accounts + пересчёт active_participants_count
  из soviet::participants;
- soviet::migrate — erase 6 фантом-participants (гард: остаётся ≥1 реальный);
- ledger2::migrate — снять L3-доли фантомов на w.reg.minshr, на снятую сумму
  уменьшить L2-кошелёк и бухсчета 80 (Cr, паевой фонд) и 51 (Dr, расчётный).

Off-chain коррекции (mongo individuals и пр.) — вручную, см. ~/fixes.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:24:25 +00:00
ant 85e2f05bdb Merge pull request 'fix(payments): убрать обязательное поле «КПП банка» из реквизитов счёта' (#81) from fix/remove-bank-kpp into dev
Reviewed-on: #81
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-04 19:21:19 +00:00
coopops 96e16ee6ab Revert "Merge pull request '[apps-catalog umbrella] mono — Federation MWP (E9 demo pilot + E10 runtime federation + 9.3.b publish)' (#64) from apps-catalog into dev"
This reverts commit 9df3bfe4a9, reversing
changes made to e54115d28f.
2026-06-04 14:32:13 +00:00
coopops 93b94326ca fix(payments): убрать обязательное поле «КПП банка» из реквизитов счёта
Typecheck / desktop (pull_request) Successful in 13m21s
Typecheck / controller (pull_request) Successful in 13m24s
КПП — реквизит юрлица, у ИП и физлиц его нет. Общий тип реквизитов
банковского счёта (IBankAccount.details.kpp) требовал КПП у всех держателей
счёта, из-за чего индивидуальный предприниматель не мог пройти регистрацию
(и фронт, и бэкенд заворачивали пустой КПП).

Поле снято во всех слоях:
- cooptypes: тип IBankAccount.details + шаблон ParticipantApplication
  (render-аргументы и плейсхолдеры) + mock-данные registry
- controller: BankAccountDetails(Input) DTO, domain-интерфейсы, init-config
- factory: ajv-схема BankAccountSchema (properties + required)
- sdk: zeus-селектор bankAccountSelector + перегенерённый zeus-клиент
- desktop: формы ИП/юрлица/физлица, реквизиты, добавление способа оплаты,
  CSV-импорт участников

КПП организации/кооператива (details.kpp, отчётность, qrpay/sberpoll)
НЕ затронут — это легитимный реквизит юрлица.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:40:13 +00:00
claude 9df3bfe4a9 Merge pull request '[apps-catalog umbrella] mono — Federation MWP (E9 demo pilot + E10 runtime federation + 9.3.b publish)' (#64) from apps-catalog into dev 2026-06-04 11:26:47 +00:00
claude 83a3207805 Merge pull request '[E9-9] feat(controller): approveModeration + rejectModeration + appsCatalogPendingModerations' (#76) from feat/E9-9-approve-moderation into apps-catalog
Typecheck / desktop (pull_request) Successful in 14m35s
Typecheck / controller (pull_request) Successful in 13m24s
2026-06-04 11:23:10 +00:00
claude db2ff23756 Merge pull request '[E10-4] gateway-orchestrator: install-pipeline (OCI pull → compose up → health → registry)' (#73) from feat/E10-4-install-pipeline into apps-catalog
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
2026-06-04 11:22:37 +00:00
coopops 66ad103e90 [E9-9] feat(controller): approveModeration + rejectModeration + appsCatalogPendingModerations
Story 9.9 «Стол восхода: approve pending packages» — backend-сторона.

GraphQL contract:
- Query.appsCatalogPendingModerations(status?: ModerationStatusEnum, limit?: Int)
  → [ModerationRequestDTO]. По умолчанию SUBMITTED. Chairman-only.
- Mutation.approveModeration(data: ApproveModerationInputDTO!)
  → ApproveModerationResultDTO. Discriminated outcome:
    applied / pendingChain (423) / conflict (409) / requiresOverride (403) / failed.
- Mutation.rejectModeration(data: RejectModerationInputDTO!)
  → RejectModerationResultDTO. Discriminated outcome: applied / conflict / failed.

HTTP-service: 3 новых метода (listSubmittedModerations / approveModeration /
rejectModeration) на ca-admin endpoints, добавленных параллельным
apps-catalog PR #7. Mapping snake_case → camelCase; degraded mode на
list — пустой массив, на mutations — failed с явной причиной (чтобы UI
не вводился в заблуждение «approved»).

ReleaseScope передаётся как InputType с union-style полями
(type + subnets?/coopnames?); резолвер валидирует соответствие type →
непустой массив перед отправкой на ca-admin.

Зависимость: apps-catalog PR #7 (GET /v1/admin/moderation) должен быть
смёрджен в feat/E9-demo-app-pilot до того, как этот endpoint можно
использовать в e2e.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 06:01:45 +00:00
ant e54115d28f Merge pull request 'feat: экспорт документов в .sig v2.0 + manifest.json (верификатор)' (#65) from feat/sig-v2-export into dev
Reviewed-on: #65
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-04 05:48:50 +00:00
coopops 3f1d816ac0 [E10-4] feat(gateway-orchestrator): install-pipeline через DockerRunner/HealthProbe/OciToken порты
Story 10.4 — pipeline установки subgraph-расширения:
1. (опц.) OCI token у CA-auth → docker pull образа;
2. (опц.) docker compose up сервиса;
3. healthcheck poll по url subgraph'а до 200 OK;
4. upsert в subgraph_registry (Apollo Gateway за ≤10с подхватит);
5. на failure — composeDown rollback + discriminated outcome {applied|failed}.

InstallController стал тонкой обёрткой над InstallOrchestratorService.
Внешние зависимости (docker shell / fetch / CA-auth) изолированы за
портами; тесты (7/7 green) гоняют сценарий через ин-мемори моки без
реального docker daemon'а и сети.

Что ОСТАЁТСЯ для отдельной ветки под этим зонтиком:
- wiring approve→install REST из mono controller (mutation approvePackage);
- desktop notification после успешного install'а;
- E2E на реальном compose + chatcoop subgraph (Story 10.8).
2026-06-04 05:08:39 +00:00
coopops 4e64637823 [9.3.b umbrella-merge] Mutation.publishPackage + publishRelease — proxy на ca-admin — closes #72
Typecheck / desktop (pull_request) Successful in 13m38s
Typecheck / controller (pull_request) Successful in 13m17s
2026-06-04 04:54:12 +00:00
coopops bf1f8ba345 [E10 umbrella-merge] Runtime Federation для backend-расширений (10.1+10.2+10.2b+10.3) — closes #71 2026-06-04 04:54:04 +00:00
coopops f698e39cb8 [9.3.b-rel] feat(controller): Mutation.publishRelease — on-chain setrelease через ca-admin
Story 9.3.b-rel — мутация выкладки нового релиза ранее зарегистрированного
пакета. Прокидывает manifest + версию + tarball_sha256 на ca-admin
POST /v1/admin/releases, который сам валидирует manifest Zod-схемой
(PackageManifestSchema) и подписывает on-chain apps::setrelease от
имени chairman'а кооператива-оператора каталога.

Backend (apps-catalog-proxy):
- PublishReleaseInputDTO / PublishReleaseResultDTO — GraphQL Input/Object;
  manifest как GraphQLJSON (произвольная структура валидируется на
  ca-admin'е).
- AppsCatalogHttpService.createRelease(): POST /v1/admin/releases;
  discriminated outcome — applied (с опциональным transaction_id) |
  invalidManifest (HTTP 422 INVALID_MANIFEST мапится явно, чтобы UI
  показывал валидационную ошибку отдельно от network/auth) | failed
  для прочих.
- AppsCatalogProxyResolver.publishRelease(): @AuthRoles(['chairman']).

Под архитектуру E10 manifest содержит ссылки на артефакты в Nexus:
- coopenomics.backend.image — docker-image (subgraph расширения);
- coopenomics.frontend.tarball — npm tarball (desktop-bundle).

Сам manifest НЕ валидируется на стороне controller'а — это делает
ca-admin (там Zod-схема и она же будет расширена для package.json
полей в story 10.7).

Frontend (форма Опубликовать релиз) — отдельной story 9.3.b-ui после
codegen'а Zeus.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 19:03:38 +00:00
coopops b3ed693db6 [794-4][@ant] feat(desktop): расширение файла подписи .sig → .coopsig — чтобы ОС не предлагала открыть в КриптоПро (несовместимый secp256k1-JSON vs ГОСТ-CMS)
Typecheck / desktop (pull_request) Successful in 13m34s
Typecheck / controller (pull_request) Successful in 13m11s
2026-06-03 19:01:19 +00:00
coopops a07b5f8d60 [9.3.b-pub] feat(controller): Mutation.publishPackage — on-chain регистрация пакета через ca-admin
Story 9.3.b-pub стола разработчика — мутация для воскходовского
chairman'а: координаты пакета (@scope/name + owner + compatible_subnets)
прокидываются на ca-admin POST /v1/admin/package, который сам
подписывает on-chain action apps::regpkg от имени chairman'а
кооператива-оператора каталога.

Backend (apps-catalog-proxy):
- PublishPackageInputDTO / PublishPackageResultDTO — GraphQL InputType +
  ObjectType (camelCase для Zeus, без snake-case утечек).
- AppsCatalogHttpService.registerPackage(): POST на /v1/admin/package;
  discriminated outcome (applied | conflict | failed) — мапит 409 в
  conflict, прочие axios-ошибки в failed; degraded-mode (без
  APPS_CATALOG_API_KEY) возвращает failed с явным сообщением, чтобы UI
  не вводился в заблуждение «опубликовано».
- AppsCatalogProxyResolver.publishPackage(): @UseGuards(GqlJwtAuthGuard,
  RolesGuard) + @AuthRoles(['chairman']) — мутация доступна только
  председателю кооператива.

Multipart upload install.js под архитектуру E10 НЕ делаем: расширения
теперь публикуются как npm-package + docker-image в Nexus отдельной
процедурой (`npm publish` / `docker push`) через scoped JWT от
CA-auth. Эта мутация — только on-chain маркер «такой пакет
существует»; релизы с manifest'ом — story 9.3.b-rel.

schema.gql / Zeus client — codegen за пределами этого PR (mono-каноном
pnpm generate-schema + generate-client + sdk build делается отдельным
коммитом, чтобы PR с backend changes не зависел от пересборки SDK
артефактов).

Frontend — отдельной story 9.3.b-ui: DeveloperPublishReleasePage.vue
сейчас заглушка, форму подключим после codegen'а Zeus.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 18:59:37 +00:00
coopops c1d1b08973 [794-4][@ant] feat(desktop): .sig пишет canonicalization по версии документа (1.1.0→jcs-1.0) + signed_at чистится до подписанного формата байт-в-байт
Typecheck / desktop (pull_request) Successful in 13m54s
Typecheck / controller (pull_request) Successful in 13m22s
2026-06-03 18:46:49 +00:00
coopops da55cbac08 [794-4][@ant] feat(cooptypes): canonicalizationForVersion(version) — единый источник соответствия версия подписи → режим канонизации (.sig для desktop и верификатора) 2026-06-03 18:46:49 +00:00
coopops 4a5f41d15e [794-4][@ant] feat(sdk): версия подписи 1.1.0 с детерминированной канонизацией meta (JCS RFC 8785) — meta_hash перестаёт зависеть от порядка ключей; 1.0.0 остаётся legacy; compareDocuments берёт версию из документа (бэкенд авто-корректен) 2026-06-03 18:46:49 +00:00
coopops 8ea8bceea9 [E10-3] feat(gateway-orchestrator): Apollo Gateway + subgraph registry skeleton
Typecheck / desktop (pull_request) Successful in 13m48s
Typecheck / controller (pull_request) Successful in 13m40s
Epic 10 Story 10.3 — отдельный per-tenant Nest-сервис, объединяющий:
1. Apollo Federation Gateway (ApolloGatewayDriver) на /v1/graphql.
2. REST orchestrator API на /v1/internal/{extensions,composition}/*.
3. Subgraph registry в Postgres (TypeORM entity SubgraphRegistryEntity).

Что работает:
- Bootstrap seed'ит запись core-subgraph'а (coopback) → IntrospectAndCompose
  стартует с одним subgraph'ом → gateway проксирует все queries в core.
- JWT forwarding: Authorization-header forward'ится в каждый subgraph
  через RemoteGraphQLDataSource.willSendRequest.
- POST /v1/internal/extensions/install { packageId, version, url } —
  upsert в registry.
- DELETE /v1/internal/extensions/uninstall/:packageId — deactivate.
- POST /v1/internal/composition/refresh — 202 Accepted (реальный recompose
  по polling'у IntrospectAndCompose, ≤10 сек).
- GET /v1/internal/composition/status — текущие subgraph'ы.

Trade-off MVP:
- subgraphs читаются ОДИН раз на bootstrap (Apollo 2.9 IntrospectAndCompose
  ожидает статичный массив). Добавление НОВОГО subgraph'а → требует
  рестарта контейнера gateway-orchestrator. Warmup ~5 сек, делается
  orchestrator-pipeline при install'е (Story 10.4).
- Реальный docker pull/up/healthcheck — Story 10.4 (Install pipeline).
- Динамическая supergraph composition без рестарта — Story 10.3b
  (custom SupergraphManager).
- On-chain auto-update watcher — Story 10.5.

ENV: GATEWAY_PORT, DATABASE_URL, CORE_SUBGRAPH_URL, COOPNAME, JWT_SECRET.
2026-06-03 18:44:08 +00:00
coopops 68b91566ad [E10-2b] feat(extension-hello-subgraph): PoC smoke-тест @coopenomics/extension-sdk
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Epic 10 Story 10.2b — минимальный subgraph на extension-sdk доказывает что
Federation v2 driver + JWT module + healthcheck реально работают вместе.

Smoke результат (SUBGRAPH_PORT=33001 JWT_SECRET=test-secret COOPNAME=voskhod):
- GET /_health → {"status":"ok","version":"unknown","uptimeSeconds":N}
- POST /v1/graphql { helloFromExtension(name:"мир") { greeting fromExtension } }
  → {"greeting":"Здравствуй, мир!","fromExtension":"hello-subgraph@0.1.0"}
- POST /v1/graphql { _service { sdl } } → returns federation v2 SDL с
  @key/@external/@requires/@shareable директивами

Также в этом коммите:
- extension-sdk переключён с unbuild на обычный tsc (unbuild esbuild не
  поддерживает декораторы для target ES2020). main: dist/index.js, CommonJS.
- @types/passport-jwt в devDeps.

Cross-extension entity references (SharedCooperator @key + ResolveField)
живут в chatcoop subgraph'е (story 10.8) — там есть локальная сущность
ChatThread которая ссылается на пайщика. Этот PoC только доказывает
базовую механику.
2026-06-03 18:38:37 +00:00
coopops 883e597d40 [E10-2] feat(extension-sdk): @coopenomics/extension-sdk — Nest-template subgraph'а
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Epic 10 Story 10.2 — SDK для разработки backend-расширений Apollo Federation v2
subgraph'ов экосистемы кооператива.

Что внутри:
- ExtensionFederationModule.forRoot() — готовый GraphQL модуль с
  ApolloFederationDriver + autoSchemaFile.federation: 2
- ExtensionAuthModule.forRoot({ secret }) — passport-jwt стратегия,
  валидирует токен который Apollo Gateway forward'ит в Authorization-header.
- ExtensionJwtAuthGuard — guard для GraphQL resolver'ов
- SharedCooperator/SharedCooperative/SharedAccount — federation v2 stub'ы
  на core-entity для cross-extension references (@key + @external)
- HealthController GET /_health — обязательный для orchestrator'а
- loadExtensionConfig() — fail-fast загрузчик env'ов

Templates:
- Dockerfile multi-stage (builder + runtime + wget healthcheck)
- docker-compose.snippet.yaml — шаблон service'а для orchestrator'а

Контракт фиксирован Epic 10 PRD (apps-catalog/docs/epics/E10-federation-runtime.md)
и Architecture v3 (blago req 8b-architecture-v3-runtime-federation).
2026-06-03 18:33:24 +00:00
coopops f6b5c01030 [E10-1] feat(controller): переключение GraphQL на ApolloFederationDriver (subgraph mode)
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Epic 10 Story 10.1 — coopback становится первым federation v2 subgraph'ом.
Локальная core-схема компилируется с federation директивами (`autoSchemaFile.federation: 2`).
Endpoint остаётся на /v1/graphql; non-federated клиенты (desktop) не должны
заметить переключения — Apollo Federation добавляет лишь служебные
`_service { sdl }` и `_entities` query.

Gateway-режим (Apollo Gateway + IntrospectAndCompose) зайдёт отдельной
story 10.1b — поверх этого subgraph'а + registry в Postgres. На MVP пока
один subgraph, gateway не нужен.

Зависимости:
- + @apollo/subgraph ^2 (peer для federation 2 в @nestjs/apollo)

@key директивы на core entity (User/Cooperative/Account/Wallet) добавятся
по мере вытаскивания первых extension'ов — отдельными точечными PR.
2026-06-03 18:24:51 +00:00
coopops cb014d7b10 [C28-33][@ant] refactor(capital-desktop): asset symbol/precision из системного стора + строгий тип generate-input
R4: убрал хардкод "RUB" и "1000.0000 RUB" из feature ProgramExpense.
 - submitTopup принимает raw число "10000" → форматирует через formatToAsset
   с info.symbols.root_govern_symbol/precision.
 - submitProgramExpense форматирует total_amount + planned_amount каждого item
   через тот же helper (UX-долг "amount без IMask" закрыт по сумме).
 - Placeholder'ы Dialog'ов вычисляются computed'ом из system.info.symbols.

R1 (частично): тип `GenerateStatementInput = Mutations.Expense.GenerateExpense
ProposalStatementDocument.IInput['data']` + деструктуризация результата через
computed `[Mutations.X.Y.name]` — убраны 2 из 4 `as any` каста (input + index).
Оставшиеся (signed-as-any и create-input-as-any) — отдельный refactor PR через
signed-doc adapter в shared/lib/document.

Source-wallet вынесен в локальную map SOURCE_WALLET_BY_OPERATION с throw на
неизвестный код — backend-derivation (R3) отдельной задачей.
2026-06-03 16:17:01 +00:00
coopops 9c29cc3260 [C28-33][@ant] refactor(common): buildPaginationResult helper для inter-port consumers
Вынес общий PaginationInput → PaginationResult helper из capital-service в
~/application/common/dto/pagination.dto. Consumer'ы inter-port'ов (без своего
Repository.findAndCount) получают канон-сборку без дубля логики totalPages/
currentPage.

Также `paginationInputToOffset(options)` — стандартная конверсия для адаптеров,
работающих offset-form (большинство inter-port read API).

Capital ProgramExpensesManagementService переведён на оба helper'а — ужал
listProgramExpenses до 10 строк.
2026-06-03 16:16:45 +00:00
coopops 4f15fc7c7c [C28-33][@ant] fix(capital): item-enum + canon-резолвер программных расходов
ProgramExpenseItemOutputDTO теперь отдаёт ExpenseMechanics / ExpenseRecipientType /
ExpenseItemStatus вместо magic Int — это явное нарушение canon'а GraphQL "enum
вместо строковых литералов / number". Сервис маппит number→enum зеркально
expenses-extension. Резолвер возвращает PaginationResult<T> через canon-сигнатуру
вместо «as never». Десктоп подменил локальные string-union'ы на Zeus enum
(ExpenseRecipientType / ExpenseMechanics / ExpenseProposalStatus).

Schema regen + Zeus regen + SDK build.
2026-06-03 14:29:14 +00:00
coopops bec306f97a [C28-33][@ant] fix(desktop-capital): привести страницу/диалоги к канону /_dev/ui
Why: использовал несуществующие токены/классы — UI бы рендерился со сломанными цветами/иконками. Привёл к канону:
- BaseChip: `:variant` (pos/accent/warn/neg/info/neutral), не `:label/:tone` (нет такого API).
- BaseButton: иконка через `<template #icon-left>`, не `icon='X'` (нет такого пропа).
- CSS-токены: `--p-ink`/`--p-ink-2` (не `--p-text`/`--p-muted`), `--p-fs-meta` (не `--p-fs-body-xs`).
- Утилитарные классы: `t-h2`/`t-sm`/`t-eyebrow`/`t-muted` из components.css, не `t-section`/`t-body-sm`.
2026-06-03 13:23:12 +00:00
coopops eb39e1a456 [C28-33][@ant] fix(expense): callback включает coopname — иначе capital::onpgexpdone не десериализуется
Why: send_callback_if_any слал (proposal_hash, status, amount, data), а capital::onpgexpdone ждёт (coopname, expense_hash, status, total_actual, data). Скоуп таблицы progexpenses = coopname, без него инициатор не найдёт запись. Применимо ко всем будущим consumer'ам шасси (marketplace, EMP).
2026-06-03 13:17:59 +00:00
coopops 1296b85199 [C28-33][@ant] fix(capital): маппинг enum→uint8 для mechanics/recipient_type — было Number('ADVANCE')=NaN
Why: ExpenseMechanics и ExpenseRecipientType — string enums; Number(str) даёт NaN, wharfkit не сериализует. Зеркалит маппинг из expenses-mutations.service.ts:57-63.
2026-06-03 13:15:08 +00:00
coopops 8a80c15fdb [C28-33][@ant] feat(desktop-capital): страница «Управление расходами программы»
Тонкая consumer-страница Капитала над шасси расходов. Доступ chairman+member.

  entities/ProgramExpense — store + api (Queries.Capital.GetProgramExpense{s}).
  features/ProgramExpense/CreateProgramExpense — useCreateProgramExpense:
    generateUniqueHash → reuse `generateExpenseProposalStatementDocument` шасси
    → подпись через DigitalDocument (registry 2010, Эпик 4 pipeline)
    → Mutations.Capital.CreateProgramExpense.
  features/ProgramExpense/TopupPool — useTopupProgramExpensePool:
    Mutations.Capital.TopupProgramExpense.
  pages/ProgramExpensesPage — list по канону mono-platform (BaseCard/Chip/
    EmptyState/--p-* токены), два диалога в шапке.
  install.ts — route `/:coopname/capital/program-expenses` (name
    `capital-program-expenses`, иконка receipt, agreementsBase, roles=
    chairman+member).
2026-06-03 13:05:12 +00:00
coopops d30e2ab025 [C28-33][@ant] feat(sdk): Zeus regen + Mutations/Queries.Capital.{Create,Topup,GetProgram}Expense
Регенерация после backend-расширения. Добавлены manual wrappers (SDK Mutations/
Queries.Capital — не auto-generated, обёртки над Zeus):

  Mutations.Capital.CreateProgramExpense (capitalCreateProgramExpense)
  Mutations.Capital.TopupProgramExpense  (capitalTopupProgramExpensePool)
  Queries.Capital.GetProgramExpenses     (capitalProgramExpenses + pagination)
  Queries.Capital.GetProgramExpense      (capitalProgramExpense by hash)

Селекторы:
  programExpenseSelector / programExpensesPaginationSelector
  + items/callback вложенные селекторы.
2026-06-03 12:30:47 +00:00
coopops c2b0e12115 [C28-33][@ant] feat(capital-backend): расходы программы через шасси (resolver+service)
Тонкое расширение Capital для программных расходов: чисто consumer над
шасси `expense` (только Капитал-специфика, остальное обслуживает шасси).

Чейн:
  - CapitalBlockchainPort.createProgramExpense → capital::createpgexp.
  - CapitalBlockchainPort.topupProgramExpense  → capital::topupprogexp.

Backend:
  - ProgramExpensesManagementService — write через capital, read через
    INTER_EXPENSE_CHASSIS (list по owner='capital'+action='onpgexpdone' +
    карточка по hash). Никакого собственного TypeORM-mirror.
  - ProgramExpensesResolver — chairman/member guards, mutation+query.
  - DTO: CreateProgramExpenseInputDTO (items+statement registry 2010),
    TopupProgramExpenseInputDTO, ProgramExpenseOutputDTO.
  - Регистрация в capital-extension.module.

actual_amount/status в createProgramExpense инициализируются 0/0 серверно
(их C++ дефолтит, но cooptypes требует все поля IItem).

PR2 (Капитал на шасси).
2026-06-03 12:12:58 +00:00
coopops 7323572bfc [C28-33][@ant] feat(cooptypes): capital программные расходы через шасси expense
Адаптация blagorost-cooptypes под архитектуру «capital = инициатор + callback,
шасси expense обслуживает весь flow». Из blagorost-E2 берётся минимальный набор
интерфейсов под программный расход, без debt/role/duplicate-action типов.

Изменения:
- ICreatepgexp: amount/description упрощены, добавлены operation_code+items[]
  (передаются inline в шасси expense::createexp).
- ITopupprogexp — без изменений (пополнение пула).
- IOnpgexpdone — НОВЫЙ callback от шасси, payload (hash, status, total_actual, data).
- IApprvpgexp/IAuthpgexp/IPgexppay/IDeclpgexp — УДАЛЕНЫ (шасси expense замещает).
- IProgramExpense table-interface — сохранён.
- import IItem из expense interface для типизации items[].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 12:03:57 +00:00
coopops 1c82bdebe4 [C28-33][@ant] feat(capital+expense): соединение шасси expense с capital через inline-action+callback
capital теперь только инициатор+получатель результата для программного расхода —
весь flow обслуживает шасси expense. Раньше blagorost-ветка дублировала
approve/auth/pay/decline на стороне capital.

Изменения capital:
- удалены apprvpgexp/authpgexp/pgexppay/declpgexp (дубль шасси).
- createpgexp переписан: вместо самообслуживания резервирует пул и шлёт inline
  expense::createexp с callback={capital, onpgexpdone}.
- сигнатура createpgexp принимает operation_code+items[] (передаётся в шасси).
- добавлен onpgexpdone(coopname, hash, status, total_actual, data) — callback от
  шасси на CLOSED/DECLINED, обновляет program_expense_pool и удаляет progexpense.

Изменения expense (шасси):
- callback payload расширен полем status (ExpenseDomain::ProposalStatus) — инициатор
  отличает CLOSED от DECLINED.
- declexp теперь шлёт callback тоже (раньше только closeexp).

Изменения names.hpp:
- удалён CAPITAL_RESOLVE_PROGRAM_EXPENSE / AUTHORIZE_PROGRAM_EXPENSE /
  DECLINE_PROGRAM_EXPENSE / CONFIRM_PROGRAM_EXPENSE_PAYMENT.
- добавлен ON_PROGRAM_EXPENSE_DONE.
- добавлен External::CREATE_EXPENSE_PROPOSAL для inline в шасси.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 12:03:57 +00:00
coopops 5660b06199 feat(capital/expense): расходы программы — createpgexp/apprvpgexp/authpgexp/pgexppay/declpgexp + таблица progexpenses
Story 1.5 эпика «Благорост» (blago issue 51F-17).

Полный цикл целевого расхода программы «Благорост» (не привязанного к проекту):
от заявки председателя до подтверждения оплаты gateway. Источник средств —
program_expense_pool (Story 1.6), резерв снимается окончательно в pgexppay.

PRD-B1: «createprogexp/approveprogex/authprogexp/progexppay/declprogexp». Имена
действий сокращены под 12-символьный лимит EOSIO — семантика та же.

Изменения:
- expenses.hpp: struct program_expense + таблица progexpenses (scope coopname)
  + helper'ы create/get/set_program_approved/set_program_authorized/delete
- 5 новых cpp actions в expense_managment/program_expenses/:
  - createpgexp — резервирует в State::reserve_program_expense, создаёт запись
  - apprvpgexp — председатель → soviet::create_agenda с CAPITAL_RESOLVE_PROGRAM_EXPENSE
  - authpgexp — совет → ::Gateway::create_outpay (через Action::send), статус AUTHORIZED
  - pgexppay — gateway callback → consume_program_expense + Ledger2::apply PAY_EXPENSE + delete
  - declpgexp — отказ от coopname/_soviet/_gateway → release_program_expense + delete
- names.hpp: CREATE_PROGRAM_EXPENSE, AUTHORIZE_PROGRAM_EXPENSE, DECLINE_PROGRAM_EXPENSE,
  CONFIRM_PROGRAM_EXPENSE_PAYMENT, SovietActions::CAPITAL_RESOLVE_PROGRAM_EXPENSE

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 12:03:57 +00:00
coopops d02c0ce66f feat(capital/state): program_expense_pool + topupprogexp
Story 1.6 эпика «Благорост» (blago issue 51F-18).

Программные расходы (Story 1.5) списываются из дедикейтнутого пула, а не из
аллокаций проектов. До этого коммита поля для такого пула в global_state не было.

Изменения:
- global_state.program_expense_pool — доступная сумма программы под расходы
- global_state.program_expense_reserved — зарезервированная под approved/authorized
  программные расходы (Story 1.5: createprogexp резервирует, declprogexp возвращает,
  exppaycnfrm списывает окончательно)
- State::topup_program_expense_pool / reserve_program_expense /
  release_program_expense / consume_program_expense — utility-функции для Story 1.5
- topupprogexp(coopname, amount) action — председатель переводит amount из
  global_available_invest_pool в program_expense_pool; деньги остаются на
  ledger2-кошельке BLAGOROST_FUND, меняется только разбивка по назначению

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 12:03:57 +00:00
coopops 67c5b96a18 [C28-33][@ant] feat(inter+expenses): порт InterExpenseChassisPort + adapter
Typecheck / desktop (pull_request) Failing after 7m14s
Typecheck / controller (pull_request) Failing after 6m35s
Шасси расходов получает inter-порт для consumer-extension'ов (capital,
marketplace, EMP). Порт — read-only: list-view и карточка proposal'а
по hash. Mutations approve/authorize/pay/report/close/decline остаются
прямыми GraphQL-endpoint'ами шасси, consumer их НЕ оборачивает.

inter:
  - tokens.ts: INTER_EXPENSE_CHASSIS = Symbol.for(...)
  - expense-chassis.port.ts: InterExpenseChassisPort + DTO (Proposal/Item/
    Callback/Status). Enum-статусов = зеркало доменного ExpenseProposalStatus.
  - index.ts: реэкспорт.

extensions/expenses (шасси-backend):
  - infrastructure/inter/expenses-inter-expense-chassis.adapter.ts —
    тонкий read через ExpenseProposalTypeormEntity. Проекция доменных
    полей в нейтральный inter-DTO, без зависимости inter → controller.
  - Регистрация adapter'а в expenses-extension.module + экспорт.

inter-communication-bridge.module:
  - Импорт ExpensesExtensionModule + provider INTER_EXPENSE_CHASSIS =
    useExisting ExpensesInterExpenseChassisAdapter.

PR1 (шасси-собственное), будет использовано в PR2 (Капитал на шасси).
2026-06-03 12:03:20 +00:00
claude 593ce63489 Merge pull request 'ci(typecheck): concurrency cancel-in-progress по PR-ветке' (#66) from fix/ci-typecheck-concurrency into dev 2026-06-03 09:40:28 +00:00
coopops 10e1075d5d ci(typecheck): concurrency cancel-in-progress по PR-ветке
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Новый push в PR отменяет ещё бегущий typecheck той же ветки —
прежде 30-минутные прогоны копились очередью.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 09:39:39 +00:00
coopops 1bca8cf34d [C28-32][@ant] feat(desktop+backend): подпись Classes.Document для CreateDialog/AuthorizePage + generate-mutations 2010/2011
Typecheck / desktop (pull_request) Failing after 6m29s
Typecheck / controller (pull_request) Failing after 6m30s
- Backend: новые mutations generateExpenseProposalStatementDocument (2010) и generateExpenseProposalDecisionDocument (2011) на канонe wallet (Throttle + GqlJwtAuthGuard + RolesGuard, через GeneratorInfrastructureService).
- DTOs документов 2010/2011: расширены вложенными типами header/items/decision-body (registry-Action поля проброшены в GraphQL).
- expenses-extension.module: подключён GeneratorInfrastructureModule.
- expenses-mutations.service: добавлены generateExpenseProposalStatementDocument/generateExpenseProposalDecisionDocument; spec обновлён под новый конструктор.
- SDK: handwritten Mutations.Expense.GenerateExpenseProposalStatementDocument/GenerateExpenseProposalDecisionDocument + index re-export.
- Desktop API: generateExpenseProposalStatementDocument/generateExpenseProposalDecisionDocument + authorizeExpenseReport wrappers.
- Desktop model: useExpenseProposalActions composable (submitProposal: generate→sign→createExpenseProposal; authorizeProposal: generate→sign→authorizeExpenseReport).
- ExpenseProposalCreateDialog.vue: полный flow подписи через DigitalDocument.sign; убран sha256-плейсхолдер; убран баннер «ждёт расшивки».
- ExpenseProposalAuthorizeDialog.vue: новый диалог решения председателя с автоматической генерацией+подписью документа 2011.
- ExpensesAdminAuthorizePage: добавлена кнопка «Авторизовать» рядом с «Открыть»; открывает диалог авторизации.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 08:32:08 +00:00
coopops bf73659100 [C28-31][@ant] feat(backend): расшивка createExpenseProposal/authorizeExpenseReport + capital-trigger event
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
- Расширил ExpensesBlockchainPort до 8 actions (+createExp/authExp с document2).
- Adapter: реализованы createExp/authExp через BlockchainService.transact на ключе кооператива (canon с capital).
- DTO: CreateExpenseProposalInputDTO теперь несёт username/operation_code/source_wallet/items[]/callback/statement (signed document2 registry 2010); AuthorizeExpenseReportInputDTO — decision (signed document2 registry 2011).
- Service: убраны два NotImplementedException — mutation полностью пробрасывает payload в port.
- ExpensesCapitalTriggerService: TODO log заменён на emit платформенного события `expense::capitalization::required` с реальным payload (coopname/proposal_hash/username/total_actual/block_num); capital extension Эпика 5 подпишется и сделает capital::createrid.
- cooptypes/expense.IDocument2 синхронизирован с C++ struct (version/hash/doc_hash/meta_hash/meta/signatures).
- Spec'и адаптера/сервиса: добавлены кейсы createExp/authExp; удалён ожидавший 501 кейс.
- README expenses-extension: scaffold-stub → implemented.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 08:17:51 +00:00
coopops 067718cf1f [794-4][@ant] feat(desktop): экспорт документов в .sig v2.0 + manifest.json вместо signature.txt — самодостаточный доказательный архив (canonical как есть, legacy-канонизация, встроенный сертификат) для верификатора
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
2026-06-03 08:02:59 +00:00
coopops 28ce608efe [794-4][@ant] feat(cooptypes): канонический тип SigFile v2.0 + PackageManifest для верификатора — единый источник формата .sig (целостность+подпись+OID), чтобы desktop-экспорт и верификатор ссылались на один контракт 2026-06-03 07:58:28 +00:00
coopops 8eb3996895 [C28-32][@ant] feat(desktop): ExpenseProposalCreateDialog — форма-конструктор СЗ с массивом items на canon
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
BaseDialog (size=lg) с BaseInput / q-select (recipient_type, mechanics) + динамическим массивом item-card'ов и валидацией canSubmit.

При submit:
- canonical JSON.stringify полей payload → SHA-256 через crypto.subtle (детерминированный proposal_hash);
- Mutations.Expense.CreateExpenseProposal({ coopname, proposal_hash }) — бэкенд резервирует placeholder, далее signature-pipeline document2.

Info-баннер информирует пайщика что подача откроется после Эпика 2.

7/7 страниц #104 расшито. Mutation-цепочка одобрить/авторизовать/оплатить — следующий шаг после расшивки document2 signature-pipeline.
2026-06-03 07:40:15 +00:00
coopops 530136510f [C28-32][@ant] feat(desktop): CashierPage — 4 таба кассы на canon (Queries.Expense.ExpenseProposalsByCooperative)
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
PageTabs с 4 вкладками: К оплате (AUTHORIZED) · Ждут отчёта (PARTIALLY_PAID) · На авторизации (REPORT_SUBMITTED) · Закрытые (CLOSED). Счётчики по факту items в загруженной выборке.

Структура таблицы — тот же canon: TableSkeleton + .table-wrap + .table-foot + EmptyState (per-tab title/body). Действия (оплатить + чек → MinIO, авторизовать отчёт) появятся после расшивки Эпика 2 signature-pipeline.

6/7 страниц #104 расшито. Остался только ExpenseProposalCreateDialog (mutation create — заблокирован document2 stub).
2026-06-03 07:37:27 +00:00
coopops ecd0d7e66a [C28-32][@ant] feat(desktop): ExpenseDetailPage — экран доказательств на canon (proposal + items + docs + files)
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Двухколоночный layout:
- Левая: BaseCard «Сводка» (DataRow per поле) + BaseCard «Строки расходов» (items table + статус-бейдж).
- Правая: BaseCard «Документы» (statement_doc 2010, decision_doc 2011) + BaseCard «Чеки» (ExpenseFiles c MinIO storage_key).

Источники:
- Queries.Expense.ExpenseProposal — по proposal_hash из route.
- Queries.Expense.ExpenseFilesByProposal — по coopname+proposal_hash.

Маппинг item-статусов и mechanics в model/expenseItemStatus.ts (label+variant).

5/7 страниц #104 расшито.
2026-06-03 07:35:05 +00:00
coopops 5c5657149e [C28-32][@ant] feat(desktop): председательские очереди Approve/Authorize на canon
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
ExpensesAdminApprovePage — таблица proposals в status=CREATED (in-memory filter после ExpenseProposalsByCooperative).
ExpensesAdminAuthorizePage — таблица proposals в status=REPORT_SUBMITTED (тот же подход).

Обе по канону: PageHead + TableSkeleton + .table-wrap + .table-foot + EmptyState + переход в /expenses/:hash.
Mutation-действия (одобрить/авторизовать) появятся после расшивки Эпика 2 signature-pipeline на document2.

4/7 страниц #104 расшито.
2026-06-03 07:31:02 +00:00
coopops 68122f5d32 [C28-32][@ant] feat(desktop): MyAdvancesPage через Queries.Expense.ExpenseProposalsByMember — реестр своих авансов на canon
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Table-based страница «Мои авансы» по тому же канону что ExpensesRegistryPage:
PageHead + TableSkeleton + .table-wrap + BaseBadge статуса + .table-foot + EmptyState.
Источник — useSessionStore().username + route.params.coopname.

Thin API: api/queries/getExpenseProposalsByMember.ts на Queries.Expense.ExpenseProposalsByMember.

2/7 страниц #104 расшито.
2026-06-03 07:28:33 +00:00
coopops e7b7770c4a [C28-32][@ant] feat(desktop): расшивка ExpensesRegistryPage через Queries.Expense.ExpenseProposalsByCooperative
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
- api/queries/getExpenseProposalsByCooperative — thin wrapper над SDK,
  возвращает PaginatedExpenseProposals (типизировано через IInput/IOutput).
- api/index.ts — barrel-экспорт.
- model/expenseProposalStatus — label/variant maps для 7 значений
  Zeus.ExpenseProposalStatus (CREATED/AUTHORIZED/PARTIALLY_PAID/REPORT_SUBMITTED/
  CLOSED/DECLINED/UNDEFINED) + helpers с дефолтом neutral.
- ExpensesRegistryPage.vue: pug + .table-wrap canon, TableSkeleton(6 cols),
  EmptyState (receipt_long 48px), BaseBadge с variant из map, BaseButton ghost
  "Загрузить ещё", монохешчик в .t-mono-sm. coopname из route.params,
  sortBy=created_at DESC, page-by-page до hasMore.

Open: tooltip с FQH хеша, sort по колонкам, filter-bar по статусу — будут
вместе со страницами /expenses/admin/{approve,authorize} и /my/advances
(своя сигнатура запроса by-member).
2026-06-03 07:23:29 +00:00
coopops bfdabcfded [C28-31][@ant] feat(sdk): expense namespace — 8 mutations + 6 queries + selectors
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Mutations.Expense: CreateExpenseProposal / PayExpenseItem / ReportExpenseItem /
ReturnExpenseItem / OverspendExpenseItem / SubmitExpenseReport /
AuthorizeExpenseReport / DeclineExpenseReport.

Queries.Expense: ExpenseProposal / ExpenseProposalsByCooperative /
ExpenseProposalsByMember / ExpenseFile / ExpenseFilesByItem /
ExpenseFilesByProposal.

Selectors.expense*: ExpenseProposal (с items + statement/decision DocumentAggregate),
ExpenseItem, ExpenseFile, ExpenseProposalsPagination.

Zeus regen + re-exports в mutations/queries/selectors/index.ts.

Разблокирует #103 backend хвост и #104 UI расшивку 7 страниц end-to-end.
2026-06-03 07:18:45 +00:00
coopops a7bd5aae5c [C28-31][@ant] feat(controller): createExpenseProposal stub — ждёт document2 statement_doc
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
8-я mutation симметрично authorizeExpenseReport: NotImplementedException
со ссылкой на statement_doc (type=2010) от signature-pipeline UI Эпика 2.
Регистрирует GraphQL-сигнатуру end-to-end, чтобы SDK + UI видели mutation
до подключения document2.

jest 8/8 passed.
2026-06-03 06:17:07 +00:00
coopops 33d8163d08 [C28-31][@ant] test(controller): contract-spec adapter — formats transact для 6 actions
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
7 тестов проверяют что каждое из 6 действий expense-контракта уходит в
BlockchainService.transact с правильным форматом:
  account = ExpenseContract.contractName.production
  name    = ExpenseContract.Actions.<X>.actionName
  authorization = [{actor: coopname, permission: 'active'}]

Защита от дрейфа cooptypes actionName → adapter имя. Также verify
HttpApiError 502 при отсутствии WIF в vault'е (initialize/transact не зовутся).

jest 7/7 passed.
2026-06-03 05:51:24 +00:00
coopops 531b9574fe [C28-31][@ant] feat(controller): добавляю overspendExp — 6-я работающая mutation
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Доплата при перерасходе (expense::overspendexp, ADVANCE-механика):
- OverspendExpenseItemInputDTO (coopname/proposal_hash/item_hash/overspend_amount)
- ExpensesBlockchainPort.overspendExp + adapter (transact с coop-ключом)
- ExpensesMutationsService.overspendExpenseItem
- ExpenseMutationsResolver.overspendExpenseItem (@AuthRoles chairman)
- spec: 7-й тест action-mapping

Теперь 6 из 8 actions контракта expense работают через port:
payexp / reportexp / returnexp / overspendexp / closeexp / declexp.
createexp + authexp ждут document2 от signature-pipeline UI Эпика 2.

tsc EXIT=0, jest 7/7 passed.
2026-06-03 05:49:42 +00:00
coopops fbfd0a259e [C28-32][@ant] feat(desktop): регистрация expenses extension в registry
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Шасси расходов подключено к init-installed-extensions:
- expenses → expensesInstall в extensionsRegistry
- install.ts: убран TODO-комментарий о блокировке C28-31

Workspace 'expenses' (icon: receipt_long) с 6 routes (registry/admin/cashier/my)
становится доступен ролям chairman + user из конфигурации кооператива.
Stub-страницы внутри ждут расшивки через SDK Zeus после generate-client.
2026-06-03 05:46:00 +00:00
coopops b4dd4b335a [C28-31][@ant] feat(controller): расшивка 5 mutations через ExpensesBlockchainPort
Typecheck / desktop (pull_request) Successful in 13m18s
Typecheck / controller (pull_request) Successful in 13m12s
После Эпика 0 cooptypes/expense — подключаю реальный chain-submit через
port+adapter (канон CapitalBlockchainAdapter):

- ExpensesBlockchainPort: 5 actions без document2 (payexp/reportexp/returnexp/closeexp/declexp)
- ExpensesBlockchainAdapter: BlockchainService.transact + vault-ключ кооператива
- ExpensesMutationsService DI на port — 5 мутаций работают, authorize ждёт decision_doc Эпика 2
- spec обновлён: 6 тестов через mocked port, action-mapping verify

createexp + authexp ждут signature-pipeline UI Эпика 2 (document2 type=2010/2011).
overspendexp подключится с UI Phase 2.

tsc EXIT=0, jest 6/6 passed.
2026-06-03 05:29:41 +00:00
coopops da50e423ca [9.4.b] feat(controller+desktop): реальный remote-loader fetch+eval install.js
Typecheck / desktop (pull_request) Successful in 13m24s
Typecheck / controller (pull_request) Successful in 13m19s
mono controller:
- AppsCatalogHttpService.fetchInstallScript(scope, name) — fetch text
  install.js из ca-admin /v1/public/packages/:scope/:name/install.js;
  degraded mode (null) при отсутствии env-config'а apps-catalog'а.
- Новый REST controller AppsCatalogInstallScriptController
  `GET /v1/apps-catalog/install/:scope/:name` под HttpJwtAuthGuard:
  валидирует scope/name regex'ом, проксирует на ca-admin, отдаёт
  text/javascript + Cache-Control: no-store. Admin-API key остаётся
  на backend'е.
- AppsCatalogProxyModule: добавлен controller в module.controllers[].

mono desktop:
- remote-loader.ts: реальный fetch + CJS-eval. fetchInstalledRemotePackages
  читает Queries.Extensions.AppsCatalogRemotePackages, парсит packageId
  на (scope, name). loadRemoteExtension качает install.js по REST через
  axios responseType:'text' с JWT desktop'а, оборачивает в
  `new Function('module','exports','require', code)`, вызывает
  `module.exports.install()` → IWorkspaceConfig[].
- Try/catch per item (NFR15-стиль): падение одного пакета не валит
  остальные. require() сразу throw — пакеты должны быть CJS без
  внешних зависимостей (sandbox — Story 11.x).
- sha256 пока не проверяем (release-таблица будет в 9.4.c).

Demo path: voskhod publishes @voskhod/demo-app → ca-admin отдаёт
examples/demo-app/install.js → controller-proxy → desktop boot тянет +
eval'ит + получает workspace «Demo» с роутом demo-hello.
2026-06-03 05:26:05 +00:00
coopops 2fcbe3612f [C28-29][@ant] feat(cooptypes): Эпик 0 — типы контракта expense (8 actions + proposals table)
Typecheck / desktop (pull_request) Successful in 13m22s
Typecheck / controller (pull_request) Successful in 13m5s
- interfaces/expense.ts: 8 action input + IItem/IProposal/ICallbackHandler (hand-written из expense.hpp до ABI regen)
- contracts/expense/{actions,tables,index}: тонкие обёртки в стиле capital — authorizations + actionName + типовой re-export
- common/names: _expense ({production:'expense',testnet:'expense'})
- interfaces/index + contracts/index: re-export ExpenseContract
- ledger2/wallets.generated: +w.exp.adv (prebuild gen-from-cpp подцепил свежий wallet)

build EXIT=0, dist exports ExpenseContract; smoke node: contractName=expense, 8 actions, 1 table
2026-06-03 05:18:06 +00:00
coopops ece4d59c56 [C28-31][@ant] test(controller): smoke-контракт mutations — все 6 методов throw NotImplementedException до Эпика 0
Typecheck / desktop (pull_request) Successful in 13m29s
Typecheck / controller (pull_request) Successful in 13m30s
2026-06-03 05:02:44 +00:00
coopops 45def1bbec [9.5.b] feat(sdk+desktop): Zeus client + ExtensionsShowcase реальный fetch apps-catalog
Typecheck / desktop (pull_request) Failing after 7m27s
Typecheck / controller (pull_request) Failing after 7m36s
- sdk/zeus: regenerated через `controller generate-schema && generate-client`
  + `pnpm -F sdk build`; типы AppsCatalogRemotePackageDTO и
  Query.appsCatalogRemotePackages пробрасываются end-to-end.
- sdk/selectors/extensions: новый appsCatalogRemotePackageSelector с
  валидатором MakeAllFieldsRequired.
- sdk/queries/extensions: новый namespace Queries.Extensions.AppsCatalogRemotePackages
  (page/pageSize → AppsCatalogRemotePackageDTO[]).
- desktop/entities/Extension/api: api.loadAppsCatalogRemotePackages(page, pageSize)
  по канону client.Query + строгие типы из @coopenomics/sdk.
- desktop/pages/ExtensionStore/ExtensionsShowcase: убран inline-mock,
  каталог загружается через Zeus query. Добавлены loading/error/empty
  состояния по дизайн-канону (--p-*, base-card токены), плюсификация
  слова «пакет», version-чип и Intl-форматирование цены.

Это шаги 3–4 V2 9.5.b. Закрывает task #38 и #37.
2026-06-03 04:52:20 +00:00
coopops ce01d48df7 [C28-31][@ant] test(controller): unit-тест capital-trigger — фильтр CLOSED + total_actual
Typecheck / desktop (pull_request) Successful in 13m20s
Typecheck / controller (pull_request) Successful in 12m56s
Покрывает 5 кейсов: REPORT_SUBMITTED/AUTHORIZED/DECLINED → no-op;
CLOSED без total_actual → warn; CLOSED + total_actual → log с
TODO-ссылкой на Эпик 0. Защищает контракт триггера на будущее, когда
skeleton будет заменён на реальный capital::createrid через
WriteMutationPool.
2026-06-03 04:48:45 +00:00
coopops cb22349401 [C28-31][@ant] feat(controller): authorize/decline СЗ-отчёта — skeleton mutations
Typecheck / desktop (pull_request) Successful in 13m52s
Typecheck / controller (pull_request) Successful in 13m11s
GraphQL surface: authorizeExpenseReport (председатель утверждает,
триггерит capital-trigger → capitalization Благороста) +
declineExpenseReport (с reason, переход в DECLINED, capitalization
НЕ запускается). Skeleton — NotImplementedException, chain-submit
после Эпика 0 (cooptypes regen для expense::authexp / declineexp).
2026-06-03 04:34:22 +00:00
coopops a6d6b0af2f [9.5.b] fix(apps-catalog-proxy): явный type fn для nullable Field + .env.example
Typecheck / desktop (pull_request) Failing after 7m15s
Typecheck / controller (pull_request) Failing after 7m21s
При nullable=true декоратор @Field NestJS GraphQL не умеет вывести тип из
TS-reflection-метаданных (TS пишет в metadata-ключ Object). Без явного
type fn (() => String) — UndefinedTypeError на старте.

Фикс:
- @Field({ nullable: true }) → @Field(() => String, { nullable: true })
  для lastActiveVersion: string | null.
- .env.example добавлены APPS_CATALOG_URL и APPS_CATALOG_API_KEY с
  готовыми значениями для dev (URL = DNS-имя ca-admin в mono-shared
  сети).

Verify: coopback стартует чисто (Nest application successfully started),
DTO виден через introspection (__type AppsCatalogRemotePackageDTO →
поля packageId/publisher/title/description/rubPerMonth/lastActiveVersion/
compatibleSubnets), guard срабатывает (без JWT → 401 Unauthorized).
2026-06-02 22:00:59 +00:00
coopops 5a2fc336e9 [9.5.b] feat(controller): apps-catalog-proxy — резолвер appsCatalogRemotePackages
Typecheck / desktop (pull_request) Failing after 7m39s
Typecheck / controller (pull_request) Failing after 7m23s
Story 9.5.b mono часть: добавляет модуль расширения
`apps-catalog-proxy` с GraphQL Query.appsCatalogRemotePackages, который
проксирует запрос на apps-catalog/ca-admin /v1/public/packages.

Архитектурно — выбран альтернатива C (см. apps-catalog
docs/epics/E9-demo-app-pilot.md, V2 follow-ups): браузер ходит в каталог
через mono controller, а не напрямую в ca-admin. Поэтому:
- admin-secret apps-catalog'а живёт в env controller'а, не в SPA;
- нет CORS — desktop запрос на свой же origin;
- единый канон mono GraphQL (никаких raw HTTP в desktop).

Компоненты:
- AppsCatalogRemotePackageDTO (@ObjectType): packageId/publisher/
  compatibleSubnets/lastActiveVersion + UI-поля title/description/
  rubPerMonth (в V1 description и rubPerMonth — заглушки; в V2 будут из
  package manifest и apps-catalog pricing).
- AppsCatalogHttpService: axios-клиент к ca-admin с Authorization: Bearer
  admin-key из APPS_CATALOG_API_KEY. Degraded mode (пустой список без
  ошибки), если env пустой — чтобы dev/CI без apps-catalog не валили
  desktop boot.
- AppsCatalogProxyResolver: @Query, @UseGuards(GqlJwtAuthGuard) — каталог
  видят только авторизованные пайщики.
- Регистрация в ExtensionsModule.

Config:
- Новые env APPS_CATALOG_URL и APPS_CATALOG_API_KEY (оба optional).
- Зеркальные поля в config.apps_catalog.{url, api_key}.

TODO (требует ручного запуска на остановленном контейнере, см. CLAUDE.md:
'pnpm generate-schema / generate-client — не запускать локально'):
- pnpm -F controller generate-schema → пересоздать schema.gql.
- pnpm -F controller generate-client → graphql-zeus в SDK.
- pnpm -F sdk build → unbuild dist.
Без codegen фронт ещё не сможет дёрнуть резолвер через Zeus.
2026-06-02 21:55:19 +00:00
coopops f1dd62cfce [C28-31][@ant] feat(controller): capital-trigger skeleton — capitalization Благороста на CLOSED
Typecheck / desktop (pull_request) Successful in 13m27s
Typecheck / controller (pull_request) Successful in 13m2s
ExpenseProposalSyncService override handleSyncDelta → emit
`entitysynced::expense::proposals` после save (per-contract pubsub,
canon controller/CLAUDE.md). ExpensesCapitalTriggerService подписан,
проверяет переход в CLOSED + total_actual → log skeleton. Прямой
DI на CapitalExtensionModule ждёт Эпика 0 (cooptypes regen +
WriteMutationPool для capital::createrid-эквивалента).
2026-06-02 21:45:42 +00:00
coopops f24e3fffbf [C28-31][@ant] feat(controller): DocumentAggregate для statement_doc/decision_doc СЗ
Typecheck / desktop (pull_request) Successful in 13m46s
Typecheck / controller (pull_request) Successful in 13m10s
- `ExpensesManagementService.buildProposalDocumentAggregates(entity)` собирает
  оба подписанных дока (заявление + решение) через `DocumentAggregationService`
  (платформенный domain-сервис, `~/domain/document`); по EMPTY_HASH (`0x000…`)
  возвращает `null` — это валидное состояние chain-mirror'а, отличающееся от
  `undefined` («дока нет / зеркало ещё не пришло»).
- `ExpenseProposalOutputDTO.statement_doc` / `decision_doc` теперь типизированные
  `DocumentAggregateDTO | null` — UI сразу видит подпись/хэш/raw-документ
  единообразно с agreement/agenda/meet резолверами. Boolean-флаги `has_*_doc` сняты.
- `ExpenseProposalResolver` подключает агрегаты:
  - одиночный fetch — последовательно;
  - paginated — `Promise.all` по странице (10–20 смет на запрос — OK).
- `ExpensesExtensionModule` импортирует `DocumentDomainModule` для DI.

controller-tsc — 0 ошибок.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 21:15:01 +00:00
coopops 45a9381c4b [C28-31][@ant] feat(controller): submitExpenseReport + pagination для списков смет
Typecheck / desktop (pull_request) Successful in 13m19s
Typecheck / controller (pull_request) Successful in 13m4s
- `SubmitExpenseReportInputDTO` + `ExpenseMutationsResolver.submitExpenseReport`
  (chairman/member/user) — финализация СЗ-отчёта пайщиком когда все items закрыты;
  сервис — skeleton `NotImplementedException`, реальный chain-submit (`expense::closeexp`)
  подключим после Эпика 0 (cooptypes regen).
- `findByCoopnamePaginated` / `findByUsernamePaginated` в `ExpenseProposalRepository`
  + TypeORM-имплементация на `findAndCount` (sort по `_created_at DESC`, limit ≤200).
- `listProposalsByCooperativePaginated` / `ByMemberPaginated` в service'е.
- `ExpenseProposalResolver` — paginated по канону controller'а
  (`PaginationInputDTO` + `createPaginationResult('PaginatedExpenseProposals')`).

controller-tsc — 0 ошибок.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 20:45:36 +00:00
coopops 25e2d99046 [C28-31][@ant] feat(controller): expense files service + upload mutation
Typecheck / desktop (pull_request) Successful in 13m17s
Typecheck / controller (pull_request) Successful in 12m50s
- ExpenseFilesService с @UseBucket(EXPENSES_BUCKET):
  upload (base64 → SHA-256 verify → MinIO put → БД-запись),
  getReadUrl, listByProposal/Item, deleteFile;
  детерминированный ключ MinIO {coopname}/{proposal}/{item|_proposal}/{kind}/{checksum}.{ext};
  409 на повтор по checksum (per-coopname idempotence).
- UploadExpenseFileInputDTO: валидация mime allowlist + size_bytes + SHA-256 regex.
- ExpenseFileOutputDTO с короткоживущим read_url (HMAC-signed, TTL бакета).
- ExpenseFilesResolver: uploadExpenseFile + expenseFile/expenseFilesByProposal/ByItem.
- FileStorageInfrastructureModule.forFeature([ExpenseFilesService]) wire.

tsc --noEmit: 0 ошибок.
2026-06-02 20:16:16 +00:00
coopops 237cd19e62 [9.5.a] feat(ExtensionStore): inline-моки remote-витрины demo
Typecheck / desktop (pull_request) Failing after 7m18s
Typecheck / controller (pull_request) Failing after 7m14s
Секция «Удалённые пакеты» с карточкой @voskhod/demoapp (publisher,
price 1000 ₽/мес) + dialog «Подписаться» (V1 — заглушка).

Why: Анна видит реальный второй раздел, отделённый от bundle-расширений,
и проходит UX-сценарий клика «Подписаться» — без backend-резолверов.
Реальный список remoteExtensions через Query.appsCatalogRemotePackages
+ Mutation.appsCatalogSubscribe — отложено в 9.5.b (см. mono#64
follow-ups).

Стили — токены MONO Design System (--p-*), никаких magic hex.
2026-06-02 20:10:34 +00:00
coopops 9c8449408f [C28-31][@ant] feat(controller): expense mutation DTO + skeleton resolver
Typecheck / desktop (pull_request) Successful in 13m1s
Typecheck / controller (pull_request) Successful in 13m1s
- pay/report/return-expense-item.input.ts с asset-regex валидацией
- ExpensesMutationsService: skeleton с throw NotImplementedException
  (chain-submit через write-mutation pool подключится после Эпика 0
  cooptypes regen для контракта expense)
- ExpenseMutationsResolver: 3 Mutation (chairman / member+user)
  возвращают TransactionDTO — sigнатура зафиксирована в схеме сразу

GraphQL: payExpenseItem (chairman), reportExpenseItem (chairman/member/user),
returnExpenseItem (chairman/member/user).

tsc --noEmit: 0 ошибок.
2026-06-02 19:44:26 +00:00
coopops da0690bca1 [9.5] feat: Epic 9 hint-banner в pages/ExtensionStore/ExtensionsShowcase
Typecheck / desktop (pull_request) Failing after 7m22s
Typecheck / controller (pull_request) Failing after 7m15s
V1 минимум для UI ExtensionStore:
- Информационный banner сверху grid'а карточек, объясняющий что bundle-
  extensions ниже бесплатные, а remote-пакеты с подпиской появятся
  на следующем шаге пилота.
- Реальная карточка remote-пакета с кнопкой «Подписаться» / «Уже
  подписан до DD.MM» — story 9.5.b (требует расширения extStore
  на kind/subscribed/pricingRubPerMonth + backend-резолверы coopback'а).

Refs: docs/integration/native-to-catalog-research.md §9 story 9.5.
2026-06-02 19:21:56 +00:00
coopops 9f6d2dc699 [9.3][9.4] feat: стол разработчика + remote-loader каркас в desktop
Typecheck / desktop (pull_request) Failing after 7m22s
Typecheck / controller (pull_request) Failing after 7m21s
9.3 — components/desktop/extensions/developer/:
- install.ts: workspace `developer`, defaultRoute=developer-my-packages,
  4 страницы (packages, publish, subscribers, pricing), все under
  meta.roles=['chairman']. Регистрация в extensions-registry.ts.
- pages/: V1 Vue-заглушки с описанием TODO (без реальных мутаций).
  Mutation flow (POST /v1/admin/package, /releases, /pricing с подписью
  chairman'а) — story 9.3.b.

9.4 — processes/init-installed-extensions/remote-loader.ts:
- Контракт fetch/load для remote-пакетов с активной подпиской: тип
  RemoteExtensionDescriptor, функции fetchInstalledRemotePackages,
  loadRemoteExtension, installRemoteExtensions.
- V1 — заглушка возвращающая []. Не падает, логирует пропуск.
- index.ts: после bundle-pass — remote pass через installRemoteExtensions;
  try/catch чтобы падение remote-flow не валило bundle extensions.
- TODO 9.4.b: реальный fetch к coopback + tar-x + dynamic require
  + sha256-check + (V2 sandbox).

Видимость стола: пока через meta.roles в install.ts. Полноценная
авторизация через DesktopWorkspace.grants с флагом is_operator_coop —
9.3.b/9.5.

Refs: /home/admin/mono-ai-5/docs/integration/native-to-catalog-research.md §9.
2026-06-02 19:20:57 +00:00
coopops e248ddb2b0 [C28-31][@ant] feat(controller): expense read resolver + output DTO
Typecheck / desktop (pull_request) Successful in 13m9s
Typecheck / controller (pull_request) Successful in 12m48s
- ExpenseProposalOutputDTO + ExpenseItemOutputDTO + fromDomain mapper
- mechanics/recipient_type/item_status: uint8 → enum
- ExpenseProposalResolver — 3 query (byHash / byCooperative / byMember)
- Auth: chairman/member для списка по коопу, +user для своих
2026-06-02 19:15:14 +00:00
coopops 89f7ded6fc [C28-31][@ant] feat(controller): expenses syncer + management read-service
Typecheck / desktop (pull_request) Successful in 13m8s
Typecheck / controller (pull_request) Successful in 12m45s
- ExpenseProposalSyncService — extends AbstractEntitySyncService, OnEvent fork::*
- ExpensesManagementService — read-only (proposalByHash / byCoop / byMember)
- DI wire в ExpensesExtensionModule
2026-06-02 18:58:28 +00:00
coopops 9359e6b324 [C28-31][@ant] feat(controller): expenses repositories + database module + DI wire
Typecheck / desktop (pull_request) Successful in 13m18s
Typecheck / controller (pull_request) Successful in 12m50s
ExpenseProposalTypeormRepository extends BaseBlockchainRepository (sync_key
proposal_hash, findByCoopname/findByUsername/findByProposalHash).
ExpenseFileTypeormRepository — БД-only, поиск по checksum/proposal/item.
ExpensesDatabaseModule — TypeOrm.forFeature на 2 зеркала + EntityVersion.
ExpensesExtensionModule подаёт EXPENSE_PROPOSAL_REPOSITORY / EXPENSE_FILE_REPOSITORY
через useClass; EntityVersioningService приходит из @Global() TypeOrmModule.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 18:54:12 +00:00
coopops e8f5d7604e [C28-31][@ant] feat(controller): expenses infra — typeorm-зеркала, mappers, delta-mapper
Typecheck / desktop (pull_request) Successful in 13m14s
Typecheck / controller (pull_request) Successful in 12m58s
ExpenseProposalTypeormEntity (proposal_hash sync_key, items/docs в jsonb),
ExpenseFileTypeormEntity (БД-only, unique по coopname+checksum).
ExpenseProposalMapper TypeORM↔Domain (toUpdateEntity = только block_num/present
по канону: блокчейн-поля обновляются через AbstractEntitySyncService).
ExpenseFileMapper — symmetric copy.
ExpenseContractInfoService — паттерны таблиц для parser2.
ExpenseProposalDeltaMapper extends AbstractBlockchainDeltaMapper, lowercase
proposal_hash, signed-docs через DomainToBlockchainUtils.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 18:44:24 +00:00
coopops f7a5defa95 [C28-31][@ant] feat(controller): expenses domain — interfaces, entity, repo tokens
Typecheck / desktop (pull_request) Successful in 13m15s
Typecheck / controller (pull_request) Successful in 12m47s
ExpenseProposalDomainEntity на BaseDomainEntity (зеркалит ExpenseDomain::proposal)
+ IExpenseProposalBlockchainData/DatabaseData, IExpenseItemBlockchainData/DatabaseData,
IExpenseFileDatabaseData (только БД, файл в MinIO).
Repository-интерфейсы + DI-токены EXPENSE_PROPOSAL_REPOSITORY, EXPENSE_FILE_REPOSITORY.
mapStatusToDomain зеркалит uint8_t status, неизвестное → UNDEFINED.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 18:34:28 +00:00
coopops c18828c534 docs(apps-catalog): Epic 9 MVP demo-app pilot — раскладка решений и stories
Typecheck / desktop (pull_request) Successful in 13m20s
Typecheck / controller (pull_request) Successful in 12m58s
Финальные решения по V1 пилоту (без миграции native):
- demo-app = пустой Hello-стол, @voskhod/demo-app
- Pricing 1000 RUB/мес (60 сек на dev), trial 15 мин
- partner1 как покупатель через pnpm boot:extra (на той же цепи)
- Self-subscription bypass: воскод как оператор подписан бесплатно
- Без кэша tarball'ов — каждый desktop boot качает заново
- Стол Разработчика = новый bundle-extension developer, виден chairman оператора
- partner1 desktop+coopback в отдельном docker overlay из apps-catalog

Epic 9 раскладка на 8 stories с зависимостями. Реализация — следующими
коммитами в зонтичной ветке apps-catalog mono + feat/E9-demo-app-pilot
в apps-catalog репо.
2026-06-02 18:30:18 +00:00
coopops 1c46fd0338 [C28-31][@ant] feat(controller): scaffold expenses extension — enum'ы + bucket + module wiring
Typecheck / desktop (pull_request) Successful in 13m4s
Typecheck / controller (pull_request) Successful in 13m4s
Шаг 1 из C28-31 (backend шасси расходов):
- 5 enum'ов (registerEnumType): ProposalStatus / ItemStatus / Mechanics / RecipientType / FileKind — зеркалят expense.hpp
- EXPENSES_BUCKET (constants/expenses-bucket.ts): MinIO-bucket spec для платёжек/чеков/возвратов
- ExpensesExtensionModule (минимальный) подключён в ExtensionsModule.register() (не desktop-расширение, без записи в AppRegistry)

Дальше: domain entities (composite db/bc/derived), TypeORM-зеркало, delta-mapper, sync-service, resolver. Реальная sync-инфра — после ABI regen контракта expense.
2026-06-02 18:24:07 +00:00
coopops 8897a93e83 [C28-29][@ant] feat(contracts): новый контракт expense — 8 actions, шасси расходов
Typecheck / desktop (pull_request) Successful in 13m4s
Typecheck / controller (pull_request) Successful in 12m44s
components/contracts/cpp/expense/:
- expense.hpp: 1 таблица proposals (scope=coopname; secondary by_hash/by_username/by_status),
  типы item, callback_handler, enum Mechanics/RecipientType/ProposalStatus/ItemStatus.
- expense.cpp: 8 actions (createexp/authexp/declexp/payexp/reportexp/closeexp/returnexp/overspendexp).
- CMakeLists.txt: стандартный add_contract(expense expense expense.cpp).

components/contracts/cpp/lib/consts.hpp:
- _expense = expense_n + добавлен в contracts_whitelist.

components/contracts/CMakeLists.txt:
- add_contract_build(expense).

Принцип v3:
- Один процесс p.exp.expns, 5 ledger2 operation_codes (o.exp.{blgadv,blgdir,advrpt,advret,over}).
- createexp принимает operation_code+source_wallet+callback в payload — контракт агностичен к источнику.
- payexp на DIRECT-механике сразу помечает item REPORTED (BURN blago = один полный шаг).
- payexp на ADVANCE — отдельный reportexp пайщиком после чека.
- closeexp шлёт inline action на сохранённый callback (contract,action,data) если он непустой.
- overspendexp — две последовательные Ledger2::apply (OVERSPEND + ADVANCE_REPORT) в одной транзакции.
2026-06-02 18:17:12 +00:00
coopops 87834c4e56 docs(apps-catalog): research-доклад по миграции native extensions в каталог
Typecheck / desktop (pull_request) Successful in 13m21s
Typecheck / controller (pull_request) Successful in 12m43s
2026-06-02 18:04:20 +00:00
coopops 42165ec3e8 [C28-28][@ant] feat(ledger2): шасси расходов — 1 кошелёк, 1 процесс, 5 операций
Typecheck / desktop (pull_request) Successful in 13m21s
Typecheck / controller (pull_request) Successful in 13m6s
wallets.hpp:
- w.exp.adv ADVANCE_HOLD (USER_SHARED) — кошелёк-резерв подотчёта пайщика
- mapping без program-gate: source-программа проверяется контрактом expense

processes.hpp:
- p.exp.expns PROPOSAL — единый процесс расхода по СЗ

operations.hpp (namespace operations::expense):
- o.exp.blgadv BLAGO_ADVANCE  — TRANSFER blago→adv, Дт 08 / Кт 51
- o.exp.blgdir BLAGO_DIRECT   — BURN blago,       Дт 08 / Кт 51
- o.exp.advrpt ADVANCE_REPORT — BURN adv,         без проводки (canal уже сделан)
- o.exp.advret ADVANCE_RETURN — TRANSFER adv→blago, Дт 51 / Кт 08 (зеркало blgadv)
- o.exp.over   OVERSPEND      — TRANSFER blago→adv, Дт 08 / Кт 51 (перерасход)

Принцип: расход не уменьшает паевой фонд (80) — меняется только форма актива
51 → 08. Канал 08/51 строится ровно один раз: на blgadv/blgdir. ADVANCE_REPORT —
только BURN кошелька-резерва. Все ack v3 (см. NAMING-C28-28.md commit 143d9a9d30).
2026-06-02 17:58:31 +00:00
claude c5496a7988 Merge pull request 'fix: перекрёст user↔privacy в coagreements + html2pdf SSR' (#63) from feat/decision-authorize-backend into dev 2026-06-02 17:57:27 +00:00
coopops 3482cfe094 fix(desktop): html2pdf — ленивый import, чинит SSR boot error «self is not defined»
Typecheck / desktop (pull_request) Successful in 13m17s
Typecheck / controller (pull_request) Successful in 12m53s
html2pdf.js на верхнем уровне модуля трогает `self` → при SSR-бандле Quasar
падает [Quasar] boot error: ReferenceError: self is not defined. Экспорт PDF
нужен только на клиенте по действию пользователя — переносим импорт в
dynamic import() внутри exportFormToPdf, сервер модуль не вычисляет.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:57:04 +00:00
coopops 01c4815c65 fix(soviet): coagreements — user→draft_id 4, privacy→draft_id 3
Были перекрещены относительно реестра документов: draft 3=PrivacyPolicy,
4=UserAgreement (и тексты on-chain drafts, и cooptypes Registry, и protocol-vars
совета это подтверждают). Только make_base_coagreements крестил user↔privacy.
Затрагивает вновь создаваемые коопы; существующим нужна отдельная миграция.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:57:04 +00:00
coopops 143d9a9d30 [C28-28][@ant] docs: v3 — все ack получены, callback как переменная при createexp, проводки 08/51 для обеих механик Благороста
Typecheck / desktop (pull_request) Successful in 13m31s
Typecheck / controller (pull_request) Successful in 13m8s
2026-06-02 17:56:10 +00:00
coopops bf6a5fe8a3 [C28-34][@ant] docs: 2012/2013/2014 убраны — это файлы (MinIO), не document2; assertions на o.exp.* коды
Typecheck / desktop (pull_request) Successful in 13m36s
Typecheck / controller (pull_request) Successful in 13m4s
2026-06-02 17:31:06 +00:00
coopops c3cf1201c8 [C28-27][@ant] chore: убрать schema.gql из трекинга (gitignored-артефакт)
Typecheck / desktop (pull_request) Failing after 4m47s
Typecheck / controller (pull_request) Failing after 4m47s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:29:56 +00:00
coopops a22abfc45f [C28-27][@ant] chore: кодоген схемы и Zeus-клиента для операций Центра уведомлений
Typecheck / desktop (pull_request) Failing after 4m48s
Typecheck / controller (pull_request) Failing after 4m46s
generate-schema + generate-client: getNotifications/getInboxNotifications/
getUnreadNotificationsCount/markNotificationRead/markAllNotificationsRead/
resendNotification + enum NotificationChannel/NotificationOutboxStatus.
SDK-операции теперь компилируются (tsc зелёный).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:26:55 +00:00
coopops 88d47afb7a [C28-27][@ant] fix: guard журнала по coopname — доступ только к своему кооперативу
getNotifications/getNotification/resendNotification сверяют coopname
(фильтра и найденной строки) с config.coopname → ForbiddenException на чужой.
Канон-паттерн контроллера (free-decision/branch interactor). Federation-hardening.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:26:55 +00:00
claude dedcbfb5fe Merge pull request 'Кнопка подписи в прижатый футер + забытый WIP (authorize RAM-payer, boot pf, canon docs)' (#62) from feat/decision-authorize-backend into dev
Build bootstrap container / build (push) Successful in 3m28s
2026-06-02 17:17:09 +00:00
coopops 42722efb59 fix(desktop): кнопка «Подписать» в прижатый футер диалога — доступна без прокрутки длинного документа
Typecheck / desktop (pull_request) Successful in 13m31s
Typecheck / controller (pull_request) Successful in 13m10s
Документ остаётся в скроллящемся body BaseDialog, кнопка вынесена в слот
footer (.base-dialog__foot, вне overflow) — устраняет залипание, когда до
кнопки нельзя домотать (стек maximized-диалогов RequireAgreements).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:15:19 +00:00
coopops b058c7d9bd docs: канон вёрстки desktop → skill /mono-desktop-canon 2026-06-02 17:14:51 +00:00
coopops 472db16d4b [@ant] feat(boot): builtin protocol-feature ASSERT_RECOVER_KEY_ACCOUNT 2026-06-02 17:14:51 +00:00
coopops b60b281fc3 [@ant] fix(soviet): authorize — RAM-плательщик coopname, не председатель (tx подписывает кооператив) 2026-06-02 17:14:51 +00:00
coopops 3fd81215cf [C28-27][@ant] fix: тень панели колокола — канон-токен --p-shadow-pop вместо несуществующего --p-elev-2
Typecheck / desktop (pull_request) Failing after 4m50s
Typecheck / controller (pull_request) Failing after 4m48s
Code-review нашёл: --p-elev-2 нет в tokens.css → box-shadow не отрисовывался.
Канон-тень overlay (поповер) — --p-shadow-pop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:11:28 +00:00
coopops 043a52abf3 [C28-28][@ant] docs: универсальное шасси v2 — без 60/71, 5 op-кодов, payload-флаг capital trigger
Typecheck / desktop (pull_request) Successful in 13m31s
Typecheck / controller (pull_request) Successful in 13m32s
2026-06-02 17:08:01 +00:00
coopops b9c96bed6d [C28-28][@ant] docs: naming proposal — 6 op-кодов вместо 5 + 60/71 счета + capital trigger
Typecheck / desktop (pull_request) Successful in 13m34s
Typecheck / controller (pull_request) Successful in 13m23s
2026-06-02 16:09:49 +00:00
coopops 61f35b1ef9 [C28-34][@ant] docs: план e2e Journey A/B — scaffold-stub до выбора фреймворка и закрытия C28-28..33
Typecheck / desktop (pull_request) Successful in 13m31s
Typecheck / controller (pull_request) Successful in 13m28s
E2E.md в expenses/:
- Journey A (DIRECT-оплата Yandex Cloud из BLAGOROST_POOL): 10 шагов + matrix ledger2/document2 assertions
- Journey B (ADVANCE master+svetlana 2 items): 9 шагов + matrix
- Helpers-skeleton (loginAs, seedBlagorostPool, mockCouncilAuthorize, fixture*, assertLedgerOp/Document2)
- Performance gates ≤ 3 мин на сценарий
- Прежде чем писать — спросить про Cypress vs Playwright (канон-naming-soglasovyvat)
- Зависимости + дорожная карта расшивки

E2E framework в desktop отсутствует; реальные spec'и пишутся после C28-28..C28-33 и согласования инфраструктуры.
2026-06-02 15:37:22 +00:00
coopops 9757f5ec6b [C28-33][@ant] docs: план интеграции Благороста + cleanup-чеклист — doc-scaffold до C28-29/31/32
Typecheck / desktop (pull_request) Successful in 13m36s
Typecheck / controller (pull_request) Successful in 13m22s
INTEGRATIONS.md в expenses/:
- BLAGOROST_POOL flow через существующий capital::createprogexp (никаких новых contract-actions)
- Каталог 9 универсальных событий шасси для Notification Center
- Backward-compat редиректы /programs/blagorost/expenses → /expenses?source=BLAGOROST_POOL
- Cleanup-чеклист legacy program-expense: на ветке пусто (re-grep ProgramExpense → 0 файлов; PR #59 не залит)
- Шаблоны 1010/1011 — не существуют, удалять нечего; роль закрывают 2010/2011 из C28-30
- Расшивка после закрытия C28-29/C28-31/C28-32

Не трогает рабочие сервисы (только новый .md в extensions/expenses/, не зарегистрировано в extensions-registry).
2026-06-02 15:35:55 +00:00
coopops 6e0fb185b6 [C28-32][@ant] feat: UI scaffold-stub expenses extension — 7 page stubs + dialog + install.ts на canon до C28-31
Typecheck / desktop (pull_request) Successful in 13m33s
Typecheck / controller (pull_request) Successful in 13m37s
2026-06-02 15:08:37 +00:00
coopops 3537c7d89b [C28-31][@ant] docs: scaffold-stub нового expenses extension — план файловой структуры до C28-29
Typecheck / desktop (pull_request) Successful in 13m44s
Typecheck / controller (pull_request) Successful in 13m20s
C28-31 (backend extension шасси расходов) логически блокируется C28-29 (контракт expenses) и C28-28 (P0 коды ledger2). Чтобы зонтичный PR не простаивал и следующий цикл шёл механически — кладём README с планируемой структурой файлов, MinIO-спекой бакета `expenses:files` и черновой GraphQL-схемой.

Когда расшивается C28-28 → C28-29 → этот README раскрывается в код по списку.
2026-06-02 14:47:15 +00:00
coopops 1fa41b2b39 [C28-30][@ant] feat: добавить 2 шаблона document2 для шасси расходов (2010/2011) — единый источник СЗ-сметы и протокола-1 для MVP-SINGLE
Typecheck / desktop (pull_request) Successful in 13m28s
Typecheck / controller (pull_request) Successful in 13m4s
Реализация Эпика 2 шасси системы расходов (волна 6 проекта 14).

cooptypes/cooperative/registry/:
- 2010.ExpenseProposalStatement — СЗ-смета с массивом items (description, amount,
  recipient_type SELF/MEMBER/ORG, mechanics ADVANCE/DIRECT, recipient_name, requisites),
  заголовком proposal (description/total_amount/items_count/source_wallet). LiquidJS
  context с таблицей позиций, переводы ru.
- 2011.ExpenseProposalDecision — протокол-1 утверждения/отказа председателем.
  Поля decision {kind: approve|decline, reason?, protocol_number?, protocol_date?},
  переиспользование IExpenseItem/IExpenseProposalHeader из 2010 через cross-import.

factory/src/:
- Schema/ExpenseItemSchema — JSONSchemaType для ExpenseItem, ExpenseProposalHeader,
  ExpenseProposalDecisionBody.
- Templates/2010+2011 — обёртки по образцу 1010 ExpenseStatement / 1011 ExpenseDecision,
  но с массивом items в Schema.
- Actions/2010+2011 — генераторы pdf через DocFactory с маппингом meta+coop+user+vars
  +proposal+items[+decision].

factory/test/documents-1000-plus.test.ts:
- 3 golden-pdf теста (2010 СЗ, 2011 approve, 2011 decline + reason).

Legacy 1010/1011 НЕ удалены — будут выпилены в C28-33 после миграции backend/UI
на новые ID (чтобы не порвать PR #55 на марафоне рефакторинга).

Решение 2026-06-02: только 2 шаблона document2 в MVP (2010, 2011); остальные слоты
(payment_proof, report_files, return_proof, протокол-2) — файлы в MinIO/expense_files
без factory-цепочки. Тяжёлая document2-цепочка нужна только для юридически-весомых
актов СЗ-смета + протокол совета; первичные документы внешнего происхождения и
events приёмки идут отдельным механизмом.

Refs: C28-30, PRD f8 раздел 8.16 + override 2026-06-02, образцы 1010.ExpenseStatement
+ 1011.ExpenseDecision.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 14:39:37 +00:00
coopops 2420d3173f [C28-26][@ant] chore: убрать упоминание Novu из комментария init-wallet
Typecheck / desktop (pull_request) Failing after 4m47s
Typecheck / controller (pull_request) Failing after 4m46s
Бэкенд больше не использует Novu — комментарий устарел.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:17:15 +00:00
coopops 53376971b7 [C28-27][@ant] feat: стол председателя — журнал уведомлений + переотправка
Typecheck / desktop (pull_request) Failing after 4m47s
Typecheck / controller (pull_request) Failing after 4m49s
Эпик 6.5: страница «Журнал уведомлений» (Chairman desktop, /notifications-journal).
Канон-таблица .table-wrap (дата/получатель/тип/канал/статус/попытки), FilterBar по
каналу и статусу, BaseBadge-статусы, кнопка «Переотправить» на строке (resend →
новая строка в очереди, перечитываем). TableSkeleton/EmptyState/«Загрузить ещё».
Имя типа уведомления — из каталога Workflows. Только канон-компоненты + токены.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:15:06 +00:00
coopops b500789dd6 [C28-27][@ant] feat: личный инбокс пайщика — колокол на нативном Центре
Typecheck / desktop (pull_request) Failing after 4m50s
Typecheck / controller (pull_request) Failing after 4m48s
Эпик 6.4: виджет NotificationCenter переписан с Novu UI на доменный канон-компонент
shared/ui/domain/NotificationCenter (колокол .icon-btn + BaseBadge-счётчик + q-menu).
Pinia-стор: лента инбокса/unreadCount/markRead/markAllRead через SDK, оптимистичные
отметки, поллинг непрочитанных 30с (live-подписки нет — опрос), тост на новое.
Категория группировки выводится из workflowId. Удалён @novu/js и NOVU_* из env.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:09:32 +00:00
coopops 813cc1aeff [C28-27][@ant] feat: SDK-операции уведомлений — журнал, инбокс, переотправка
Typecheck / desktop (pull_request) Failing after 6m26s
Typecheck / controller (pull_request) Failing after 6m30s
Zeus-селекторы (Notification/NotificationDetail/NotificationAttempt/InboxNotification/
UnreadNotificationsCount + пагинации) + queries (getNotifications/getNotification/
getInboxNotifications/getUnreadNotificationsCount) + mutations (resendNotification/
markNotificationRead/markAllNotificationsRead). Enabling-слой для FE 6.4/6.5.
Компилируются после generate-schema/generate-client оператора (zeus regen).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:02:50 +00:00
coopops f9543991e3 [C28-27][@ant] feat: read-side инбокса пайщика — лента/unreadCount/markRead
Typecheck / desktop (pull_request) Successful in 13m25s
Typecheck / controller (pull_request) Successful in 12m58s
Эпик 6.6 (backend-часть): личный инбокс канала In-app поверх notification_inbox.
getInboxNotifications (лента по сессии, пагинация канон), getUnreadNotificationsCount
(бейдж колокола), markNotificationRead/markAllNotificationsRead. Скоуп строго по
subscriber_id из JWT — пайщик видит только свой инбокс (ownership на markRead).
Live-подписка onNotification отложена: graphql-ws/PubSub в репо нет — фронт poll'ит.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:59:37 +00:00
coopops 96a8ecdf54 [C28-27][@ant] feat: бэкенд журнала уведомлений — getNotifications/getNotification/resend
Typecheck / desktop (pull_request) Successful in 13m25s
Typecheck / controller (pull_request) Successful in 13m0s
Read-side стола председателя (эпик 6, backend):
- getNotifications: журнал очереди notification_outbox с фильтрами
  (coopname/workflowId/channel/status/recipient) + пагинация (канон createPaginationResult).
- getNotification: детализация уведомления + история попыток из notification_deliveries.
- resendNotification: force-переотправка — новая строка очереди со свежим
  idempotencyKey (обходит дедуп), worker подхватывает.
- Статусы каналов/доставки — GraphQL enum через registerEnumType (канон).
- Гейтинг ролями: журнал/детализация — chairman+member, resend — chairman
  (RolesGuard+AuthRoles; платформенный CASL — Phase 2).

FE (инбокс/журнал) + codegen Zeus/SDK — за оператором (generate-schema/client
локально не запускать); live-подписка in-app — следующая история. tsc: 0 новых ошибок.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:50:00 +00:00
coopops 4bb9a317ca [C28-26][@ant] refactor: полное удаление Novu из контроллера
Typecheck / desktop (pull_request) Successful in 13m42s
Typecheck / controller (pull_request) Successful in 13m4s
Контроллер больше не зависит от внешнего Novu (ADR-001, DEC-025):
- Удалены infrastructure/novu/* (адаптеры + модуль), порты NovuWorkflow/
  NovuCredentials + старый notification.port (NotificationPort→NovuAdapter),
  NotificationDomainService, DeviceTokenService, webhook-приёмник Novu, novu.utils.
- subscriber_hash переведён на HMAC по SERVER_SECRET (utils/subscriber-hash.util),
  identity своя; subscriber_id (адресация Центра) сохранён — backfill cron оставлен
  без Novu-синхронизации.
- Развязаны AccountDomainService/account+participant-interactor/subscription-service
  от Novu-вызовов; NovuModule/NotificationDomainModule убраны из 7+5 модулей.
- config.novu + NOVU_* env + @novu/api удалены; lockfile обновлён (desktop @novu/js
  снимется синхронно с эпиком 6).
- @coopenomics/notifications: удалён sync/ (внешняя синхронизация), bin/scripts/deps;
  остался каталог типов + шаблоны; пакет пересобран.
- grep -i novu по controller/src пуст; tsc = baseline (0 новых ошибок).

Boot/grep-ERROR верификация — за оператором (поднять контроллер нельзя из фоновой сессии).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:40:15 +00:00
coopops 0afb73a506 [C28-25][@ant] feat: миграция consumer-сервисов с Novu на единый порт уведомлений
Typecheck / desktop (pull_request) Successful in 13m32s
Typecheck / controller (pull_request) Successful in 13m3s
Drop-in переключение ~28 call-sites в 15 сервисах с
NOVU_WORKFLOW_PORT.triggerWorkflow на NOTIFICATION_PORT.notify (Центр
уведомлений DC v3). Listener'ы/события не тронуты — меняется только тело
вызова: name→workflowId, coopname обязателен (action.coopname/config.coopname),
recipient дополнен username (адресация in-app/web-push).

NotificationCenterModule помечен @Global — порт cross-cutting, консьюмеры
инжектят без импорта модуля (как NOTIFICATION_SUBSCRIPTION_PORT).
Cross-plugin InterCoopCalendarEventNotificationPort (chatcoop) сохранён,
мигрировано лишь тело. Из application NotificationSenderService убраны мёртвые
Novu-измы (broadcast/cancel/getStatus) — не использовались.

tsc: 0 новых ошибок (17 = baseline). NOVU_WORKFLOW_PORT-биндинг остаётся
мёртвым до удаления в эпике 5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:08:06 +00:00
coopops 5b3a28d56c [C28-24][@ant] feat: outbox-worker — надёжная доставка с retry и статусами
Typecheck / desktop (pull_request) Successful in 13m42s
Typecheck / controller (pull_request) Successful in 13m14s
OutboxWorkerService @Interval(3000): поллит notification_outbox по индексу
(status, scheduledAt), шлёт через порт канала, исход пишет в журнал
notification_deliveries. at-least-once: backoff-ретрай (5s→30s→2m→10m→1h,
maxAttempts), после исчерпания — терминальный failed (переотправка на столе
председателя). Реклейм зависших SENDING после рестарта. Строка = один канал →
независимые попытки по каналам. Статусы — enum. ScheduleModule.forRoot()
подключён в app.module. Параметры worker'а из env с дефолтами.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:51:03 +00:00
coopops 0e98ae5538 [C28-23][@ant] feat: канал in-app — персист уведомлений в инбокс кооператива
Typecheck / desktop (pull_request) Successful in 13m24s
Typecheck / controller (pull_request) Successful in 13m18s
InAppChannelAdapter за InAppChannelPort: доставка in-app = запись
отрендеренного из каталога уведомления в notification_inbox. Новая
сущность notification_inbox (isRead/readAt, индексы под ленту + unreadCount).
ChannelMessage расширен outboxId + actorSubscriberId (worker даёт из outbox).
Live-подписка/unreadCount/mark-read — read-side инбокса, в эпик 6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:46:23 +00:00
coopops 8e6e45eba5 [C28-23][@ant] feat: каналы email + web-push + шаблонизатор
Typecheck / desktop (pull_request) Successful in 13m37s
Typecheck / controller (pull_request) Successful in 13m16s
EmailChannelAdapter (nodemailer, SMTP из config.email; disabled при пустом
SMTP_HOST, наружу не стучится — NFR11). WebPushChannelAdapter: контроллер
доставляет сам своим подписчикам напрямую через WebPushService на endpoint'ы
из web_push_subscriptions по username (фикс пушей — точки доставки, не ключи);
410 → деактивация подписки. Шаблоны рендерятся из каталога @coopenomics/
notifications (controlValues.subject/body), mustache-интерполяция {{payload.x}}
без liquidjs (лишняя зависимость для MVP). Порты EMAIL_CHANNEL_PORT/
WEB_PUSH_CHANNEL_PORT в NotificationCenterModule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:32:39 +00:00
coopops c783ce322b [C28-22][@ant] feat: порты каналов доставки — Email/InApp/WebPush
Typecheck / desktop (pull_request) Successful in 14m44s
Typecheck / controller (pull_request) Successful in 13m20s
Контракты EmailChannelPort/InAppChannelPort/WebPushChannelPort + ChannelMessage/
ChannelDeliveryResult + DI-токены. Только интерфейсы — реализации в эпике 2;
worker эпика 3 зависит от абстракций, не от провайдеров. Имена без I-префикса
(канон портов репо).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:23:40 +00:00
coopops 38187f3f26 [C28-22][@ant] feat: роутер NotificationService + идемпотентность — запись в outbox
NotificationService implements NotificationPort: резолвит каналы по типу из
каталога @coopenomics/notifications (steps[].type, только email/in_app/push),
для каждой пары получатель×канал пишет строку в notification_outbox идемпотентно
(ON CONFLICT DO NOTHING по unique idempotencyKey). Опциональный EntityManager —
запись в транзакции вызывающего (story 1.3). idempotencyKey = sha256(coopname|
workflowId|subscriberId|channel|stable(payload)) с детерминированной сериализацией
payload (story 1.5). email-канал — только при подтверждённом email (gate).
Модуль зарегистрирован в app.module, экспортирует NOTIFICATION_PORT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:22:53 +00:00
coopops 7d26237748 [C28-22][@ant] feat: схема БД Центра уведомлений — outbox + журнал доставок
Сущности notification_outbox (очередь recipient×channel, hot-path индекс
(status, scheduledAt), unique idempotency_key) и notification_deliveries
(append-журнал попыток, индексы по coopname/recipient для стола председателя
и личного инбокса). Статусы — enum, не строки. Фундамент для роутера (1.3),
worker'а (эпик 3) и стола председателя (эпик 6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:14:42 +00:00
coopops 409a19f6ea [C28-22][@ant] feat: ввести доменный порт NotificationPort.notify — единый вход уведомлений без привязки к Novu
Порт notify(NotifyInput) + типы входа (coopname обязателен как federation-инвариант,
workflowId ссылается на каталог @coopenomics/notifications, NotificationChannel enum
email/in_app/push). Старый NovuWorkflowPort сохраняется до миграции call-sites (эпик 4),
удаляется в эпике 5. Часть истории 1.1 эпика 1 (C28-22).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:48:22 +00:00
ant 6866717f98 Merge pull request 'feat(soviet): утверждение решений совета через бэкенд (coopname-auth)' (#51) from feat/decision-authorize-backend into dev
Reviewed-on: #51
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-06-02 07:59:09 +00:00
coopops 7a4d2e0cb3 [C28-21][@ant] fix: реестр совета показывает все документы (submitted+resolved)
Typecheck / desktop (pull_request) Successful in 13m36s
Typecheck / controller (pull_request) Successful in 13m33s
Дефолт реестра совета был newresolved → status=Resolved → виден только
утверждённый документооборот, submitted-документы (в т.ч. свежеподанные
заявления) пропадали. Переключатель вкладок на странице скрыт
(showFilter=false), сменить вид было нельзя. Выравниваем с реестром пайщика:
newsubmitted → status не ограничивается → совет видит ВСЕ документы кооператива.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 18:52:57 +00:00
coopops f7b12bda60 [C28-21][@ant] fix: пересборка агрегата не откатывает статус (resolved→submitted)
Typecheck / desktop (pull_request) Successful in 14m24s
Typecheck / controller (pull_request) Successful in 13m28s
freedecision эмитит newresolved и newdecision по одному package в одной
транзакции — оба @OnEvent-хендлера срабатывают одновременно. safeReassemble
(newdecision/newact/newlink) пере-upsert'ил все строки пакета ПОЛНЫМ набором
колонок со сталым row.status, прочитанным до коммита newresolved → затирал
resolved обратно в submitted. Решение совета застревало в submitted и пропадало
из реестра совета (дефолт «Только утверждённые» = status=Resolved).

Пересборка теперь обновляет ТОЛЬКО агрегат-производные колонки
(document_aggregate/full_title/content_text/signers_text) точечным UPDATE.
status/block_num/source_action_data принадлежат статус-событиям и пересборкой
не трогаются → конкурентный newresolved не откатывается. Новый метод
SignedDocumentRepository.updateAggregate + SignedDocumentAggregateUpdate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 18:32:42 +00:00
coopops e810644386 fix(desktop): открывать первую страницу стола при выборе из command palette
Предыдущий фикс снимал лоадер, но onSelectWorkspace пушил на mainRoute.name —
родительский layout-роут без компонента → серый экран. WorkspaceSwitcher
работает потому что зовёт goToDefaultPage(), который через getDefaultPageRoute()
резолвит реальную лист-страницу (authorized_default_route / defaultRoute /
первый child) И сбрасывает isWorkspaceChanging. Используем его же.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:29:15 +00:00
coopops fec4058ff6 feat(soviet): утверждение решений совета через бэкенд (coopname-auth) вместо client-side push
Typecheck / desktop (pull_request) Successful in 13m54s
Typecheck / controller (pull_request) Successful in 13m7s
Кнопка «Утвердить» совета слала authorize+exec напрямую из браузера ключом
председателя — при HTTP 504 от ноды ошибка терялась в браузере, бэкенд её не
видел и не логировал. Переводим действие на проведение через контроллер ключом
кооператива, как остальные soviet-мутации (sendAgreement) — теперь ошибки связи
с блокчейном попадают в логи бэкенда.

Контракт soviet: authorize/exec require_auth(chairman/executer) → require_auth(coopname).
Согласие председателя не теряется — закреплено его личной подписью на document2
(verify_document_or_fail) + проверкой is_valid_chairman.

- contracts: authorize.cpp/exec.cpp — require_auth(coopname)
- controller: новая мутация authorizeDecision (DecisionAuthorizeModule) + порт/адаптер,
  пушит authorize+exec одной транзакцией ключом кооператива из vault
- cooptypes: authorizations actor chairman/username → coopname
- sdk: Mutations.Decisions.AuthorizeDecision + регенерация schema.gql/zeus
- desktop: фича AuthorizeAndExecDecision и process-decisions зовут бэкенд-мутацию
  вместо useGlobalStore().transact; подпись документа остаётся клиентской

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 09:26:48 +00:00
coopops 264d592033 fix(desktop): снимать full-page лоадер при переключении стола из command palette
selectWorkspace() ставит isWorkspaceChanging=true (full-page WindowLoader
поверх router-view), а сбрасывал флаг только goToDefaultPage(). Обычное меню
(WorkspaceSwitcher) зовёт goToDefaultPage и лоадер снимается; command palette
(onSelectWorkspace/onSelectPage) делал прямой router.push без сброса флага —
лоадер висел вечно. Тот же leak в ConnectionAgreementStepper.goToQuickClient.

Снимаем флаг через setWorkspaceChanging(false) в .finally() после навигации.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:14:41 +00:00
coopops 7d43ef9668 fix(desktop): выделение текста — solid accent вместо болотного teal
Глобальный ::selection был --p-primary-soft (teal 10% alpha) — болотный,
почти невидим. Меняем на solid --p-accent + белый текст: видно на любом фоне.
Тост-специфичные ::selection-override (.q-notification/.toast) убраны —
accent читаем и на тёмной поверхности тоста, выделение единое везде.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 07:06:02 +00:00
ant bc989f47d1 Merge pull request '[C28-21] Реестр подписанных документов в Postgres: getDocuments+searchDocuments на PG, newdeclined, авто-backfill' (#47) from feat/C28-21-pg-signed-documents into dev
Reviewed-on: #47
2026-05-31 06:21:18 +00:00
coopops 6e388e3196 [C28-21][@ant] fix: icon в IWorkspaceRouteMeta опционален для hidden-роутов
Typecheck / desktop (pull_request) Successful in 13m52s
Typecheck / controller (pull_request) Successful in 13m12s
Страницы deep-link (карточка документа, name=document-details/
user-document-details) помечены meta.hidden=true и в навигацию не
попадают — иконка им не нужна. Drawer рендерит q-icon под v-if='item.icon',
undefined безопасен. Убирает TS2741 'Property icon is missing'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 05:37:11 +00:00
coopops 42d3fc46e0 [C28-21][@ant] fix: открытие документа из карточки по hash (подписанный), а не doc_hash
Typecheck / desktop (pull_request) Failing after 8m20s
Typecheck / controller (pull_request) Successful in 12m31s
Карточка реестра вела на страницу документа, передавая rawDocument.hash — а это
doc_hash (хеш самого документа, без подписи). Бэкенд же ищет документ по колонке
hash (подписанный документ = содержимое + подпись): UPPER(d.hash)=UPPER(:hash).
Значения разные → getDocuments возвращал 0 → «Документ не найден».

Из поиска работало, потому что searchDocuments отдаёт именно hash (колонку), а не
doc_hash. Карточка теперь берёт тот же document.hash (= колонка hash в реестре).

Проверено по БД: фильтр по doc_hash → 0 строк, по document.hash → 1; у всех
записей document.hash == колонка hash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 16:17:30 +00:00
coopops 716eb4fe5a [C28-21][@ant] feat: реестр документов на канон-карточки + липкая кнопка «назад»
Typecheck / desktop (pull_request) Failing after 8m31s
Typecheck / controller (pull_request) Successful in 12m49s
Реестр документов переведён с таблицы на канон плоские строки (DocumentRow):
строка кликабельна и ведёт на отдельную страницу документа. Таблица с
инлайн-раскрытием оставлена только для Union-страницы (expand=true).

- DocumentCardsList: список DocumentRow (заголовок, дата, ФИО подписантов,
  скачивание в actions), скелетоны при загрузке, «Загрузить ещё». Статус-чип
  не показываем — в реестре всё подписано (канон stop-signal).
- ListOfDocumentsWidget: проп expand — карточки по умолчанию (совет/пайщик),
  таблица для Union (передаёт expand=true) — поведение Union не меняется.
- useDocumentNavigation перенесён из фичи поиска в entity Document
  (переиспользуют и поиск, и карточный список — чистые слои).
- DocumentDetailsPage: кнопка «назад» теперь в липком баре (sticky,
  top=--p-topbar-h), видна всегда при прокрутке документа.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 16:04:31 +00:00
coopops 7f65478dc5 [C28-21][@ant] fix: раздел документа выводить из имени роута, не из workspace
Typecheck / desktop (pull_request) Failing after 8m32s
Typecheck / controller (pull_request) Successful in 12m33s
activeWorkspaceName инициализируется асинхронно — при холодном переходе по
прямой ссылке на документ совета скоуп мог упасть в скоуп пайщика и документ
«не находился». Имя роута (document-details / user-document-details) доступно
сразу и детерминированно.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:41:23 +00:00
coopops 0cbc76b018 [C28-21][@ant] feat: отдельная canon-страница документа вместо фильтра реестра
Typecheck / desktop (pull_request) Failing after 8m24s
Typecheck / controller (pull_request) Successful in 12m42s
Прошлый подход (фильтр реестра по ?document=<hash> + баннер) отклонён —
нужна полноценная отдельная страница документа с переходом назад и deep-link.

- DocumentDetailsPage: canon-страница по эталону карточки собрания (back-link
  под шапкой, имя документа в заголовок через setPageTitleOverride, скелетон,
  EmptyState; контент — ComplexDocument). Грузит документ сама по hash из роута,
  поэтому открывается и прямым переходом по ссылке.
- Роуты documents/:hash в обоих расширениях: document-details (совет) и
  user-document-details (пайщик), hidden — по аналогии с meet-details.
- DocumentModel.loadDocument(username, hash): точечная загрузка одного документа
  (статус-агностичный фильтр newsubmitted), не трогает состояние списка.
- Клик в поиске ведёт на страницу документа (useDocumentNavigation выбирает роут
  по рабочему столу), а не фильтрует реестр.
- Откат фильтр-подхода: удалены SearchedDocumentBanner и useDocumentRouteFilter,
  страницы реестра/виджет/таблица возвращены к baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:39:48 +00:00
coopops f64a430bff [C28-21][@ant] feat: открытие документа из поиска через фильтр реестра
Typecheck / desktop (pull_request) Successful in 12m55s
Typecheck / controller (pull_request) Successful in 12m31s
Клик по результату поиска вёл в никуда (openDocument был пуст) — найденный
документ нельзя было посмотреть. Теперь клик кладёт hash в ?document=<hash>
текущего роута; страница реестра читает его и сводит список к одному документу,
авто-раскрывая его. Переиспользуем готовый backend-фильтр filter.document.hash
(тот же путь, что у Union-страницы) — без изменений API и codegen.

- useDocumentRouteFilter/useDocumentNavigation: чтение и установка ?document.
- SearchedDocumentBanner: баннер «показан один документ» с возвратом к списку.
- Обе страницы реестра (совет + пайщик) фильтруют виджет по выбранному документу.
- Виджет реагирует на смену filter и форсит статус-агностичный newsubmitted при
  hash-фильтре (иначе на вкладке «только утверждённые» неутверждённый документ
  из поиска не нашёлся бы).
- DocumentsTable авто-раскрывает строку по expandHash (регистронезависимо).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:06:38 +00:00
coopops 9d183f3b64 [C28-21][@ant] copy: срок рассмотрения заявления — до 30 дней вместо 24 часов
Typecheck / desktop (pull_request) Successful in 12m54s
Typecheck / controller (pull_request) Successful in 12m32s
В экране ожидания регистрации (WaitingRegistration) «до 24 часов» — неактуально:
реальный регламентный срок рассмотрения советом — до 30 дней (обычно решение
принимается за день-два). Поправили текст под фактический срок.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 14:34:54 +00:00
coopops e877bd3eac [C28-21][@ant] style: убрать верхний отступ в выдаче поиска документов
Typecheck / desktop (pull_request) Successful in 13m3s
Typecheck / controller (pull_request) Successful in 12m36s
Большая пустая полоса между строкой поиска и первым результатом мешала —
снят верхний padding секции результатов (q-pt-none), список начинается ближе.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 12:52:52 +00:00
coopops 9427c204ab [C28-21][@ant] feat: выдача поиска документов — ФИО подписанта и чистый заголовок
Typecheck / desktop (pull_request) Successful in 12m57s
Typecheck / controller (pull_request) Successful in 12m36s
Поиск показывал username («ant») и сырой full_title с хвостом ` - - дата.pdf`.
Приводим к виду реестра на главной:
- SearchResult.signer — ФИО подписанта-субъекта («Иванов Иван Иванович»), берётся
  из signer_certificate готового агрегата (подпись с signer===username, иначе первая;
  организация → short_name). Тот же источник, что у чипов подписей в реестре.
- full_title в выдаче поиска теперь = meta.title (чистое наименование), как в реестре;
  fallback — прежний full_title.
- Фронт DocumentSearchDialog: заголовок + чип ФИО (BaseBadge) + дата вместо строки
  «username · дата»; убран пустой highlights-блок.

Схема + zeus-клиент перегенерированы (generate-schema/generate-client + sdk build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 12:30:54 +00:00
coopops cdb9cc8344 [C28-21][@ant] fix: парсинг meta.created_at формата DD.MM.YYYY HH:mm + устойчивый backfill
Typecheck / desktop (pull_request) Successful in 13m13s
Typecheck / controller (pull_request) Successful in 12m41s
Документы хранят дату в локализованном формате `DD.MM.YYYY HH:mm` + отдельное
meta.timezone (так пишет фабрика @coopenomics/factory через moment.tz). Прежний
`new Date(meta.created_at)` этот формат не парсил → Invalid Date → колонка
timestamptz падала с "invalid input syntax ... 0NaN-NaN-NaN", и throw в одной
записи ронял весь прогон backfill. Парсим тем же moment.tz, что и фабрика
(timezone из меты, иначе config.timezone), невалидное → null. Плюс per-record
try/catch в backfill: одна битая запись логируется и пропускается, а не валит прогон.

Проверено вживую на voskhod: backfill завершён scanned=8 created=4 updated=4
skipped=0, document_created_at = 2026-05-28 07:53:00+00 (валидные даты, не null).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 12:07:29 +00:00
coopops c5272c7088 [C28-21][@ant] fix: @Index по имени свойства packageHash, не db-колонки package
Typecheck / desktop (pull_request) Successful in 12m56s
Typecheck / controller (pull_request) Successful in 12m36s
TypeORM @Index(['...']) принимает имена СВОЙСТВ сущности, а не db-колонок.
Колонка объявлена @Column({ name: 'package' }) packageHash, поэтому индекс
['coopname','package'] падал на старте: "Index contains column that is missing
in the entity (SignedDocumentEntity): package". Индекс по db-колонке `package`
строится корректно и через имя свойства packageHash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 11:52:32 +00:00
coopops 6490aa81f4 chore(release): publish
Build bootstrap container / build (push) Successful in 3m10s
Release / release (push) Successful in 25m9s
Release / publish-packages (push) Successful in 14m40s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / trigger-mono-docs (push) Successful in 1s
2026-05-30 07:56:36 +00:00
coopops 7b58187553 Merge branch 'testnet' 2026-05-30 07:56:22 +00:00
coopops b1c2f3d558 fix gitgnore
Build bootstrap container / build (push) Successful in 1m21s
2026-05-30 07:22:36 +00:00
coopops 6f53923303 chore(release): publish
Build bootstrap container / build (push) Successful in 3m7s
Release / release (push) Successful in 24m25s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-05-30 06:58:26 +00:00
coopops 4804909569 Merge branch 'testnet'
# Conflicts:
#	components/blago-cli/package.json
#	components/boot/package.json
#	components/cleos/package.json
#	components/contracts/package.json
#	components/controller/package-lock.json
#	components/controller/package.json
#	components/cooptypes/package.json
#	components/desktop/package.json
#	components/docs-harness/package-lock.json
#	components/docs-harness/package.json
#	components/docs/package.json
#	components/factory/package.json
#	components/inter/package.json
#	components/migrator/package.json
#	components/notifications/package.json
#	components/parser/package-lock.json
#	components/parser/package.json
#	components/sdk/package.json
#	components/setup/package.json
#	lerna.json
2026-05-30 06:55:07 +00:00
coopops 33b7923773 chore(release): publish
Build bootstrap container / build (push) Successful in 3m37s
Release / release (push) Successful in 25m15s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-05-30 06:48:30 +00:00
coopops 10d792613f Merge branch 'dev' into testnet 2026-05-30 06:48:13 +00:00
coopops 58bdb8a995 fix(desktop): форматировать суммы в тостах ошибок до 2 знаков
Ошибки цепи возвращают суммы с сырой precision=4 ("100.0000 RUB"). Добавлен formatAssetsInText() рядом с formatAsset2Digits и применён в FailAlert — единая точка для всех тостов ошибок. Суммы вида <целое>.<>=3 знаков> <ТИКЕР> приводятся к 2 знакам с группировкой; уже отформатированные не трогаются (идемпотентно).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops 067fd8d562 fix(desktop): не вешать watch темы на SSR-сервере (утечка подписчиков на Dark)
theme boot подписывал watch на глобальный Quasar-реактив Dark. В SSR boot-файлы
выполняются на каждый рендер-запрос, а watch без owning-scope не утилизируется —
подписчики копились на синглтоне Dark безгранично (та же природа, что у sentry.ts,
только дешевле за итерацию). Тема (data-theme на documentElement) нужна только на
клиенте; гейтим boot через process.env.SERVER.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops ca2b97f18f fix(desktop): не инициализировать Sentry на SSR-сервере (спам логов + утечка RAM)
Boot-файл sentry.ts использует браузерный @sentry/vue с browserTracingIntegration.
В SSR boot-файлы выполняются на каждый рендер-запрос, поэтому Sentry.init плодил
новый клиент/интеграции на каждом запросе (утечка памяти → «пила» RAM, заметная
на нодах с малым объёмом RAM) и спамил серверный лог строкой про инициализацию
(~80/мин). Гейтим boot через process.env.SERVER (как в chatwoot.ts) — Sentry
остаётся только на клиенте, где ему и место.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops 708607f83a fix(desktop): цвет выделения текста в тостах
Глобальный ::selection красит текст в var(--p-ink) (тёмный на свет-теме)
поверх полупрозрачного фона — на тёмной поверхности тоста выделенный текст
пропадал. Scoped-override для .q-notification/.toast: полупрозрачная светлая
подложка + светлый текст, читаемо на всех типах тостов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops 5a21d017e0 fix(desktop): убрать гонку Teleport-host шапки и обновлять гранты/столы после регистрации
Баг A: предупреждение "Failed to locate Teleport target '#header-actions-host'"
возникало, потому что host был под v-if='loggedIn'. Сразу после регистрации
навигация на стол происходит раньше, чем loggedIn (зависит от async
session.isAuth / isRegistrationComplete) станет true → цель Teleport
отсутствует в DOM → даже defer-Teleport источников страниц её не находит.
Рендерим #header-actions-host безусловно — видимость блока действий уже
регулируется CSS (.topbar__actions:has(> .header-actions-host:only-child:empty)).

Баг B: после регистрации grant-gated кнопки («Совершить взнос», «Получить
возврат») не появлялись до F5. В потоке завершения регистрации (watch на
participant_account) не вызывался desktops.loadDesktop() (свежие столы/гранты),
а кошелёк/agreements не грузились: на момент первого run() статус ещё не
'active' → isFullyActive=false. После того как статус обновился, до-вызываем
run(true) и loadDesktop() по образцу init-app / EnableButton. Условие
isFullyActive в init-wallet не ослаблялось.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops a2bfa69c2b feat(desktop): placeholder для наименования банка в форме ИП
Только неочевидное поле — наименование банка получило подсказку ПАО "Сбербанк".
Остальные поля ИП самоописательны (формат даты, кол-во цифр, «как в паспорте»).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops 804a178234 feat(desktop): добавить placeholder-примеры в форму организации при вступлении
Поля краткого/полного наименования, должности представителя и наименования
банка получили placeholder-подсказки (ПК "Ромашка", Председатель совета,
ПАО "Сбербанк") — показываются при фокусе пустого поля.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops 0eb1224390 fix(desktop): не терять выбор в BaseRadioCard при наведении
Hover-правило .base-radio-card:hover (специфичность 0,2,0) перекрывало
.base-radio-card--selected (0,1,0), поэтому при наведении на выбранную
карточку программы primary-фон/граница подменялись на surface/soft и
выбор визуально пропадал. Ограничил hover невыбранными карточками через
:not(.base-radio-card--selected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops b465529f8a [design-wave1][@dark] fix: палитра команд — рамка курсора на любом столе, не только активном
Заголовок workspace-row получал outline только когда стол активный
(.is-active .row.is-selected). На неактивных столах при навигации курсор
«терялся»: только лёгкая смена фона на surface-2, без рамки — в отличие
от page'ей, которые подсвечиваются outline'ом всегда. Выровнял правило:
outline на любом .workspace-row.is-selected.
2026-05-30 06:47:31 +00:00
coopops 6e4ee9303a [design-wave1][@dark] fix: палитра команд — clamp навигации стрелками вместо wrap-around
На верхней/нижней позиции стрелка дальше прыгала в противоположный
конец списка из-за (idx+delta+len)%len. Заменено на clamp в [0..len-1]:
курсор остаётся на месте, как в нативном listbox/меню. Пустое выделение
+ ↓ ставит на первый, ↑ — на последний.
2026-05-30 06:47:31 +00:00
coopops c3cc6bffb6 [design-wave1][@dark] fix: палитра команд — снят sticky-баннер активного стола
Активный workspace был position:sticky top:0 — при навигации стрелкой
вверх курсор-выделение «исчезал» за sticky-overlay'ем, потому что
scrollIntoView({block:'nearest'}) считал элемент видимым в normal flow,
не учитывая sticky-перекрытие. Активный стол и так идёт первым по
сортировке + есть badge «Активный», sticky не нужен.
2026-05-30 06:47:31 +00:00
coopops 39be17dfbc [@dark] fix(desktop): CommandPalette — primary вместо accent + meta.conditions/hidden + drawer close на мобильниках
- CommandPalette.vue: все --p-accent / --p-accent-soft / --p-ink-on-accent
  заменены на --p-primary (бирюзовая палитра канона). Откатывается
  регрессия после fix-коммитов design-wave1 (sticky-banner / clamp /
  рамка курсора), которые случайно вернули accent поверх primary
  (см. b82b778b81 — изначально palette был в primary).

- default.vue::paletteWorkspaces: возвращена фильтрация meta.conditions
  (participant/chairman/soviet install.ts) + meta.hidden (capital
  install.ts) — та же логика что в LeftDrawerMenu.filteredRoutes,
  чтобы CommandPalette не показывал страницы, которые не отображаются
  в rail-меню. onSelectWorkspace/onSelectPage закрывают левый дровер
  на мобильниках через desktop.closeLeftDrawerOnMobile().
2026-05-30 06:47:31 +00:00
coopops ab8f1186e8 [@dark] refactor(desktop): canon CommandPalette + снос осиротевшего меню — фикс утерянного ⌘K после design-wave1
- layouts/default.vue: вместо widgets/Desktop/CmdkMenu mount'им
  shared/ui/domain/CommandPalette (canon из design-wave1) с адаптером
  workspaceMenus → CommandPaletteWorkspace[] и глобальным ⌘K / Ctrl+K.
- entities/CommandPalette: минимальный store (isOpen / open / close /
  toggle) — заменяет тяжёлый useCmdkMenuStore.
- LeftDrawerMenu.onCmdk → palette.open() (триггер «Найти» в AppDrawer).
- Удалены целиком осиротевшие после design-wave1 директории:
  entities/CmdkMenu, widgets/Desktop/CmdkMenu, widgets/Desktop/CmdkTrigger,
  widgets/Desktop/SecondLevelMenuList (никем не использовался),
  widgets/Desktop/WorkspaceMenu (заменён на WorkspaceSwitcher),
  widgets/Wallet/MicroWallet (заменён на RailUserCard).
- Поправлены barrels widgets/Desktop и widgets/Wallet.
2026-05-30 06:47:31 +00:00
coopops 7b39ed34be fix(desktop): читаемость тостов на тёмной теме
Фон и текст тоста (.q-notification + .toast) теперь фиксированные тёмная
поверхность + светлый текст в обеих темах, а не инвертируемые
var(--p-ink)/var(--p-canvas). На dark-теме текст уходил в тёмный на тёмном
deep-tint фоне (#052e16 и т.п.) — нечитаемо. Цветные иконки по типу
остаются на токенах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:31 +00:00
ant e68475e1d8 fix(desktop): отступ документа от рамки в диалоге подписи соглашения
q-pa-lg на flat-карточке SignAgreementDialog — документ больше не прижат
к границе на странице кошелька пайщика.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:31 +00:00
coopops a076962058 [blago-cli-add-filter][@ant] fix(blago-cli): фильтр non-entity .md в add + понятные ошибки push — чтобы blago add . не валил push «type: undefined»
— format/peekBlagoEntityType: не бросает; возвращает project|issue|story|undefined.
— sync/add: классификация unreadable/non-entity/clean/dirty; README/notes/CLAUDE/AGENTS не попадают в staging, отдельный счётчик skippedNonEntity.
— sync/push: на старте чистит staging от non-entity (warn); ошибки parseBlagoMarkdown / validateParsedForPush префиксуются путём файла.
— cmd/run-cli: вывод add показывает skippedNonEntity.
— bump 2026.5.28-1.
2026-05-30 06:47:31 +00:00
coopops 7e98a3adfb [mobile-drawer][@ant] fix: левый дровер — автозакрытие на мобиле при смене маршрута 2026-05-30 06:46:00 +00:00
coopops eb3e388b78 [fix-vue-tsc][@dark] fix: IPayment override → локальный IPaymentRow в виджете
Глобальный Omit<IPayment,'id'>&{id:string} ломал store.ts/mergePayments
и features/Payment/SetStatus/api (туда приходит сырой Zeus-вывод с
id:unknown). Откатываю override в entities/Payment/model/types.ts,
сужаю id до string локально в ListOfPaymentsWidget через IPaymentRow —
точка сужения только в UI, контракт стора/API не трогается.

vue-tsc desktop локально: PASS.
2026-05-30 06:46:00 +00:00
coopops d65643065e [fix-vue-tsc][@dark] fix: vue-tsc desktop — IAgenda в QuestionsTable + IPayment.id:string
QuestionsTable принимал Cooperative.Document.IComplexAgenda[] (cooptypes,
table:IDecision без certificates, id:IUint64=number|string), а сверху
ListOfAgendaQuestions передаёт IAgenda[] из desktop/entities/Agenda
(GraphQL-вывод getAgenda, table:BlockchainDecision с certificates,
id:number). Перевожу QuestionsTable на IAgenda — это реальная форма
данных из стора; уходит расхождение IComplexAgenda↔IAgenda для
:agenda='row' в QuestionCard (он тоже IAgenda).

IPayment.id был ModelTypes['ID']=unknown (Zeus не имеет scalar-resolver
для ID), из-за чего шаблон ListOfPaymentsWidget падал на expanded.get/.set
и :id-биндингах. Переопределяю id как string через Omit+&.
2026-05-30 06:46:00 +00:00
ant 0c343f0d50 Merge pull request 'fix(desktop): форматировать суммы в тостах ошибок до 2 знаков' (#48) from fix/desktop-format-asset-errors into dev
Reviewed-on: #48
2026-05-30 06:43:25 +00:00
coopops 91f74c76b4 fix(desktop): форматировать суммы в тостах ошибок до 2 знаков
Typecheck / desktop (pull_request) Successful in 14m11s
Typecheck / controller (pull_request) Successful in 13m38s
Ошибки цепи возвращают суммы с сырой precision=4 ("100.0000 RUB"). Добавлен formatAssetsInText() рядом с formatAsset2Digits и применён в FailAlert — единая точка для всех тостов ошибок. Суммы вида <целое>.<>=3 знаков> <ТИКЕР> приводятся к 2 знакам с группировкой; уже отформатированные не трогаются (идемпотентно).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:38:57 +00:00
coopops 3691d95777 fix(desktop): не вешать watch темы на SSR-сервере (утечка подписчиков на Dark)
theme boot подписывал watch на глобальный Quasar-реактив Dark. В SSR boot-файлы
выполняются на каждый рендер-запрос, а watch без owning-scope не утилизируется —
подписчики копились на синглтоне Dark безгранично (та же природа, что у sentry.ts,
только дешевле за итерацию). Тема (data-theme на documentElement) нужна только на
клиенте; гейтим boot через process.env.SERVER.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:25:10 +00:00
coopops f264aa85a3 fix(desktop): не инициализировать Sentry на SSR-сервере (спам логов + утечка RAM)
Boot-файл sentry.ts использует браузерный @sentry/vue с browserTracingIntegration.
В SSR boot-файлы выполняются на каждый рендер-запрос, поэтому Sentry.init плодил
новый клиент/интеграции на каждом запросе (утечка памяти → «пила» RAM, заметная
на нодах с малым объёмом RAM) и спамил серверный лог строкой про инициализацию
(~80/мин). Гейтим boot через process.env.SERVER (как в chatwoot.ts) — Sentry
остаётся только на клиенте, где ему и место.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:19:11 +00:00
coopops 3a59be5101 fix(desktop): цвет выделения текста в тостах
Глобальный ::selection красит текст в var(--p-ink) (тёмный на свет-теме)
поверх полупрозрачного фона — на тёмной поверхности тоста выделенный текст
пропадал. Scoped-override для .q-notification/.toast: полупрозрачная светлая
подложка + светлый текст, читаемо на всех типах тостов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 05:39:47 +00:00
coopops 43d9363800 [C28-21][@ant] docs: backfill идемпотентен по (coopname, doc_hash), не package
Typecheck / desktop (pull_request) Successful in 13m31s
Typecheck / controller (pull_request) Successful in 13m7s
Устаревшая формулировка в docstring backfill-сервиса: после перехода на ключ
doc_hash идемпотентность/уникальность реестра — по (coopname, doc_hash), а НЕ по
(coopname, package). package сознательно не уникален (один процесс = несколько
разных документов с разными подписантами), поэтому ключом записи он быть не может.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:15:16 +00:00
coopops 33437c6b35 [C28-21][@ant] fix: block_num реестра = блок on-chain действия, не генерации
Typecheck / desktop (pull_request) Successful in 13m36s
Typecheck / controller (pull_request) Successful in 13m5s
Ось упорядочивания версий подписи одного doc_hash должна мерить одну величину
на обеих сторонах guard'ов ingestAction. Раньше block_num хранился через
resolveBlockNum(meta.block_num, action.block_num) с приоритетом meta.block_num
(блок ГЕНЕРАЦИИ документа фабрикой), а incomingBlock в guard'ах — action.block_num
(блок ДЕЙСТВИЯ в цепи). Так как документ всегда генерируется до подачи в цепь
(meta.block_num <= action.block_num), защита от отката на старую версию подписи
(incomingBlock < existing.blockNum) и идемпотентность по блоку фактически не
срабатывали. Теперь приоритет — action.block_num, meta.block_num лишь fallback.

Заодно выправлены устаревшие комментарии интерфейса репозитория (ключ — doc_hash,
а не hash).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:07:47 +00:00
coopops 423648d342 [C28-21][@ant] fix(controller): ключ реестра — doc_hash (схлопывание версий подписи) + поиск пайщику по своим документам
Typecheck / desktop (pull_request) Successful in 13m57s
Typecheck / controller (pull_request) Successful in 13m34s
Два уточнения по замечаниям:

1. Ключ реестра переведён с hash на doc_hash. doc_hash = идентичность документа; hash включает
   наложенные подписи, поэтому у ОДНОГО документа бывает несколько hash (версии по мере накопления
   подписей — напр. акт приёма-передачи: 1-я подпись → один hash, 2-я → другой, doc_hash тот же).
   При ключе по hash документ задваивался в списке (одна подпись / две подписи). Теперь:
   - уникальный индекс (coopname, doc_hash); hash и package — неуникальные индексы;
   - ingestAction ключуется по data.document.doc_hash и держит КРАЙНЮЮ версию: guard'ы «не понижать
     статус» и «не откатывать на меньший block_num» (getState отдаёт status+blockNum); крайняя версия
     перетирает предыдущую (тот же документ, больше подписей);
   - hash хранится как версия последней подписи (для фильтра Union по document.hash и отображения).
   package по-прежнему группирует разные документы процесса (обмен = разные doc_hash = разные строки).

2. searchDocuments вернул доступ пайщику, но со скоупом: член совета (chairman/member) ищет по всему
   кооперативу, обычный пайщик — ТОЛЬКО по своим документам (search scope username=user.username).
   Снял role-гард, добавил @CurrentUser; SearchResolver решает скоуп по роли. Заодно закрывает прежнюю
   утечку (старый поиск был coop-wide для любого авторизованного).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 18:38:11 +00:00
coopops b9be36d203 [C28-21][@ant] fix(controller): ключ реестра — hash документа, а не package (package = процесс с N заявлений)
Typecheck / desktop (pull_request) Successful in 13m53s
Typecheck / controller (pull_request) Successful in 14m23s
Самоаудит по замечанию: package — это идентификатор ПРОЦЕССА, в который может входить несколько
заявлений с разными подписантами. Доказано в контракте: marketplace/change.cpp шлёт ДВА newsubmitted
с одним package, но разными document и username (обмен: contribute+return). Прежний уникальный индекс
(coopname, package) + upsert-по-package затирал второе заявление первым → ПОТЕРЯ ДОКУМЕНТА и склейка
username. Старый explorer-путь (список по newsubmitted) показывал оба.

- Сущность: уникальный индекс теперь (coopname, hash) — одна строка = один документ-заявление;
  (coopname, package) стал НЕуникальным группирующим индексом.
- ingestAction ключуется по data.document.hash; смена статуса (newsubmitted/newresolved/newdeclined,
  все несут то же заявление — подтверждено createagenda/declinedoc/make_complete_document) матчит
  строку по hash.
- newdecision/newact/newlink несут package (не заявление) → пересобирают агрегаты ВСЕХ заявлений
  пакета (repository.findByPackage), сохраняя статус каждого.
- Репозиторий: upsert/getStatus по hash; findByPackage вместо getSourceActionData; убраны
  неиспользуемые setStatus/exists.

Также: searchDocuments закрыт гардом RolesGuard + @AuthRoles(['chairman','member']) — поиск по
документообороту кооператива только для совета (SearchDocumentsInput не содержит username, поэтому
правило «свои ресурсы» не открывает доступ обычному пайщику). Поиск пайщиком по своим документам
пока не предусмотрен.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 18:20:38 +00:00
coopops 3943802931 [C28-21][@ant] fix(controller): парность getDocuments — «Все входящие» = все статусы + скоуп backfill по кооперативу
Typecheck / desktop (pull_request) Successful in 14m40s
Typecheck / controller (pull_request) Successful in 13m16s
Самоаудит видимости документов выявил две регрессии относительно прежнего explorer-пути:

1. Вкладка «Все входящие» (type=newsubmitted) теряла решённые документы. Прежний фильтр был по
   ИМЕНИ действия: у каждого пакета есть newsubmitted-действие → «Все входящие» = все документы
   (решённые показывались и тут, и во «Только утверждённые»). Партиция по статусу (newsubmitted→
   status=Submitted) скрывала решённые из дефолтной вкладки User/DocumentsPage. Фикс: newsubmitted →
   без фильтра статуса (все), newresolved → status=Resolved.

2. Backfill сканировал explorer с query={} — без скоупа. Explorer (cooparser) хранит трейсы по
   получателям; без receiver вернулись бы все трейсы каждого действия (soviet/пайщик) и, возможно,
   чужие кооперативы. Фикс: query={receiver: coopname} — ровно как прежний chairman-путь getDocuments
   (один трейс на документ, только наш кооператив).

Утечки не было и нет: RolesGuard разрешает обычному пайщику только data.username==его_имя, а фильтр
findAggregates ограничивает username; coopname-скоуп (весь кооператив) доступен лишь совету/председателю.
findAggregates всегда фильтрует по config.coopname.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 14:06:54 +00:00
coopops 33801798ed [C28-21][@ant] refactor(controller): полное удаление OpenSearch — поиск и список документов целиком на Postgres
Typecheck / desktop (pull_request) Successful in 14m26s
Typecheck / controller (pull_request) Successful in 13m48s
Завершение перехода одним PR: searchDocuments и getDocuments уже читают PG-реестр подписанных
документов, OpenSearch стал мёртвым кодом — убираем его полностью.

- Удалены infrastructure/search/* (opensearch.service, search-registry.service,
  search-infrastructure.module) и application/search/services/search-event.service
  (старый индексатор по newsubmitted — заменён SignedDocumentIngestionService + backfill).
- app.module: снят SearchInfrastructureModule. search.module: только SearchResolver (читает
  SIGNED_DOCUMENT_REPOSITORY из глобального TypeOrmModule).
- package.json: убрана зависимость @opensearch-project/opensearch; pnpm-lock.yaml пересчитан.
- docker-compose: удалён сервис opensearch + volume opensearch_data.
- .env-example: убраны OPENSEARCH_* (поиск больше не конфигурируется через env).
- system.interactor: features.search = true (поиск на PG доступен всегда, не зависит от OPENSEARCH_ENABLED).

Файлы удалены только из текущего состояния (git rm) — в истории остаются.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 11:36:55 +00:00
coopops aeeb9de380 [C28-21][@ant] fix(controller): getDocuments — точная эквивалентность read-path (receiver-скоуп + фильтр по hash)
Typecheck / desktop (pull_request) Successful in 13m35s
Typecheck / controller (pull_request) Successful in 13m6s
Старый getDocuments через DocumentService подмешивал в explorer-фильтр `receiver: username`
(on-chain require_recipient): receiver=coopname отдавал все документы кооператива, receiver=<пайщик>
— его документы; Union-страница добавляла filter.document.hash. PG read-path эти ключи игнорировал
(искал несуществующий data.username) — личная страница пайщика показала бы все документы кооператива.

- interactor: extractReceiverScope (receiver===coopname → весь кооператив; иначе фильтр username=receiver,
  т.к. колонка username = пайщик-субъект заявления) + extractHashFilter (filter.document.hash).
- repository: новый параметр hash в SignedDocumentListParams; фильтр UPPER(d.hash)=UPPER(:hash)
  (фронт шлёт хэш в upper-case).

Набор документов getDocuments теперь совпадает со старым explorer-путём для всех трёх потребителей
(Cooperative/ListOfDocuments, User/DocumentsPage, Union/ListOfCooperatives).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 11:30:02 +00:00
coopops 4d1b3b87c1 [C28-21][@ant] feat(controller): денормализация ФИО подписантов (signers_text) + поиск по подписанту
Typecheck / desktop (pull_request) Successful in 13m28s
Typecheck / controller (pull_request) Successful in 13m12s
Поиск по документам — в основном по фамилии подписанта, а не username. content_text (html заявления)
этого не покрывает: соподписанты (председатель/совет) подписывают решение/акты, а не тело заявления,
плюс зависимость от текста шаблона. ФИО уже присутствуют в агрегате — в signer_certificate каждой подписи
(DocumentAggregator резолвит их из учётных данных подписанта).

- Новая колонка signed_documents.signers_text — ФИО/наименования всех подписантов пакета
  (last/first/middle_name физлиц-ИП и short_name организаций из signer_certificate всех частей
  агрегата: заявление/решение/акты/связанные) + их username.
- Заполняется на ingestion и backfill из готового агрегата (collectSignersText).
- searchDocuments ILIKE теперь покрывает signers_text (приоритетно), full_title, content_text, username.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:24:14 +00:00
coopops d34303a5c4 [C28-21][@ant] feat: getDocuments+searchDocuments на Postgres, newdeclined в cooptypes, авто-backfill агрегатов
Typecheck / desktop (pull_request) Successful in 13m43s
Typecheck / controller (pull_request) Successful in 13m14s
Вторая итерация C28-21 — сервис функционирует по-новому из PG:

- cooptypes: заведено registry-действие soviet `newdeclined` (+ экспорт в Registry); отслеживается ingestion'ом.
- Реестр хранит ГОТОВЫЙ агрегат пакета (document_aggregate jsonb) + денормализованную проекцию
  (full_title/content_text/action/registry_id/username/block_num/document_created_at) для поиска и фильтра.
- Ingestion собирает агрегат через DocumentPackageAggregator на 6 событиях soviet:
  newsubmitted/newresolved/newdeclined (статус) + newdecision/newact/newlink (досборка по source_action_data).
- getDocuments — read-path из PG: отдаём готовые агрегаты, фильтр по статусу(type)/действию/пайщику/блокам,
  без обращений к explorer/Mongo на лету.
- searchDocuments — SQL ILIKE по PG-реестру (контракт SearchResultDTO сохранён).
- Backfill запускается автоматически РАЗОВО при пустом реестре (onApplicationBootstrap, детачем) или
  принудительно по SIGNED_DOCS_BACKFILL_ON_BOOT=true; идемпотентен по (coopname, package).

OpenSearch-инфра пока оставлена (снос — отдельный финальный шаг). cooptypes/dist gitignored — пересобирается в CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 08:52:48 +00:00
coopops f5ff2a7b10 [C28-21][@ant] feat(controller): реестр подписанных документов в Postgres — ingestion + backfill
Typecheck / desktop (pull_request) Successful in 14m18s
Typecheck / controller (pull_request) Successful in 13m30s
Фундамент C28-21: PG-проекция подписанных документов (таблица signed_documents),
наполняемая ловлей blockchain-событий soviet с внутренней шины и разовым backfill'ом.
Аддитивно — OpenSearch и резолверы (searchDocuments/getDocuments) пока не трогаются.

- SignedDocumentEntity (signed_documents): package/hash/coopname/username/status,
  full_title/content_text/html/pdf(bytea), document_aggregate(jsonb), meta, block_num.
- SignedDocumentRepository (+SIGNED_DOCUMENT_REPOSITORY) + TypeORM-реализация (upsert/setStatus/search ILIKE).
- SignedDocumentIngestionService: @OnEvent(action::soviet::{newsubmitted,newresolved,newdeclined})
  -> идемпотентный upsert с защитой от понижения статуса; контент из factory-Mongo.
- SignedDocumentBackfillService: разовый идемпотентный скан действий (env-gate SIGNED_DOCS_BACKFILL_ON_BOOT).
- Регистрация в TypeOrmModule + app.module.

newdeclined пока не экспортирован как registry-action в cooptypes — подписка по литералу на будущее.
Следующая итерация: перевод searchDocuments/getDocuments на PG + снос OpenSearch + тесты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 08:31:49 +00:00
coopops e96e157b91 fix(desktop): убрать гонку Teleport-host шапки и обновлять гранты/столы после регистрации
Баг A: предупреждение "Failed to locate Teleport target '#header-actions-host'"
возникало, потому что host был под v-if='loggedIn'. Сразу после регистрации
навигация на стол происходит раньше, чем loggedIn (зависит от async
session.isAuth / isRegistrationComplete) станет true → цель Teleport
отсутствует в DOM → даже defer-Teleport источников страниц её не находит.
Рендерим #header-actions-host безусловно — видимость блока действий уже
регулируется CSS (.topbar__actions:has(> .header-actions-host:only-child:empty)).

Баг B: после регистрации grant-gated кнопки («Совершить взнос», «Получить
возврат») не появлялись до F5. В потоке завершения регистрации (watch на
participant_account) не вызывался desktops.loadDesktop() (свежие столы/гранты),
а кошелёк/agreements не грузились: на момент первого run() статус ещё не
'active' → isFullyActive=false. После того как статус обновился, до-вызываем
run(true) и loadDesktop() по образцу init-app / EnableButton. Условие
isFullyActive в init-wallet не ослаблялось.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 07:39:59 +00:00
coopops bbb0ae5e41 feat(desktop): placeholder для наименования банка в форме ИП
Только неочевидное поле — наименование банка получило подсказку ПАО "Сбербанк".
Остальные поля ИП самоописательны (формат даты, кол-во цифр, «как в паспорте»).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 07:26:02 +00:00
coopops 2eeb64c188 feat(desktop): добавить placeholder-примеры в форму организации при вступлении
Поля краткого/полного наименования, должности представителя и наименования
банка получили placeholder-подсказки (ПК "Ромашка", Председатель совета,
ПАО "Сбербанк") — показываются при фокусе пустого поля.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 07:25:37 +00:00
coopops 33d4230873 fix(desktop): не терять выбор в BaseRadioCard при наведении
Hover-правило .base-radio-card:hover (специфичность 0,2,0) перекрывало
.base-radio-card--selected (0,1,0), поэтому при наведении на выбранную
карточку программы primary-фон/граница подменялись на surface/soft и
выбор визуально пропадал. Ограничил hover невыбранными карточками через
:not(.base-radio-card--selected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 07:07:29 +00:00
coopops 829f8f823b [design-wave1][@dark] fix: палитра команд — рамка курсора на любом столе, не только активном
Заголовок workspace-row получал outline только когда стол активный
(.is-active .row.is-selected). На неактивных столах при навигации курсор
«терялся»: только лёгкая смена фона на surface-2, без рамки — в отличие
от page'ей, которые подсвечиваются outline'ом всегда. Выровнял правило:
outline на любом .workspace-row.is-selected.
2026-05-28 19:17:02 +00:00
coopops 9f92e4bb52 [design-wave1][@dark] fix: палитра команд — clamp навигации стрелками вместо wrap-around
На верхней/нижней позиции стрелка дальше прыгала в противоположный
конец списка из-за (idx+delta+len)%len. Заменено на clamp в [0..len-1]:
курсор остаётся на месте, как в нативном listbox/меню. Пустое выделение
+ ↓ ставит на первый, ↑ — на последний.
2026-05-28 19:17:02 +00:00
coopops 8d895c5fa9 [design-wave1][@dark] fix: палитра команд — снят sticky-баннер активного стола
Активный workspace был position:sticky top:0 — при навигации стрелкой
вверх курсор-выделение «исчезал» за sticky-overlay'ем, потому что
scrollIntoView({block:'nearest'}) считал элемент видимым в normal flow,
не учитывая sticky-перекрытие. Активный стол и так идёт первым по
сортировке + есть badge «Активный», sticky не нужен.
2026-05-28 19:17:02 +00:00
coopops 8bfd4e916c [@dark] fix(desktop): CommandPalette — primary вместо accent + meta.conditions/hidden + drawer close на мобильниках
- CommandPalette.vue: все --p-accent / --p-accent-soft / --p-ink-on-accent
  заменены на --p-primary (бирюзовая палитра канона). Откатывается
  регрессия после fix-коммитов design-wave1 (sticky-banner / clamp /
  рамка курсора), которые случайно вернули accent поверх primary
  (см. b82b778b81 — изначально palette был в primary).

- default.vue::paletteWorkspaces: возвращена фильтрация meta.conditions
  (participant/chairman/soviet install.ts) + meta.hidden (capital
  install.ts) — та же логика что в LeftDrawerMenu.filteredRoutes,
  чтобы CommandPalette не показывал страницы, которые не отображаются
  в rail-меню. onSelectWorkspace/onSelectPage закрывают левый дровер
  на мобильниках через desktop.closeLeftDrawerOnMobile().
2026-05-28 19:15:47 +00:00
coopops df26d5ac9c [@dark] refactor(desktop): canon CommandPalette + снос осиротевшего меню — фикс утерянного ⌘K после design-wave1
- layouts/default.vue: вместо widgets/Desktop/CmdkMenu mount'им
  shared/ui/domain/CommandPalette (canon из design-wave1) с адаптером
  workspaceMenus → CommandPaletteWorkspace[] и глобальным ⌘K / Ctrl+K.
- entities/CommandPalette: минимальный store (isOpen / open / close /
  toggle) — заменяет тяжёлый useCmdkMenuStore.
- LeftDrawerMenu.onCmdk → palette.open() (триггер «Найти» в AppDrawer).
- Удалены целиком осиротевшие после design-wave1 директории:
  entities/CmdkMenu, widgets/Desktop/CmdkMenu, widgets/Desktop/CmdkTrigger,
  widgets/Desktop/SecondLevelMenuList (никем не использовался),
  widgets/Desktop/WorkspaceMenu (заменён на WorkspaceSwitcher),
  widgets/Wallet/MicroWallet (заменён на RailUserCard).
- Поправлены barrels widgets/Desktop и widgets/Wallet.
2026-05-28 19:02:24 +00:00
coopops 2136252933 fix(desktop): читаемость тостов на тёмной теме
Фон и текст тоста (.q-notification + .toast) теперь фиксированные тёмная
поверхность + светлый текст в обеих темах, а не инвертируемые
var(--p-ink)/var(--p-canvas). На dark-теме текст уходил в тёмный на тёмном
deep-tint фоне (#052e16 и т.п.) — нечитаемо. Цветные иконки по типу
остаются на токенах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 17:58:18 +00:00
ant e5e97d9074 fix(desktop): отступ документа от рамки в диалоге подписи соглашения
q-pa-lg на flat-карточке SignAgreementDialog — документ больше не прижат
к границе на странице кошелька пайщика.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 17:51:58 +00:00
coopops 7d2f49468d [blago-cli-add-filter][@ant] fix(blago-cli): фильтр non-entity .md в add + понятные ошибки push — чтобы blago add . не валил push «type: undefined»
— format/peekBlagoEntityType: не бросает; возвращает project|issue|story|undefined.
— sync/add: классификация unreadable/non-entity/clean/dirty; README/notes/CLAUDE/AGENTS не попадают в staging, отдельный счётчик skippedNonEntity.
— sync/push: на старте чистит staging от non-entity (warn); ошибки parseBlagoMarkdown / validateParsedForPush префиксуются путём файла.
— cmd/run-cli: вывод add показывает skippedNonEntity.
— bump 2026.5.28-1.
2026-05-28 17:17:08 +00:00
coopops 9d3be7259f [mobile-drawer][@ant] fix: левый дровер — автозакрытие на мобиле при смене маршрута 2026-05-28 16:43:15 +00:00
ant 4ffb32ff9e Merge pull request 'fix(desktop): vue-tsc — IAgenda для QuestionsTable + IPayment.id:string' (#45) from fix/desktop-vue-tsc-errors into dev
Reviewed-on: #45
2026-05-28 16:42:01 +00:00
Alex Ant 9d5d3ae967 chore(release): publish
Build bootstrap container / build (push) Successful in 3m29s
Release / release (push) Successful in 26m5s
Release / publish-packages (push) Successful in 14m41s
Release / trigger-coopenomics-docs (push) Successful in 1s
Release / trigger-mono-docs (push) Successful in 2s
2026-05-28 21:31:36 +05:00
Alex Ant b8ff69f40b Merge branch 'main' of ssh://git.coopenomics.world:222/C9S/mono 2026-05-28 21:31:03 +05:00
Alex Ant 2747ac34a4 chore(release): publish
Build bootstrap container / build (push) Successful in 3m27s
Release / release (push) Successful in 26m48s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-05-28 21:17:47 +05:00
coopops 745df7f6cc [fix-vue-tsc][@dark] fix: IPayment override → локальный IPaymentRow в виджете
Typecheck / desktop (pull_request) Successful in 13m36s
Typecheck / controller (pull_request) Successful in 13m14s
Глобальный Omit<IPayment,'id'>&{id:string} ломал store.ts/mergePayments
и features/Payment/SetStatus/api (туда приходит сырой Zeus-вывод с
id:unknown). Откатываю override в entities/Payment/model/types.ts,
сужаю id до string локально в ListOfPaymentsWidget через IPaymentRow —
точка сужения только в UI, контракт стора/API не трогается.

vue-tsc desktop локально: PASS.
2026-05-28 15:32:24 +00:00
coopops 61283a7276 [fix-vue-tsc][@dark] fix: vue-tsc desktop — IAgenda в QuestionsTable + IPayment.id:string
Typecheck / desktop (pull_request) Failing after 8m13s
Typecheck / controller (pull_request) Successful in 13m20s
QuestionsTable принимал Cooperative.Document.IComplexAgenda[] (cooptypes,
table:IDecision без certificates, id:IUint64=number|string), а сверху
ListOfAgendaQuestions передаёт IAgenda[] из desktop/entities/Agenda
(GraphQL-вывод getAgenda, table:BlockchainDecision с certificates,
id:number). Перевожу QuestionsTable на IAgenda — это реальная форма
данных из стора; уходит расхождение IComplexAgenda↔IAgenda для
:agenda='row' в QuestionCard (он тоже IAgenda).

IPayment.id был ModelTypes['ID']=unknown (Zeus не имеет scalar-resolver
для ID), из-за чего шаблон ListOfPaymentsWidget падал на expanded.get/.set
и :id-биндингах. Переопределяю id как string через Omit+&.
2026-05-28 15:08:37 +00:00
ant 14bee19c87 Merge pull request 'Минимум членов совета на этапе Install Coop: 5 → 3' (#44) from fix/min-soviet-members-3 into dev
Reviewed-on: #44
2026-05-28 15:01:04 +00:00
coopops 5ee3a60b72 [soviet][@dark] fix: MIN_SOVIET_MEMBERS_COUNT 5 → 3 для прод
Typecheck / desktop (pull_request) Failing after 8m42s
Typecheck / controller (pull_request) Has been cancelled
Снижает минимальный порог состава совета кооператива на этапе
Install Coop с 5 до 3 человек. Единственный ограничитель —
контракт soviet::createboard (см. consts.hpp + createboard.cpp:58).
Backend (install.interactor) и desktop (SetSovietForm) собственных
числовых проверок не имеют — фронт требует только «хотя бы одного»,
бэкэнд передаёт массив как есть в blockchainPort.createBoard.
2026-05-28 14:55:00 +00:00
ant 0ff8bb5775 Merge pull request 'design-wave2: legacy-stylers.scss → canon-токены + снос мёртвых классов' (#43) from feat/design-wave2-legacy-stylers-canon into main
Reviewed-on: #43
2026-05-28 14:50:20 +00:00
coopops 117d79ba1f [design-wave1][@dark] feat: снос legacy-stylers.scss целиком — canon-стили инлайн в потребителях
Что сделано:
- Перенёс canon-стили card-label/card-value/section-title локально в scoped
  блоки каждого реального потребителя (26 файлов).
- 7 файлов которые казались USE-ONLY оказались false-positive substring
  совпадений (.resource-info-card / .meet-info-card / .membership-info-card /
  .tariff-card-container / .cmdk-empty-state / .import-wizard__section-title /
  .result-section-title) — глобальные классы info-card/card-container/
  empty-state/section-title для них уже не нужны.
- Все 8 живых классов из legacy-stylers.scss (.card-label/.card-value/
  .section-title/.card-container/.info-card/.empty-state/.section-header/
  .title-container) теперь имеют ноль use-only потребителей: каждый файл
  имеет либо local scoped def, либо использует другой класс с похожим именем.
- Удалил `src/css/legacy-stylers.scss` целиком.
- Убрал `'legacy-stylers.scss'` из css array в `quasar.config.cjs`.

Метрики:
- 26 файлов получили scoped-блоки (~6-10 строк canon-токенов в каждом).
- 1 файл удалён (legacy-stylers.scss, 134 строки).
- 1 импорт убран (quasar.config.cjs).

Sanity:
- Все 26 scoped scss-блоков прокомпилированы через sass.compileString — OK.
- false-positives откатил через git checkout HEAD.

Файл legacy-stylers.scss больше не существует.
Долг wave1/2/3 закрыт.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:18:08 +00:00
ant 5041abe851 Merge pull request 'design-wave1: миграция desktop на canon — Form/ModalBase/Styles shims снос' (#42) from feat/design-wave1-cleanup-dialogs-forms into dev
Reviewed-on: #42
2026-05-28 14:10:13 +00:00
coopops 40c39b93fc [design-wave1][@dark] feat: legacy-stylers.scss → canon-токены — снос мёртвых классов
Что сделано:
- Снёс 8 классов с 0 потребителей: info-card-hover, page-main-card,
  card-title, card-action-btn, balance-card{,-primary,-warning},
  info-warning-card, section-card-warning (со всеми вложенными
  .balance-label/.balance-value/.warning-info/.section-content/...).
- Снёс глобальный .profile-section — единственный потребитель
  (MicroWallet) определяет его локально в <style scoped>.
- Перевёл живые классы на canon-токены var(--p-*):
  • title-container — layout-only, токенов не требует.
  • card-container — radius/border/bg через --p-r-lg/--p-line-1/--p-surface.
  • info-card — surface-2/line-1/r-md/p-4/p-3.
  • section-header + section-title — h2 (--p-fs-h2), color --p-ink,
    section-icon margin --p-3, header margin-bottom --p-6.
  • card-label/card-value — --p-fs-body/--p-ink-2 vs 16/500/--p-ink.
  • empty-state — p-8/p-5/p-4/p-2/h2/body/ink-1/ink-2.
- Убрал ручные dark-overrides (.body--dark &, .q-dark &) —
  canon-токены сами переключаются через [data-theme="dark"] в tokens.css.
- Media @max-width:768px ужал до одного правила (мобильный h3 для секций).

Метрика: 433 → 56 строк (-377/+56), CSS-выхлоп 2.1 КБ.

Не тронуто:
- Все 22-23 потребителя card-value/card-label — большинство определяет
  стили локально (LOCAL), 5 USE-ONLY компонентов наследуют глобальный.
- 13 потребителей section-title — 6 LOCAL, 7 USE-ONLY.

Долг: переписать USE-ONLY компоненты на canon-виджеты (ColorCard / BaseCard),
после чего глобальные card-* и section-* можно полностью снести
вместе с этим файлом.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:57:49 +00:00
coopops 04e7503e7d [design-wave1][@dark] feat: миграция extensions на канон — снос ModalBase/TitleStyles/CardStyles/AutoAvatar shim'ов
Typecheck / desktop (pull_request) Failing after 8m58s
Typecheck / controller (pull_request) Successful in 13m16s
ModalBase → BaseDialog (18 call-sites):
* extensions/capital — 14 features + 1 widget (ImportContributorsButton).
* extensions/chairman — 2 approval features + 1 page (AgendaPresetsPage).
* extensions/powerup — 1 widget (ResourceInfoWidget, через barrel).
Паттерн: q-dialog внутри q-btn → BaseDialog параллельно q-btn в общей div-обёртке.
persistent=true → :close-on-backdrop='false' :close-on-escape='false'.
@hide → @update:model-value='(v) => !v && callback()'. maximized → :maximized='true'.
ProjectInvestFabAction: два дублирующих q-dialog объединены в один BaseDialog.

TitleStyles + CardStyles → src/css/legacy-stylers.scss (глобально через quasar.config.cjs).
Все 8 side-effect import'ов в extensions/capital удалены.
Долг: переписать .title-container/.info-card/.card-* классы на canon-токены.

AutoAvatar: 1 импорт в CapitalProfilePage переключён на shared/ui/domain/AutoAvatar.
Shim shared/ui/AutoAvatar снесён.

Удалены: shared/ui/{ModalBase,TitleStyles,CardStyles,AutoAvatar} + reexport ModalBase
из shared/ui/index.ts. Design-wave1 закрыт полностью.
2026-05-28 13:42:39 +00:00
coopops 8d2962b786 [design-wave1][@dark] fix: вернуть re-export ModalBase в shared/ui/index.ts
ResourceInfoWidget в extensions/powerup импортирует ModalBase через
barrel: import { ModalBase } from 'src/shared/ui'. После сноса
re-export'а vite валился SyntaxError: does not provide an export.
Возвращаю строку обратно — папка ModalBase уже восстановлена
предыдущим коммитом.
2026-05-28 13:10:30 +00:00
coopops 15465e805e [design-wave1][@dark] fix: вернуть ModalBase/TitleStyles/CardStyles/AutoAvatar для extensions
В прошлой волне снёс shared/ui/{ModalBase,TitleStyles,CardStyles,AutoAvatar},
но смотрел только src/. В extensions/{capital,chairman}/ осталось:
* ModalBase — 17 файлов
* TitleStyles (side-effect SCSS) — 6 страниц capital
* CardStyles (side-effect SCSS) — 1 виджет capital
* AutoAvatar — 1 страница capital
Из-за этого vite валился на 404 TitleStyles → "Failed to fetch dynamically
imported module src/boot/init.ts" и фронт не грузился.

Восстановлены оригинальные файлы. AutoAvatar в shared/ui/AutoAvatar
оставлен как re-export нового канон-расположения shared/ui/domain/AutoAvatar.
Полная миграция extensions на BaseDialog и канон-токены — отдельная волна.
2026-05-28 13:08:03 +00:00
coopops b69920e263 [design-wave1][@dark] fix: cooperativeAgreements — не падать на пустом coopname
При bootstrap'е фронта RequireAgreements/SignUp успевают смонтироваться
ДО завершения system.loadSystemInfo(), и info.coopname=undefined.
Zeus сериализует variables в {} → сервер кидает
"Variable $coopname of required type String! was not provided".

Решение по слою:
- api/index.ts: гард — если coopname пустой, не отправляем запрос.
- RequireAgreements/SignUp: watch(() => info.coopname) — load
  отрабатывает, как только coopname прорастёт, без блокировки UI.
2026-05-28 13:03:44 +00:00
coopops 298e1919e8 [design-wave1][@dark] feat: BaseDialog :maximized — закрываем последние 5 maximized + сносим ModalBase
В BaseDialog добавлен prop :maximized (boolean) — q-dialog получает maximized, q-card растягивается на весь экран, body становится flex-scrollable. При :maximized размер size игнорируется.

Это покрывает 5 last maximized call-sites:
* Agreementer.SignAgreementDialog — fullscreen для подписи договора (hideCloseButton)
* Agreementer.ReadAgreementDialog — fullscreen для просмотра договора
* Agreementer.StaticPrivacyDialog — fullscreen для статичной privacy policy
* Branch.SelectBranch.SelectBranchOverlay — fullscreen onboarding flow с шагами 1/2
* Decision.CreateProject.CreateProjectButton — fullscreen для предложения повестки

Все 5 переведены на BaseDialog :maximized='true' :close-on-backdrop='false' :close-on-escape='false'.

После миграции ModalBase больше не используется — удалена папка shared/ui/ModalBase и реэкспорт из shared/ui/index.ts. Design-wave1 закрыт полностью.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:49:56 +00:00
coopops 7735c40d06 [design-wave1][@dark] refactor: ModalBase → BaseDialog в 14 call-sites — основная часть миграции
Простые модальные диалоги переведены с q-dialog+ModalBase на canon BaseDialog (внутри уже q-dialog), API v-model:modelValue:

* features: DeleteBranch / CreateBranch / DepositToWallet (двух-стейтовый dialog) / WithdrawFromWallet / AddPaymentMethod / DeletePaymentMethod / Decision.CreateProjectFreeDecision (lg) / FreeDecision.CreateProject (md) / Payment.SetStatus.SetOrderPaid/Completed/Refunded
* widgets: ConnectionDashboard.AxonWallet / Desktop.WorkspaceMenu (size=lg вместо 700px-фикса)
* shared: CreateDialog (общий wrapper) / CouncilOnboarding.CouncilOnboardingCard (кнопки в slot footer)

Persistent → :close-on-backdrop='false' + :close-on-escape='false'. @hide='clear' → @update:model-value с проверкой v===false. style='width: NNNpx' переведён на canon size sm/md/lg.

Что НЕ мигрировалось (5 файлов остались с ModalBase):
SelectBranchOverlay, SignAgreementDialog, ReadAgreementDialog, StaticPrivacyDialog, Decision.CreateProject.CreateProjectButton — все используют :maximized='true'. BaseDialog в canon не имеет fullscreen-режима, расширять canon без согласования — нарушение правила "Канон-нейминг согласовывать ДО реализации". Эти 5 кейсов оставлены legacy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:48:41 +00:00
coopops 41d0af6706 [design-wave1][@dark] refactor: Form → shim над BaseForm/BaseButton — 20 call-sites визуально canon
Form.vue переписан как тонкий wrapper над canon BaseForm + BaseButton с сохранением старого API (handlerSubmit/isSubmitting/showCancel/showSubmit/buttonSubmitTxt/buttonCancelTxt/disabled/size).

Что изменилось визуально:
* Кнопки cancel/submit вынесены в slot #footer (canon-layout с gap=14px между body и footer).
* Cancel — variant='ghost' (плоская), Submit — variant='primary' (canon-primary), оба BaseButton с no-caps/без ripple.
* Кнопки выровнены justify-content:flex-end (canon-pattern для форм), вместо старого .flex без выравнивания.

API call-sites не трогаем — 20 form-call-sites (Branch/Wallet/PaymentMethod/Agreementer/Decision/Union/FreeDecision/Payment/Request/AxonWallet) получают canon-визуал сразу.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:47:15 +00:00
coopops 8356b507f8 [design-wave1][@dark] chore: снос CardStyles — заинлайнил единственный live-класс info-card
* Глобальный CardStyles из 430 строк CSS реально использовался только через .info-card в RestartMeetForm.vue.
* Все остальные классы (card-container, section-header, card-label, card-value) определены локально в scoped CSS соответствующих компонентов — глобальный импорт лишний.
* Стили .info-card (+ dark-override) заинлайнены в scoped стиль RestartMeetForm под нативным именем .meet-form-agenda-item.
* Удалены: глобальный импорт в App.vue, @import в RestartMeetForm, папка shared/ui/CardStyles.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:47:15 +00:00
coopops c306789ed2 [design-wave1][@dark] chore: снос мёртвых InputStyles + TitleStyles — волна 5
* InputStyles — весь файл состоял из закомментированного кода.
* TitleStyles — .title-container не использовался ни в одном .vue.
* Удалены 3 импорта InputStyles в Editable*Card и реэкспорт TitleStyles из shared/ui/index.ts.

CardStyles оставлен: используется в 4 call-sites (RestartMeetForm, UnionMembershipStep, TariffCard, MeetInfoCard) — миграция отдельным шагом.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:47:15 +00:00
coopops 11b0d6f123 [design-wave1][@dark] refactor: ToogleDarkLight → ThemeToggle + AutoAvatar в domain — добиваем волну 5
* ThemeToggle получает asButton/showText/isMobile, поглощая роль ToogleDarkLight (исправлена опечатка в имени).
* AutoAvatar перенесён в shared/ui/domain/AutoAvatar — он DiceBear-процедурный, а не base-примитив (canon Avatar делает инициалы/image и не заменяет процедурную генерацию).
* Снесены legacy папки shared/ui/ToogleDarkLight и shared/ui/AutoAvatar.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:47:15 +00:00
ant 8bdc3dd0cd Merge pull request 'Канон тостов и шапка-мобильник: bottom-right + accent-полоска + icon-only CTA' (#38) from feat/notif-mobile-canon into dev
Reviewed-on: #38
2026-05-28 11:57:00 +00:00
coopops 0229b639b2 [design-wave2][@ant] fix: навигатор — canon clean-up + сохраняем primary
Typecheck / desktop (pull_request) Failing after 8m23s
Typecheck / controller (pull_request) Failing after 1m48s
- default.vue: убрал bordered у левого q-drawer (двойная линия с .rail
  border-right из канона) и hardcoded #00800038 (legacy зелёное cooperative)
  → var(--p-line); удалил мёртвый .drawer-close-btn.
- components.css: комментарий .rail__usercard поправлен — там primary,
  а не accent (намеренно: оранжевый в шапочке навигатора отвлекал бы).
- _dev/ui: текст секции AppDrawer привёл к фактическому поведению
  (soft-primary заливка + 2-px deep-teal rail).

Цветовая схема навигатора:
- активный пункт — primary (deep teal #0f766e), 2px rail слева;
- brand-иконка, avatar, balance-блок — primary-soft (не accent);
- никакого accent (оранжевого) в навигаторе.
2026-05-28 09:44:55 +00:00
coopops 64fa0d2f97 [design-wave2][@ant] docs: строгое указание использовать дизайн-канон в desktop
Typecheck / desktop (pull_request) Failing after 8m16s
Typecheck / controller (pull_request) Successful in 13m28s
В CLAUDE.md новый раздел «ДИЗАЙН-КАНОН desktop — ОБЯЗАТЕЛЕН»:
1) HTML-канон ~/blago/production/shared/MONO Design System.html;
2) Живая реализация components/desktop/src/pages/_dev/ui/index.vue.

После компакта забыл, что shared/MONO Design System.html — основной SoT,
и нашёл «канон» в auth-prototype, который НЕ канон — пришлось переверстать
тосты. Фиксирую правило, чтобы не повторять.
2026-05-28 09:33:57 +00:00
coopops 82478155ce [design-wave2][@ant] fix: capital header-кнопки в canon micro-режим
Typecheck / desktop (pull_request) Failing after 8m19s
Typecheck / controller (pull_request) Successful in 13m6s
Завершаю аудит — добавил capital по запросу:
- RouteMenuButton получил опциональный icon-проп; на мобильном
  превращается в round-иконку + tooltip, на десктопе — flat + label.
- В ProjectPage и ComponentPage прописал material-иконки для каждого
  раздела (Описание/Артефакты/Компоненты/План/Участники/История/
  Задачи/Голосование/Результаты).
- ImportContributorButton, ImportContributorsButton, FilterDialogWithButton
  переведены в canon-micro (isMobile → flat+dense+sm+accent + tooltip).
  Заодно вынес q-dialog из q-btn-вложенности.

RouteMenuButton по сути — раздельные таб-кнопки навигации; полноценное
UX-решение для capital (вынести в SecondLevelTabs под шапкой) — отдельная
задача, тут только мобильная читаемость шапки.
2026-05-28 09:03:45 +00:00
coopops ecd895e745 [design-wave2][@ant] fix: остальные header-кнопки в canon micro-режим
Typecheck / desktop (pull_request) Failing after 8m21s
Typecheck / controller (pull_request) Successful in 13m9s
Аудит точек установки в шапку — два механизма:
1. registerAction(useHeaderActions) — Members/Branches/Documents/Agenda
   /Participants/CoopWallets/powerup.Settings;
2. Teleport to=#header-actions-host — PaymentMethods/Documents/Wallet/Meets.

Уже в каноне: SearchHeaderAction, CreateMeetButton, DepositButton,
WithdrawButton, AddPaymentButton, AddMemberButton, CreateBranchButton.

Доделал остальные:
- CreateProjectButton (Предложить повестку);
- AddUserButton (Добавить пайщика);
- ImportParticipantsButton (Импорт);
- TransferWalletsButton (Перевести между кошельками);
- powerup SettingsPage save-кнопка (через computed + watch на isMobile).

Капитал-расширение по правилу проекта не трогаю.
2026-05-28 08:44:25 +00:00
ant e2f467b78f Merge pull request 'fix(desktop): четыре vue-tsc ошибки — типизация props и string-fallback'и' (#39) from fix/vue-tsc-desktop into dev
Reviewed-on: #39
2026-05-28 08:43:33 +00:00
coopops c29c3720d2 [design-wave2][@ant] fix: AddMember/CreateBranch — canon micro в шапке на мобиле
Typecheck / desktop (pull_request) Failing after 8m14s
Typecheck / controller (pull_request) Successful in 12m59s
CSS-хак `.topbar__actions .base-btn__label { display: none }` не покрывал
эти две кнопки: они рисуют q-btn напрямую через registerAction (не через
BaseButton под .topbar__actions). В итоге на мобильном «Добавить члена»
и «добавить участок» оставались с лейблами и распирали шапку.

Перевёл обе в canon-паттерн как CreateMeetButton/DepositButton/WithdrawButton:
isMobile → flat+dense+sm+accent, иконка + q-tooltip; без обёртки
.header-action, чтобы кнопка не растягивалась на всю высоту шапки.
2026-05-28 08:33:56 +00:00
coopops 45ef337201 [design-wave2][@ant] fix: канон тостов под shared/MONO Design System.html + dev/ui демо
Typecheck / desktop (pull_request) Failing after 8m14s
Typecheck / controller (pull_request) Successful in 13m2s
- quasar-canon.css: переписан override .q-notification под реальный
  канон (shared/MONO Design System.html → .toast): тёмный фон
  (var(--p-ink) для нейтрального; #052e16/#4c0a0a/#3d2400/#0c1e3f
  для positive/negative/warning/info), светлый текст, ЦВЕТНАЯ иконка
  по типу (--p-pos/--p-neg/--p-warn/--p-info). Раньше ошибочно был
  сделан нейтральный фон + left-accent-полоска — «откуда попало»,
  не из канона.
- alerts.ts: убран color: 'grey-7'/'positive' у actions — они
  наследуют светлый тон от тёмного фона через override.
- _dev/ui секция 39 «Тосты»: пять кнопок для триггера всех типов
  тостов (успех, успех с CTA, ошибка, in-app push, push с avatar).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 07:48:40 +00:00
coopops e0e846c24b [design-wave2][@ant] fix: канон тостов и шапка-мобильник — bottom-right + accent-полоска, icon-only CTA
Typecheck / desktop (pull_request) Failing after 8m14s
Typecheck / controller (pull_request) Successful in 13m1s
- SuccessAlert/FailAlert/NotifyAlert (shared/api/alerts.ts): единый
  визуал через override .q-notification в quasar-canon.css. Нейтральная
  поверхность + 3px accent-полоска по типу (positive/negative/warning
  /info), без «цветной заливки карточки» (stop-signal §19.1).
- Позиция тостов bottom-right — было top-right, что противоречило
  канону mono-design-system v2 (toast-host: fixed; bottom: 24px;
  right: 24px). На мобильнике — full-width минус 16px полей.
- SuccessAlert: type:'positive' вместо color:'primary' — раньше успех
  красился брендовым teal, а не позитивным зелёным.
- NotifyAlert: дефолтная иконка notifications, если нет avatar — чтобы
  in-app push от Novu визуально совпадал с Success/Fail.
- Topbar @media (max-width:600px): .topbar__actions .base-btn__label
  скрыт, остаются только иконки CTA-кнопок страницы. Бренд-название
  с ellipsis (max-width: 50vw). Раньше кнопки переносились на вторую
  строку на узких экранах.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 06:59:37 +00:00
coopops 81db8355e0 [fix-vue-tsc-desktop][@ant] fix: четыре vue-tsc ошибки в desktop — типизация props и string-fallback'и
Typecheck / desktop (pull_request) Failing after 8m22s
Typecheck / controller (pull_request) Successful in 13m12s
CI vue-tsc --noEmit падал на четырёх файлах:

- QuestionsTable.vue: props.decisions типизирован через
  PropType<Cooperative.Document.IComplexAgenda[]>. Раньше тип был
  плоский Array → row в v-for становился unknown, и template
  ругался на row.table.id, isProcessing(row.table.id) и т.п.
- ListOfPaymentsWidget.vue: явный computed items: IPayment[] через
  cast (as unknown as IPayment[]) — zeus резолвил items слабее, чем
  нужно шаблону, и поля row.id / username / quantity / etc. оставались
  unknown.
- ExtensionPage.vue и ExtensionCard.vue: для AutoAvatar :username
  добавлен fallback || '' — extension.name / extension.title могут
  быть string | null | undefined, AutoAvatar требует string.

ESLint по четырём файлам — EXIT=0; tsc-прогон делегирован CI
(точечный vue-tsc по факту тащит весь проект и вешает машину, см.
memory feedback_no_full_vue_tsc_desktop).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 06:50:12 +00:00
Alex Ant c3c49cc297 Merge branch 'main' into dev
Build bootstrap container / build (push) Successful in 2m26s
2026-05-27 23:52:26 +05:00
Alex Ant f4a5a8ce2d chore(release): publish
Build bootstrap container / build (push) Successful in 4m7s
Release / release (push) Successful in 42m38s
Release / publish-packages (push) Successful in 14m54s
Release / trigger-coopenomics-docs (push) Successful in 1s
Release / trigger-mono-docs (push) Successful in 2s
2026-05-27 23:52:22 +05:00
Alex Ant 2bbeecfa4f Merge branch 'testnet' 2026-05-27 23:36:46 +05:00
Alex Ant a32ec9532b Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev 2026-05-27 23:36:09 +05:00
ant 67b808db04 Merge pull request 'fix(security): не отдавать matrixRoomId на фронт — комнаты секретаря и транскрипции по opaque id' (#37) from feat/chatcoop-hide-matrix-room-id into dev
Reviewed-on: #37
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-05-27 09:22:52 +00:00
coopops 0bc05de392 chore(release): publish
Build bootstrap container / build (push) Successful in 3m4s
Release / release (push) Successful in 26m12s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-05-27 09:22:05 +00:00
coopops 9f1058c997 Merge branch 'dev' into testnet
Build bootstrap container / build (push) Successful in 3m22s
2026-05-27 09:19:48 +00:00
ant 3c2a03df6f [nodemon-restart-storm][@ant] fix: debounce nodemon + SIGTERM в controller/nodemon.json — устранить restart-storm, вешавший coopback на bind-mount
Без delay nodemon реагировал на каждое fs-событие пачки (правки/сборка/git на хосте) десятками рестартов в секунду; pstree.remy без `ps` в образе виснет на обходе /proc (накапливались зомби-сканеры), холодный старт ts-node прерывался, порт 2998 не занимался — бэкенд молча лежал. delay:2000 схлопывает пачку в один рестарт, signal:SIGTERM даёт чистое завершение дочернего процесса.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:30 +00:00
ant 8209482556 [fix-ext-config-lost-update][@ant] fix: убрать lost-update конфигов расширений в периодических задачах — cron-планировщики перезаписывали config устаревшим in-memory снимком с boot и стирали онбординг-флаги/хэши председателя
extensionRepository.update заменяет config JSONB целиком; chairman/powerup/meet-tracker писали захваченный при initialize снимок поверх свежих данных. Теперь read-modify-write по актуальному config из БД с наложением только владомых полей.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:30 +00:00
coopops cd323add27 [design-wave1][@dark] feat: тактильная отдача (вибрация) при перезагрузке страницы
Boot-файл haptics: при reload страницы на устройстве с Vibration API (мобильный
браузер / PWA на Android) коротко вибрирует (15 мс). Reload определяется через
Navigation Timing (с фолбэком на legacy API), чтобы не срабатывать на первой
загрузке/навигации. Полностью безопасно для десктопа и обычных сайтов: где
Vibration API нет — вызов не делается, любая ошибка проглатывается, на загрузку
приложения не влияет. Только клиент (гард для SSR).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:30 +00:00
coopops 3d4e7fae0f [design-wave1][@dark] fix: убрать паразитный горизонтальный скролл левого дровера
В левом дровере был маленький горизонтальный скроллбар, и трекпадом его можно
было «оттянуть», обнажая правую границу — дровер выглядел скроллируемым, хотя не
должен. Причина: у .rail собственный border-right (1px), который при content-box
даёт ~1px overflow внутри контента дровера. Фикс в .app-left-drawer: overflow-x
hidden + overscroll-behavior-x contain на .q-drawer__content, .rail приведён к
box-sizing border-box и width 100%.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:30 +00:00
coopops a829e9452b [design-wave1][@dark] fix: повестка — держать лоадер на «Утвердить» 3с после голоса
Если проголосовать и сразу нажать «Утвердить», утверждение падает с ошибкой —
бэкенд ещё не учёл голос из блокчейна. После успешного голоса держим состояние
загрузки пункта (а значит и кнопку «Утвердить» в :loading) ещё VOTE_SETTLE_MS
(3с), за это время голос успевает обработаться. На ошибке загрузка снимается
сразу. Заодно ручное мутирование processingDecisions + setTimeout-хак заменены
аккуратным реактивным хелпером setProcessing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:30 +00:00
coopops 64809ab37c [design-wave1][@dark] fix: повестка — скрывать пункт только при утверждении, не при голосовании
Повестка совета показывает все НЕутверждённые вопросы. Голос «за»/«против» не
должен убирать пункт — он остаётся неутверждённым, лишь помечается отметкой
голоса. Убрал ошибочное actedDecisionIds.add из onVoteFor/onVoteAgainst
(оставлен только тихий рефетч). Скрытие через actedDecisionIds оставлено только
в onAuthorizeDecision — там пункт исполняется и должен уйти из повестки без
возврата от отстающего поллинга.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:30 +00:00
coopops 8586aab85a [design-wave1][@dark] fix: повестка совета — без моргания и без «исчез/вернулся» при голосовании
Две проблемы при голосовании/утверждении в повестке совета:

1. Моргание всей страницы: QuestionsTable подменял список тремя
   скелетонами при любом loading=true, а пост-экшен loadDecisions вызывался
   без hidden → loading=true → список «моргал» в скелетоны и обратно.
   Скелетоны теперь показываются только на первой загрузке (loading &&
   !decisions.length), а пост-экшен рефетчи переведены в тихий режим (hidden).

2. «Исчез → вернулся → исчез»: повестка на бэкенде уже не отдаёт пункт, по
   которому проголосовал/утвердил, но данные из блокчейна доходят с задержкой,
   и поллинг успевал вернуть отработанный пункт. Добавлен локальный набор
   actedDecisionIds — проголосованный/утверждённый пункт прячется сразу и не
   возвращается, что бы ни подтянул отстающий рефетч. Просто, без оптимистичных
   merge-оверлеев.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:30 +00:00
coopops d7ddfeb0f2 [design-wave1][@dark] fix: push-подписка не должна вешать UI и шуметь красной ошибкой
Две независимые проблемы при выдаче разрешения на уведомления:

1. Фриз приложения: shouldShowDialog был computed с побочным эффектом —
   внутри геттера вызывался updateSupport(), присваивавший store.support
   новый объект на каждое чтение. После выдачи разрешения это давало каскад
   инвалидаций/ре-рендеров, забивавший главный поток (роутер менял URL, DOM
   «застывал»). Геттер сделан чистым, updateSupport() вынесен в showDialog().

2. Красная ошибка «Service Worker не готов в течение 5 секунд»: в dev без PWA
   SW намеренно не регистрируется, поэтому navigator.serviceWorker.ready
   никогда не резолвится. Теперь сначала проверяем getRegistrations() — при
   отсутствии SW выходим сразу и тихо (warn вместо error), а subscribe()
   возвращает false без FailAlert. Push при недоступности SW полностью
   изолирован и не влияет на работу приложения.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops e41fbf4570 [design-wave1][@dark] feat: скрывать «Совершить взнос»/«Получить возврат» до подписи соглашения кошелька
Кнопки взноса и возврата на столе пайщика недоступны, пока пайщик не
подписал главное соглашение цифрового кошелька (type='wallet') — как и
сама карточка кошелька, которая не появляется без подписанного соглашения.

Геттер isWalletAgreementSigned в сторе Wallet читает тот же список
соглашений пайщика, что и RequireAgreements.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops 0f648d0bd6 [design-wave1][@dark] copy: явная опциональность шага «Импорт пайщиков» (онбординг председателя)
«(опционально)» в заголовок + пояснение в описании: если есть пайщики —
импортируйте CSV, если нет — двигайтесь дальше. Чтобы шаг не воспринимался
обязательным.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops 7af7a64091 [design-wave1][@dark] fix: ключ в Invite — выделенная панель вместо readonly-инпута
readonly outlined q-input рендерится с dashed-рамкой (дефолт Quasar) и
обрезал значение (…UJR4) — выпадало из канона и выглядело плоско.
Сделал ключ surface-2 панелью: eyebrow-лейбл + copy-иконка в шапке,
mono-значение целиком (word-break) ниже. Даёт вес и убирает «ёлочку».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops 81b066e697 [design-wave1][@dark] fix: канон-композиция карточки «Сохраните ключ» (Invite)
Прошлый вариант разъезжался: «Скопировать» висел сиротой справа, провалы
между блоками, узкая потерянная кнопка, карточка 560px — разрежено.
Переделал по канону:
- копирование ключа — иконкой content_copy в append самого поля (+tooltip)
- кнопка «Установить ключ» — full-width (block), как «Войти» в LoginForm
- ширина карточки 480 как у остальных auth-карточек
- чекбокс слева, плотный ритм без лишних margin (reserve-hint-space)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops eac08d3438 [design-wave1][@dark] copy: полные примеры ОПФ+ в placeholder (с «Социального Комплекса»)
Голый «Потребительский Кооператив» в примере провоцировал вводить просто
ОПФ без расширения. Дал полный пример «...Социального Комплекса» во всех
трёх падежах — чтобы вводили расширенную ОПФ+, а не базовую форму.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops 933b0698be [design-wave1][@dark] copy: шаг «Настройка фабрики документов» вместо «Переменные документов»
Это не переменные, а постоянные параметры, которыми настраивается фабрика
документов кооператива. Переименовал label и intro шага.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops 6811bfa6fa [design-wave1][@dark] revert: убрать глобальный margin-bottom у .q-field--with-bottom
Микроотступ стал лишним и снова раздвигал поля слишком широко — откатываю.
Разделение полей обеспечивает сам reserve-hint-space, дополнительный
глобальный отступ не нужен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops c2bbe68e2c [design-wave1][@dark] fix: примеры-подсказки в placeholder вместо hint (SetVariablesForm)
Постоянный hint под полем занимал место и отвлекал. Перенёс примеры
наименований ОПФ+ в placeholder (видны только в пустом поле), убрал hint.
Добавил reserve-hint-space всем q-input формы — строка под ошибку
резервируется без текста, валидация не вызывает layout shift.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops 17bfd2b5cd [design-wave1][@dark] fix: системный микроотступ под полями с hint/error
Без зазора текст hint/error верхнего поля почти касался границы нижнего.
Вернул маленький отступ, но системно и один раз — глобально в quasar-canon:
margin-bottom: var(--p-1) только полям .q-field--with-bottom (те, что
резервируют нижнюю строку). Работает везде (BaseForm, формы установки,
регистрация), в стеках не складывается в дыру, инлайновые поля не трогает.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
claude ebf4929a66 booter fix 2026-05-27 09:19:29 +00:00
coopops 1961345a54 [design-wave1][@dark] fix: убрать двойной зазор между полями форм установки
reserve-hint-space у q-input/BaseInput уже даёт ~24px снизу под error/hint;
дополнительный gap в стеках полей складывался с ним → избыточное расстояние.
Убрал gap (канон BaseForm__body):
- CreateOrganizationDataForm / IndividualDataForm (.user-data-stack)
- SetVariablesForm (.vars-section__fields)
- RequestKeyForm (.request-key)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops 23918d6aea [design-wave1][@dark] fix: dense-инпуты в формах установки кооператива
Канон-плотность полей (dense) была только в BaseInput; общие формы
CreateOrganizationDataForm и IndividualDataForm + поля шагов установки
рендерили крупные не-dense q-input. Добавил dense:
- CreateOrganizationDataForm (22 поля) — данные организации
- IndividualDataForm (6 полей) — члены совета
- email-инпуты SetInitForm/SetSovietForm и поля SetVariablesForm

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops 1e774573fa [design-wave1][@dark] feat: страница установки кооператива в канон
- InstallCooperativePage: VerticalStepper вместо q-stepper, canon-панель
  с accent-стрипом, экран завершения в каноне (без градиентов/shimmer)
- RequestKeyForm: BaseInput (mono) + BaseButton
- SetInitForm: canon info-нотки, кнопки Назад/Далее на BaseButton
- SetSovietForm: canon-карточки членов совета вместо q-card/q-badge
- SetVariablesForm: canon-секции, q-input outlined с правилами, BaseButton
- Invite: осмысленное тело AuthCard при отсутствии токена приглашения

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops 30d5cf6156 [design-wave1][@dark] fix: убрать двойной отступ сверху на странице расширения
.extension-page уже внутри .catalog-shell__content с padding var(--p-6);
собственный padding давал двойной зазор от шапки до кнопки «Назад».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops 303c169fea [design-wave1][@dark] style: панель вокруг контента расширения + «Отменить» в настройках
- Весь контент страницы расширения обёрнут в канон-панель (surface+border),
  больше не лежит на голом фоне
- В режиме настроек добавлена кнопка «Отменить» (CancelButton) рядом с
  «Сохранить» — явный выход без сохранения; кнопка «Назад» страницы тоже работает

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops 30a9b5c33e [design-wave1][@dark] style: страница расширения — назад на уровень страницы, центровка лого/столов, вкл+удалить в строку
- Кнопка «Назад» убрана из шапки (useBackButton снят), добавлена канон-кнопка
  под шапкой на самой странице
- Логотип AutoAvatar центрирован в колонке
- DesktopsList: центрированный inset-блок (выделяется), а не плоский список
- ext-actions toggle: «включено» и «удалить» снова в одной строке

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops f03f8f3bed [design-wave1][@dark] feat: страница расширения и его настройки в канон
- ExtensionPage: двухколоночный канон-layout с padding; q-img заменён на
  генеративный логотип AutoAvatar (animated), консистентно с каталогом
- ExtensionInfo: канон-заголовок + статус-бейдж, теги убраны
- ExtensionActions: компоновка кнопок в столбец на токенах, teal→primary
- ExtensionInstall: BaseCard вместо .info-card/text-h5
- DesktopsList: text-grey/subtitle → канон-токены
- SaveButton/SettingsButton/InstallButton: teal→primary, no-caps

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops a41861f356 [design-wave1][@dark] style: анимация логотипа по наведению на всю карточку
Триггер ринг-анимации перенесён с hover самого SVG на hover карточки
(.app-card:hover :deep(.ring-seg)).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops 754dbe67a0 [design-wave1][@dark] feat: анимация логотипа приложения по наведению
AutoAvatar получил режим animated: инлайн-SVG вместо <img>, каждый
сегмент-дуга обёрнут в .ring-seg. По hover логотипа сегменты на ~1.3с
расходятся — прямые и зеркальные (matrix-flip) от одного поворота идут
в разные стороны, со стаггером по индексу. CoopCard не затронут (img/blob).
Уважает prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops f6503e291b [design-wave1][@dark] style: убрать фиксированный резерв высоты заголовка карточки
Статус идёт сразу под заголовком (динамично) — без дыры под однострочными
названиями. Обрезку в 2 строки оставили, чтобы длинный заголовок не ломал сетку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 3461343710 [design-wave1][@dark] style: выровнять карточки приложений при переносе заголовка
Заголовок резервирует 2 строки (clamp+min-height), аватар по верху —
статус и описание на всех карточках начинаются на одной линии, не прыгают.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 2d1f869774 [design-wave1][@dark] style: компоновка карточки приложения — заголовок и статус рядом с логотипом
Верхний ряд: логотип + (заголовок, под ним статус); описание во всю
ширину ниже; футер «Подробнее» прижат к низу для выравнивания карточек.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops a4498e1a45 [design-wave1][@dark] style: приглушить генеративный логотип каталога
Палитра колец → мягкие приглушённые тона + CSS saturate(.5)/opacity .85,
чтобы знак читался как тихая текстура, а не неоновое пятно.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 49836cd139 [design-wave1][@dark] feat: генеративный логотип (DiceBear rings) в карточке приложения
- AutoAvatar параметризован: props size/radius/background/ringColor
  с обратно-совместимыми дефолтами (CoopCard не затронут)
- ExtensionCard: буква-монограмма заменена на уникальный rings-логотип,
  seed = имя расширения; цвет кольца детерминирован по seed из палитры

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 226943a52a [design-wave1][@dark] feat: каталог приложений в канон — табы на 2-й уровень, карточки без изображений
- «Магазин приложений» → «Каталог приложений» (drawer + breadcrumb)
- Витрина/Установленные перенесены из топбара в SecondLevelTabs
- ExtensionCard переверстан: убраны изображение и теги, добавлены
  монограмма-плитка, статус-бейдж, обрезка описания, футер «Подробнее»
- Quasar .row/.col-md-3 → канон auto-fill грид

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 55983ed3f3 [design-wave1][@dark] fix: вёрстка формы провайдера платежей + текст контактов
- PaymentProviderForm: убраны вложенные q-gutter (кривой левый отступ
  селекта), standout=bg-teal → outlined, text-grey-7 → токен; чистый
  канон-столбец (селект max-width 480, hint ink-2, действия в ряд, no-caps).
- ChangeContacts: текст уточнён — контакты видны всем (раздел «Контакты
  кооператива» пайщикам + подвал сайта незарегистрированным), не только
  незарегистрированным.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 974fcb305a [design-wave1][@dark] feat: канон для остальных страниц стола председателя
Кооперативные участки, Регистрационные взносы, Ключ кооператива,
Провайдер платежей, Контакты кооператива:
- убран дублирующий заголовок страницы (есть крошка в шапке);
- описание перенесено с голого фона на канон-поверхность .banner;
- page-shell/hero/surface-card с хардкод-px → канон-отступы и токены;
- q-input/q-select standout=bg-teal → outlined color=primary.
Также удалён лишний хвост текста на Стартовых страницах.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops a9fa74fc8c [design-wave1][@dark] fix: убрать дубль заголовка на Стартовых страницах и Членах совета
Заголовок страницы уже показан крошкой в шапке — повтор h2 убран.
Пояснительный текст вынесен с голого фона на канон-поверхность (.banner
с инфо-иконкой). ApprovalsPage не трогаем — там заголовка страницы нет.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops a41550d14d [design-wave1][@dark] feat: канон для Запросов одобрений, Стартовых страниц, Членов совета
- ApprovalsPage: канон-отступы страницы, фильтр-селект teal standout → outlined.
- SystemSettingsPage: hero-card/surface-card с хардкод-px → канон page-head
  (h2 + sub на токенах) + q-card(flat); канон-отступы.
- MembersPage: тот же канон-паттерн вместо hero-card с хардкодами.
- DefaultPagesForm: 4× q-select standout=bg-teal → outlined color=primary.
Виджеты таблиц/состава совета без экзотики — канон-тема через quasar-canon.css.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 6d50758cc8 [design-wave1][@dark] fix: убрать Vue-warn о class в WorkspaceSwitcher
Шаблон двухкорневой (кнопка + Teleport затемнения), class из родителя
(LeftDrawerMenu) не наследовался автоматически. inheritAttrs:false +
v-bind=$attrs на корневую кнопку — атрибуты направлены явно.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 26b94c367c [design-wave1][@dark] style: согласовать типографику диалога повестки
Подписи секций «Вопрос на повестке»/«Проект решения» переведены в
надстрочные метки (uppercase, трекинг, ink-3) — отдельный ярус ниже
заголовка окна, чтобы не казаться конкурирующим заголовком рядом с
крупным заголовком самого документа. Текст вопроса — читаемый body.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 60d647c470 [design-wave1][@dark] fix: канон-отступы и чистка онбординга председателя
- ConnectPage: добавлены канон-отступы страницы (var(--p-6)/var(--p-4)) —
  класс .padding в проекте не определён, контент липнул к краям.
- OnboardingStepsCard: убран q-card (двойной паддинг), текст-интро
  растянут на полную ширину (снят max-width 70ch), вокруг кнопки шага
  убрана лишняя рамка (.step__content → .step__action, только отступ).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops d55b3e2fd9 [design-wave1][@dark] feat: онбординг председателя в канон (вертикальный степпер + текст + диалог повестки)
- OnboardingStepsCard: под заголовком «Адаптируйте кооператив…» добавлен
  поясняющий текст (что происходит дальше: решения совета в электронной
  форме, вступление в «Восход», импорт пайщиков, общее собрание) + плашка
  срока адаптации с отсчётом.
- Чек-лист шагов переведён на канон-вертикальный степпер (.stepper--v):
  пройденные — галочка/заливка primary, текущий — подсветка номера,
  действия — канон BaseButton; agenda/import/meet типы сохранены.
- Диалог «Предложение повестки» (объявить собрание совета) переведён
  с ModalBase на канон BaseDialog + BaseButton.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops e0a9d559a9 [design-wave1][@dark] feat: хлебные крошки в шапке + скрыта кнопка «Перевести»
- CommonHeader: крошка строится из route.matched (путь внутри стола),
  напр. «Отчётность › Календарь»; корень стола не выводится; плоские
  страницы дают одну крошку как прежде; pageTitleOverride сохранён.
- CoopWalletsPage: переводы временно скрыты под флагом transferEnabled=false
  (открывались с L3-кошельков пайщиков; вернём для кооп-кошельков позже).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:50 +00:00
coopops b5cfe40661 [design-wave1][@dark] style: канон-токены в редакторе отчёта (ReportEditorDialog)
- editor-container/action-panel/backdrop/validation-badge/mark-hint: hex и rgba → --p-* токены
- text-grey-8 → t-muted; box-shadow/overlay через токены с fallback

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:50 +00:00
coopops 180e22d07a [design-wave1][@dark] style: канон-токены на списочных страницах бухстола + cells
- Operations/Postings/Accounts: канон page-padding, убран двойной q-mt-md, text-grey-* → t-faint/t-muted, hex-границы → токены
- Settings: канон-padding, save-bar на токены (--p-surface/--p-line/--p-pos), text-grey-7 → t-muted
- AccountIdCell/WalletIdCell: text-grey-6 → t-faint; DirectionCell: move-иконка на --p-ink-3
- PROCESS_COLORS доменного цвет-кодирования операций сохранены (это фича)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:50 +00:00
coopops 34ac74f6a1 [design-wave1][@dark] style: канон-токены на столе бухгалтера — кошельки, календарь, отчётность
- CoopWalletsPage/ParticipantWalletsPage: rgba/var(--q-*) → канон-токены (--p-pos/--p-warn/--p-ink-*)
- ReportsCalendar/CalendarCell: статусы и грид на канон-палитру, убраны body--dark дубли
- Documents подстраницы: убран двойной padding, q-chip→BaseBadge, таблицы в канон-карточку

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:50 +00:00
coopops e23196d492 [design-wave1][@dark] feat: канон-меню второго уровня (SecondLevelTabs) на столе бухгалтера
- SecondLevelTabs на глобальных .tabbar/.tab — sub-навигация между топбаром и контентом
- WalletsPage/DocumentsPage shell-страницы: вкладки в .tabbar вместо RouteMenuButton в топбаре
- TransferWalletsButton: канон header-кнопка (q-btn primary)
- WalletTransferDialog: на BaseDialog + BaseButton, токены вместо rgba

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:33 +00:00
coopops 8206c9f358 [design-wave1][@dark] fix: канон-поля ZodForm и убран дубль заголовка «Настройки»
ZodForm: убран standout="bg-teal text-white" с динамического поля —
QInput/QSelect теперь канон (outlined+dense+color=primary+reserve-hint-space),
как BaseInput. SCSS на канон-токены (вместо неопределённого --q-primary-rgb).
ExtensionSettings: снят заголовок «Настройки» внутри карточки — он дублировал
«Настройки аренды» в шапке.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:33 +00:00
coopops 5e01c25ff8 [design-wave1][@dark] fix: фильтры лога на BaseInput вместо сырого q-input
Канон-инпут вместо q-input: BaseInput расширен поддержкой type="date"
и clearable (+ emit clear) — это нужно для фильтра по датам. Фильтры
в ExtensionLogsList переведены на BaseInput.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 957711e3bc [design-wave1][@dark] feat: настройки и лог расширения в канон
ExtensionSettings: EmptyState + BaseCard вокруг ZodForm вместо самописных
header/empty-state. ExtensionLogsList (используется только powerup):
q-table → список канон-карточек с фильтром по датам, скелетоном,
«Загрузить ещё» и EmptyState; слот #log-item и API сохранены.
Канон-падинги страниц настроек/лога аренды.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 2ee53bc7dd [design-wave1][@dark] feat: Стол вычислительных ресурсов в канон — раскладка, виджеты, лог
Монитор ресурсов: баннер-предупреждение на всю ширину сверху, ряд
«Как это работает» (2/3) + кошелёк AXON (1/3), ряд CPU/NET/RAM.
Убран 3D-флип карт (он же «прыгал») — детали раскрываются inline.
Все виджеты/страницы/лог сведены к канон-токенам surface/line/ink.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 2f0aad0015 [design-wave1][@dark] fix: отступы действий платежей и ширина даты в документах
- Реестр платежей: кнопки действий больше не block (не заполняют ячейку
  от левого края, не липнут к бейджу статуса). Стопка inline-flex,
  равная ширина кнопок (align-items:stretch), прижата к правому краю
  колонки (col-action 168px, text-align:right) — слева остаётся воздух.
- Реестр документов: col-date 116→156px — дата «26.05.2026 08:25» больше
  не налезает на заголовок документа; min-width 720→780.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops e986984dcc [design-wave1][@dark] fix: реестры платежей/документов и форма взноса — ширины и кнопки
- Bank.vue: панель платежа max-width 380px по центру — длинное «Назначение»
  больше не раздувает диалог «Совершите взнос» до max-content; значение
  переносится по строкам (overflow-wrap: anywhere).
- Кнопки статуса платежа SetOrderPaid/RefundedStatusButton → BaseButton
  (primary/danger, block), подписи «Подтвердить»/«Отклонить»; диалог
  вынесен из-под кнопки.
- Реестр платежей: кнопки действий друг под другом (.cell-actions),
  колонка действий 156px, дата без переноса (.col-date 132px), min-width
  таблицы уменьшен.
- Реестр документов: сужены колонки подписей (220→150), действий
  (110→56), id/дата; min-width 880→720 — освобождено место под
  наименование, таблица не вылазит за страницу.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 976b98dd3f [design-wave1][@dark] fix: реестры документов и платежей — канон-поля страницы
Таблицы документов и платежей уже на канон-токенах и самообрамлены
(.table-wrap). Не хватало полей страницы как в реестре пайщиков:
- PaymentsPage рендерил виджет голым, впритык к краям → q-page + 24/16px.
- ListOfDocumentsPage: q-page.padding (16px) → канон-24px.
- Убрана лишняя .row.justify-center обёртка в ListOfDocumentsWidget
  (.col-12 и так во всю ширину).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 7bd98a2a79 [design-wave1][@dark] fix: диалоги «Добавить» и «Импорт» пайщиков в канон
AddUserDialog перестроен в канон-визард по образцу CreateMeetForm:
шапка-bar (surface+line) вместо bg-gradient-dark, VerticalStepper из трёх
шагов — Электронная почта → Тип и данные → Вступительный взнос, BaseButton
в подвале, outlined-инпуты. UserDataForm переиспользован целиком (общий,
не тронут). Опция начисления взноса — канон-блок вместо q-item/q-card.

ParticipantsImportDialog: добавлено описание-интро (раньше отсутствовало),
ModalBase/bg-gradient-dark заменён на канон-bar, выбор типа аккаунтов через
BaseRadioCard, dropzone на токенах вместо хардкод-цветов, секции и действия
в канон-обёртках, BaseButton вместо цветных q-btn. Логика парсинга/импорта
и таблицы превью/результатов сохранены.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 52ca724508 [design-wave1][@dark] fix: реестр пайщиков — канон-поля и дыхание вместо встык-вёрстки
Страница получила канон-отступ (--p-6 / --p-4 на мобилке), таблица обёрнута
в обрамлённую surface-карточку (--p-line, --p-r-lg) вместо edge-to-edge.
Развёрнутые данные пайщика — спокойная вложенная панель на --p-surface-2
с отступами --p-5/--p-6, читаемой шириной формы (640px) и вертикальным
ритмом между полями. Снят full-height у таблицы (мешал внутри карточки).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 5c0a08bce8 [design-wave1][@dark] refactor: реестр пайщиков в канон + убран переключатель Данные/Документы
Развёрнутая строка пайщика теперь показывает только его данные;
документы пайщика живут в отдельном «Реестре документов» и здесь не дублируются.
ParticipantDetails упрощён (без q-tabs/ListOfDocumentsWidget), tab-логика
вычищена из таблицы и страницы. Мобильная ParticipantCard переведена на канон-токены
(убраны хардкод-цвета, CardStyles и q-badge статуса → disabled-checkbox).
Таблица сохранена, сортировка по дате вступления (DESC) без изменений.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 9d27acb45a [design-wave1][@dark] fix: повестка — номер вопроса на зелёной плашке вместо иконки, клик копирует
Убран EntityIdBadge. Зелёная плашка-аватар слева теперь несёт сам номер вопроса
(он же Decision ID): клик по плашке копирует id (тултип «Скопировать №»), hover —
инверсия в сплошной teal. Заголовок снова просто текст вопроса.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops e643020980 [design-wave1][@dark] fix: повестка — бейдж ID в строку заголовка перед текстом вопроса
EntityIdBadge перенесён из отдельной строки под ФИО в начало строки заголовка:
[#id] → сразу текст вопроса. Inline, vertical-align middle. Раньше висел снизу
и пусто растягивал верхнюю строку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 2221552c32 [design-wave1][@dark] feat: повестка — копируемый бейдж Decision ID под ФИО
EntityIdBadge с id вопроса под заголовком/ФИО: отображает #<id>, по клику копирует
чистый id (copy-on-click). Канон-компонент, клик не раскрывает карточку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 3ef7bd1f92 [design-wave1][@dark] fix: повестка — срок «Истекает …» в нижнюю полоску рядом с «Утвердить»
«Истекает через месяц» перенесён из строки под заголовком в постоянную нижнюю
полоску (footer): срок слева, «Утвердить» справа (только председателю). У обычного
пайщика — только срок, узкая панелька.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 8beab9ab0a [design-wave1][@dark] fix: повестка — «Истекает …» и убран избыточный бейдж статуса
Срок теперь подписан «Истекает через месяц» (было просто «через месяц» — неясно).
Удалён статус-чип «Вы за/против» — направление голоса уже видно по подсветке кнопок,
бейдж был избыточным. Убраны связанные computeds и стили.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 16884c9b1c [design-wave1][@dark] fix: повестка — переосмысленная компоновка карточки вопроса
Кнопки голосования прижаты к правому краю строки. «Утвердить» вынесена вниз
отдельной строкой-footer (справа, hairline сверху) — больше не теснит данные.
Срок «через месяц» и статус-чип «Вы за» перенесены влево под заголовок/ФИО.
Клик по строке раскрывает документ, по кнопкам голосования и footer — нет.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops b0a200a191 [design-wave1][@dark] fix: повестка — органы управления в одну строку с инфо, не остров по центру
Карточка вопроса перестроена в единую горизонтальную строку: иконка + вопрос/ФИО
слева (flex), компактный блок органов управления (голосование + «Утвердить»),
срок + статус-чип и шеврон — справа, друг рядом с другом. Убрана центрированная
vote-зона с пустотой по бокам. Клик по строке раскрывает документ, по органам
управления (@click.stop) — нет. Подсказка «Утвердить» возвращена в tooltip.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops e1f2a0cb7d [design-wave1][@dark] fix: повестка — раскрытие документа по клику на шапку, не на органы управления
Шапка карточки (вопрос/ФИО/срок/статус) кликабельна целиком и раскрывает документ,
справа шеврон-индикатор. Органы управления голосованием — отдельная зона ниже шапки
(сиблинг, не аккордеон): клик по ним голосует/утверждает и документ не раскрывает.
Убрана отдельная кнопка «Документ» — раскрытие теперь по клику на шапку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 23811fa3e7 [design-wave1][@dark] fix: голосование повестки на верхнем уровне + header-CTA совета
QuestionCard: VotingButtons + «Утвердить» вынесены на верхний уровень карточки —
голосовать можно без раскрытия; раскрытие («Документ») открывает только содержимое
документа. Истечение срока остаётся в шапке.
VotingButtons: вернул чек-индикатор «принято советом» (закрашивается при принятии)
вместо иконки verified.
Header-CTA совета (Предложить/Добавить/Импорт): убран push (q-btn--push не попадал
под канон-правило заливки primary → белый шрифт на нетиловом фоне), подписи с заглавной.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 702d395f6c [design-wave1][@dark] feat: повестка совета в канон — единый карточный список
QuestionsTable: q-table → карточный список (скелетоны + EmptyState).
QuestionCard: канон-поверхность, BaseButton «Утвердить», токен-чип статуса
вместо q-badge, локальное состояние раскрытия, без @import CardStyles и хардкод-hex.
VotingButtons: токены pos/neg вместо .text-red/.text-green/#666.
Страница: канон-padding вместо q-card flat.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 1f74367a5a [design-wave1][@dark] fix: единый регистр названий столов в переключателе
Названия столов в реестре workspace'ов заведены вразнобой («Стол
благороста», «Стол вычислительных ресурсов» vs «Стол Совета»).
Добавил text-transform: capitalize на подписи в WorkspaceSwitcher
(заголовок текущего стола + пункты выпадающего меню) и WorkspaceMenu
(карусель + диалог выбора) — первая буква каждого слова заглавная,
одинаково для всех и для будущих столов, без правки строк-источников.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 70a6d9010d [design-wave1][@dark] fix: «Контакты» — единая сетка полей вместо сегментов
По фидбеку переработана композиция:
- убраны заголовки секций «Контакты»/«Руководство» и per-строчные
  иконки телефона/почты/адреса (из-за них значения «прыгали» вправо
  относительно ИНН/ОГРН);
- ИНН и ОГРН теперь в одну строку (адаптивная сетка полей);
- председатель поднят наверх к реквизитам, поле названо просто
  «Председатель совета»;
- контакты — те же поля, телефон/email как ссылки (tel/mailto), всё на
  единой левой кромке; одна тонкая линия делит реквизиты и контакты.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops fbaca1ba9f [design-wave1][@dark] fix: «Контакты» — единая раскладка строк во всех секциях
Реквизиты/Руководство больше не разносят подпись и значение к
противоположным краям (на полной ширине это давало пустой провал и
оторванные значения). Все секции теперь как ContactSheet: подпись
сверху, значение под ней, слева, hairline между строками — один ритм
по всей карточке.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 5ddc78b535 [design-wave1][@dark] feat: страница «Контакты» в канон + удалён мёртвый MeetQuorumIndicator
- ContactsPage: три цветные ColorCard (orange/indigo/teal/blue) заменены
  на единую спокойную canon-поверхность с секциями через hairline
  (Реквизиты / Контакты / Руководство). Контакты — через canon
  ContactSheet (копирование, mailto/tel). Заголовок и строки на токенах
  (--p-ink/-2/-3, --p-surface/--p-line/--p-r-lg), полная ширина (padding
  24/16px). Убраны rgba-фоны и .q-dark-хаки.
- Удалён неиспользуемый виджет MeetQuorumIndicator (нет импортов;
  явка/кворум живут в MeetInfoCard).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 837d529ddb [design-wave1][@dark] feat: тело деталей собрания в канон — повестка/голосование/результаты
Убрал токсичный teal из state-gated виджетов деталей собрания:
- MeetDetailsAgenda/Voting/Results переписаны на canon-поверхности
  (--p-surface/--p-line/--p-r-lg), section-heading с иконкой вместо
  центрированной градиентной линии; color-mix/--q-primary/.q-dark удалены.
- Голосование: q-btn color=primary → BaseButton variant=primary;
  опции голоса на токенах pos/neg/neutral-soft, баннер «уже голосовали»
  на --p-pos-soft.
- Результаты: итог решения — токен-чип (pos/neg/unknown), счётчики
  голосов на pos/neg/surface-2-soft.
- AgendaNumberAvatar: teal q-avatar → нейтральный квадрат surface-2/ink
  (общий для трёх виджетов — убирает teal сразу везде).
- MeetCardsList: отступ между карточками 16→20px.
- MeetDetailsPage: кастомный empty-state → canon EmptyState, снят
  @import CardStyles.

MeetQuorumIndicator оставлен как есть — мёртвый код (нет использований).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops ba6464c397 [E14][@dark-sun] feat(собрания): полная ширина страниц; тело деталей (MeetInfoCard) на canon
- ListOfMeetsPage / MeetDetailsPage: убран max-width:960px center — контент
  во всю ширину с canon-паддингом (24px/16px), как документы/платежи.
- MeetInfoCard: переписан с токсичного teal на canon — единая calm-поверхность
  с тремя секциями через hairline (Даты / Ведущие / Явка и кворум) вместо
  трёх вложенных цветных карточек; заголовки ink, проценты ink (не teal);
  дубль заголовка «Общее собрание № N» убран (он в шапке). Убраны
  var(--q-primary), color-mix, .body--dark, хардкод-rgba.
- MeetDetailsInfo: контейнер с .card-container → canon.
- MeetDetailsActions: q-btn color=primary → canon BaseButton.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 225b4abc0e [E14][@dark-sun] feat(собрания): повестка на шаге проверки канон; карточка списка канон; шапка деталей — back-link вниз, имя в заголовок
- CreateMeetForm: повестка на шаге «Проверка» — аккуратные canon-карточки
  (Вопрос / Проект решения / Приложения) вместо скомканного списка.
- MeetCompactCard: переписана на canon — нейтральный hover (без
  токсичного teal-свечения), ink-заголовок, canon-плитки/иконка;
  убраны @extend .card-container, var(--q-primary), .body--dark, color-mix.
- MeetStatusBanner: нейтральный canon-контейнер (surface-2 + line),
  цвет статуса несёт иконка; убраны хардкод-rgba и .body--dark.
- MeetCardsList: empty-state на canon EmptyState, skeleton на .skel.
- Детали собрания: кнопка «Назад» убрана из топбара (снят useBackButton),
  добавлен canon back-link под шапкой слева; название собрания выводится
  в заголовок шапки через новый desktopStore.pageTitleOverride
  (приоритет над route.meta.title; транзиентно, чистится при уходе).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops ddc66d8529 [E14][@dark-sun] feat(собрания): форма созыва на canon-степпер; кнопка в шапке к канону; скрыта «Сверить»
- BaseDocument: кнопка «Сверить» временно скрыта через :hide-verify
  (обработчик @verify остаётся, вернуть = убрать флаг).
- CreateMeetButton: приведена к одобренному canon-паттерну (primary
  solid на десктопе, micro flat accent + tooltip на мобильном),
  убран legacy push/teal с белым текстом.
- ListOfMeetsPage: инжект кнопки переведён с useHeaderActions на
  Teleport #header-actions-host.
- CreateMeetForm: полностью переписана с перегруженной тёмной модалки
  на canon-визард — maximized-диалог + VerticalStepper (3 шага:
  Параметры → Повестка → Проверка). Canon outlined-поля, повестка
  аккуратными карточками, шаг проверки со сводкой. Убраны
  bg-gradient-dark / standout bg-teal / .body--dark / хардкод rgba.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 5d007b987d [E14][@dark-sun] feat(стол пайщика): скелетоны вместо спиннера в таблицах документов и платежей
Добавлен canon-компонент TableSkeleton (shared/ui/base) — повторяет
структуру .table-wrap/.table с реальными заголовками и мерцающими
плейсхолдерами (.skel) в ячейках. Каркас не дёргается при подгрузке
данных (calm-data, UX-DR2/UX-DR23). Заменил перекрывающий q-spinner
в DocumentsTable и ListOfPaymentsWidget.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops c807eb0ef8 [E14][@dark-sun] feat(стол пайщика): документы — ID копируемым бейджем, отдельные колонки Дата/Документ, подписи бейджами; поиск минимальный на мобильном
- DocumentsTable: наименование берём из meta.title (чистое, без даты/.pdf
  суффикса full_title); отдельная колонка Дата (meta.created_at) с сортировкой
  по block_num, по умолчанию свежие сверху; колонка Документ — только заголовок.
- ID — копируемый EntityIdBadge (показывает короткий хеш, копирует полный
  doc_hash по клику + иконка-affordance).
- Подписи — отдельные BaseBadge на подписанта (новый helper
  getSignersListFromDocumentPackage возвращает массив).
- EntityIdBadge канонизирован: токены вместо rgba/--q-accent/.q-dark; добавлен
  copyValue (показать одно — скопировать другое).
- SearchHeaderAction: на мобильном — минимальная round-dense иконка без подписи
  «Поиск» (label только на десктопе).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 37d6a761db [E14][@dark-sun] feat(стол пайщика): документы и платежи — канон-таблица .table-wrap вместо q-table
Переделка после ревью: q-table давал не-канон вид, дёрганье при раскрытии
(virtual-scroll пересчитывал размеры + colspan не совпадал с числом колонок)
и уродливые мобильные карточки в grid-режиме.

- Статическая канон-таблица .table-wrap/.table (бордер+радиус+surface как
  карточка), table-layout:fixed → колонки не разъезжаются при раскрытии.
- Раскрытие строки — CSS-only (expand-row td colspan по числу колонок),
  без virtual-scroll → нет дёрганья шапки/колонок.
- Пагинация load-more (.table-foot + BaseButton «Загрузить ещё» + «1–N из M»)
  вместо infinite virtual-scroll.
- Мобильный: горизонтальный скролл таблицы (.table-scroll) вместо grid-карточек
  PaymentCard/DocumentCard — карточки удалены.
- Статусы платежей — BaseBadge (pos/warn/neg/info/neutral); направление —
  иконка + цвет --p-pos/--p-neg; хеш документа — mono.
- Действия платежей (SetOrderPaid/Refunded) скрыты на столе пайщика
  (hideActions=true) — они в реестре платежей; download документов сохранён.
- EmptyState + спиннер первой загрузки.

Виджеты общие с админ-контуром: load-more и горизонтальный скролл там тоже
уместнее jittery virtual-scroll.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops e5bf464a4d [E14][@dark-sun] feat(стол пайщика): документы и платежи в канон — статус-бейджи, токены, header-action на Teleport
- ListOfPaymentsWidget + PaymentCard: статус платежа через BaseBadge
  (canon-варианты pos/warn/neg/info/neutral) вместо хардкод q-color
  teal/orange/red/blue; убраны битый inline-стиль $toolbar-min-height,
  inline max-width, глобальный .q-list style-leak; цвета/тени/радиусы —
  на токены (--p-line/--p-ink-3/--p-shadow-card/--p-surface-2).
- DocumentsTable + DocumentCard: убраны битый inline-height, inline
  max-width, .q-dark-селекторы, --q-gray, хардкод rgba; тени/линии — токены;
  глобальный q-table__top style → scoped :deep; CardStyles import убран.
- ListOfDocumentsWidget: brand-leak toggle-color teal → primary.
- DocumentsPage: SearchHeaderAction со старого useHeaderActions store на
  canon Teleport (#header-actions-host), кнопка сама гейтится по features.search.
- DocumentsPage/PaymentsPage: канон-паддинг страницы.

q-table-движок (virtual-scroll/infinite-load/раскрытие строк/mobile-grid)
сохранён — виджеты общие с админ/председательскими контурами, регресс недопустим.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 94d09d482d [E14][@dark-sun] fix(шапка): правая группа прилипала влево на страницах без действий — вернул margin-left:auto
Когда страница не телепортирует действия, .topbar__actions скрывается
display:none через :has(пустой host), но adjacent-селектор
.topbar__actions + .topbar__right всё равно матчился и обнулял margin-left
правой группы — она уезжала влево к крошке. Возвращаем auto в :has-правиле
(его специфичность выше). Также убрал лид-надпись на странице реквизитов.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops e125fb3d56 [E14][@dark-sun] feat(стол пайщика): удостоверение и реквизиты в канон — IdentityPanel/DataRow/BaseCard, header-action на Teleport
- ProfilePage (Удостоверение): IdentityPanel-шапка + BaseCard-секции (Учётная
  запись/Личные данные/Документы и реквизиты) на DataRow; убран CardStyles,
  хардкод rgba и .q-dark.
- PaymentMethods widget (Реквизиты): BaseCard на метод + DataRow + EmptyState
  вместо .info-label/.info-value и хардкод-цветов.
- PaymentMethodsPage: кнопка добавления реквизитов переведена с useHeaderActions
  store на canon Teleport (#header-actions-host), micro на мобильном.
- AddPaymentButton: триггер под canon micro-паттерн (как Deposit/WithdrawButton).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
claude 0acd4aa872 fix: remove stray node_modules symlink committed into branch
Симлинк node_modules -> /home/admin/mono-ai-2/node_modules был случайно
закоммичен и ломал pnpm install (ENOTDIR) на любой машине без этого пути.
Правило .gitignore 'node_modules/' (со слешем) ловит только каталог, не
симлинк-файл — добавлено правило 'node_modules' без слеша.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops 5d490913b2 [E14][@dark-sun] feat(шапка): крошка-название страницы слева + фикс переноса заголовка ModalBase
Крошка: в #crumb рядом с BackButton выводится route.meta.title текущей
страницы (тот же, что подсвечен в меню) — именно для неё действия и
сдвинуты вправо. Длинное название обрезается ellipsis в .topbar__crumb b.

ModalBase: q-bar получил авто-высоту, заголовок переносится (white-space
normal + overflow-wrap), крестик прижат к верху — на узком экране титул
больше не обрезается сверху.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops a3942d696c [E14][@dark-sun] fix(WalletPage): micro-кнопки взноса/возврата в шапке на мобильном
Why: полноразмерные DepositButton/WithdrawButton в узкой мобильной шапке
раздувались — текст переносился в 2 строки, кнопки вылезали за высоту
topbar. micro-вариант (иконка + tooltip, flat/dense) и предназначен
для слота шапки.

What: Teleport-кнопки получают :micro='isMobile' (useWindowSize, <768px).
Мобильный — компактные иконки; десктоп — полные кнопки с подписью.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops 5784c20aa7 [E14][@dark-sun] fix(WithdrawButton): «получить возврат» → «Получить возврат» — заглавная
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops ceac5aee31 [E14][@dark-sun] feat(стол пайщика): кошельки списком во всю ширину + действия шапки через Teleport
Кошельки (по фидбэку — переносы выглядят плохо):
- .wallet-programs из grid (repeat auto-fill minmax) → flex-column: карточки
  идут списком во всю ширину страницы.
- canon .wallet__title/__sub: возвращён nowrap + ellipsis (откат wrap-фикса);
  на широкой строке текст почти всегда влезает, иначе — ellipsis.
- WalletCard + минимум-карточка: нативный tooltip `title` — при наведении
  виден полный текст. Убран .wallet--row reset (базовый снова nowrap).

Действия шапки (кнопки взноса/возврата пропали; нужна новая механика):
- Новый canon-механизм: страница телепортирует свои действия в шапку через
  <Teleport to="#header-actions-host">. Host — постоянный span с
  display:contents в #actions слоте CommonHeader (при loggedIn).
- :has()-правило прячет .topbar__actions, когда внутри только пустой host
  (нет ни store-кнопок, ни телепорта) — чтобы не было пустого разделителя.
- WalletPage переведён на Teleport (DepositButton/WithdrawButton),
  useHeaderActions store-механизм убран со страницы.
- Старый useHeaderActions оставлен для прочих 10 страниц — мигрируем
  и удалим отдельно. DepositButton сам скрыт, пока пайщик не принят
  (status !== 'active') — это защита, не баг.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops 028eb31c03 [E11][@dark-sun] fix(CommonHeader): бренд-логотип inline-SVG в зелёном квадрате, как в личном кабинете
Why: на странице без логина логотип был <img :src='logo.svg'> — img рендерит
SVG в изоляции, не наследует currentColor, поэтому показывался чёрно-белым
и не реагировал на смену темы. В личном кабинете (WorkspaceSwitcher)
тот же logo.svg рендерится inline через v-html в зелёном квадрате
и наследует color (logo.svg на fill:currentColor) — «зелёненький
на зелёном фоне», одинаковый в обеих темах.

What: CommonHeader brand-slot переведён на тот же приём —
`logo.svg?raw` + v-html внутри .app-q-header__logo
(background var(--p-primary-soft), color var(--p-primary), 28px квадрат,
16px svg). Переключение темы больше не требуется — зелёный константен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops fc60c58f51 [E14][@dark-sun] fix(canon .wallet): просто wrap по пробелам, без overflow-wrap: anywhere
Why: предыдущий фикс `overflow-wrap: anywhere; word-break: break-word`
ломал слова в любом месте даже когда колонка достаточно широкая —
«Минимальный неснижаемый остаток» рендерился по одному слову на строку,
несмотря на ~480px доступной ширины. Реально нужен только wrap по пробелам;
agressive break-word оправдан только для URL-подобных нерасчленяемых строк.

What: убраны `overflow-wrap` и `word-break` из .wallet__title/__sub —
браузер делает естественный wrap по пробелам, длинные заголовки переносятся
только когда не помещаются. `.wallet--row` reset для compact-варианта
оставлен — там по-прежнему single-line+ellipsis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops 6a2a56dcfa [E14][@dark-sun] fix(canon .wallet): разрешить перенос длинных заголовков на full-варианте + порядок карточек в WalletProgramWidget
Why: canon-стиль .wallet__title и .wallet__sub был с
`white-space: nowrap; overflow: hidden; text-overflow: ellipsis;` — на full-
варианте это резало длинные подписи («Минимальны…», «Возвращается п…»)
даже на просторных экранах, потому что grid-колонка `.wallet__main` сжимается
ради `.wallet__amount` справа. Эта обрезка будет всплывать на любой
длинной строке (метки программ, статусы пайщика, длинные subtitle).
Compact-вариант `.wallet--row` (слот шапки) должен остаться одной строкой.

What:
- .wallet__title/__sub: убран nowrap/ellipsis; добавлено
  `overflow-wrap: anywhere; word-break: break-word; hyphens: auto` (для title)
  и `overflow-wrap: anywhere; word-break: break-word` (для sub) — длинные
  заголовки переносятся на 2+ строки.
- .wallet--row: явно возвращает nowrap+ellipsis (с reset word-break/
  overflow-wrap), чтобы compact-ряды в шапке оставались строго одной строкой.
- WalletProgramWidget: карточка минимального неснижаемого остатка
  перемещена в начало grid'а (была после программ) — это базовая защита
  средств пайщика, логичнее видеть её первой.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops a8013c7414 [E14][@dark-sun] refactor(WalletCard,WalletProgramWidget): «Главный кошелёк» канон-default + минимальный остаток отдельной карточкой
Why:
- Заголовок «Кошелёк» для program='wallet' путал — в столе пайщика этот
  кошелёк семантически главный, и в любых других местах canon (MicroWallet,
  WalletCardMini, _dev/ui) он тоже должен называться полно — «Главный
  кошелёк». Лучше один canon-default, чем локальный override в каждом
  потребителе. Согласовано — переименование canon DEFAULT_TITLES.
- Минимальный неснижаемый остаток — НЕ баланс кошелька, а самостоятельная
  сущность пайщика (паевой взнос, возвращается при выходе). Пристегивать
  его DataRow-строкой под grid'ом — нелогично, как было и раньше в legacy.
  Правильнее — отдельной карточкой в той же сетке кошельков.

What:
- WalletCard.vue: DEFAULT_TITLES.wallet 'Кошелёк' → 'Главный кошелёк'.
  Глобально для всех потребителей canon-компонента.
- WalletProgramWidget.vue: убран TITLE_OVERRIDE и DataRow-блок минимального
  остатка. Карточка остатка теперь рендерится в общем .wallet-programs
  grid'е canon-разметкой `.wallet` с иконкой `savings`, заголовком
  «Минимальный неснижаемый остаток», подзаголовком «Возвращается при выходе
  из кооператива» и зарезервированной суммой. Нейтральная подсветка иконки
  через `.wallet--minimum { --prog-bg, --prog-fg }`, чтобы визуально
  отличалась от программ, но встала в общую сетку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops 0f475e7dbd [E14][@dark-sun] feat(WalletProgramWidget): «Главный кошелёк» + минимальный неснижаемый остаток
Why: на старом WalletWidget пайщик видел свой минимальный неснижаемый остаток
(паевой взнос, возвращается при выходе из кооператива) — самостоятельная
сущность пайщика, не баланс кошелька. При переходе на canon я её упустил.
Канон-default WalletCard 'wallet' = «Кошелёк» — для стола пайщика этот
кошелёк семантически является главным (свободный остаток ЦК), поэтому
локально перекрываем заголовок на «Главный кошелёк».

What:
- TITLE_OVERRIDE['wallet'] = 'Главный кошелёк' — локальное перекрытие
  заголовка только в этом widget'е, canon DEFAULT_TITLES не трогаем
  (другие потребители WalletCard могут использовать общий «Кошелёк»).
- DataRow «Минимальный неснижаемый остаток» под grid'ом программ
  (только когда session.participantAccount.minimum_amount > 0)
  с hint «Возвращается пайщику при выходе из кооператива».
- Источник остатка — session.participantAccount?.minimum_amount,
  как и в legacy WalletWidget.

Note: locked-line уже рендерится самим canon WalletCard, когда
locked-balance > 0 (logic: hasBlocked ? blocked.amount : undefined
в WalletProgramWidget). Отдельная разметка под «Заблокировано» не нужна.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops c824b2147e [E14][@dark-sun] fix(WalletProgramWidget): vue-tsc — flatMap вместо filter+predicate
Why: type predicate `e is CanonProgramEntry` не сходился — optional `locked?: string`
в interface vs required `locked: string | undefined` в литерале map'а. TS считает
эти типы несовместимыми для predicate, хотя они эквивалентны при присваивании.

What: переход на `flatMap<CanonProgramEntry>` с возвратом `[]` для исключаемых
программ. Predicate не нужен, generic flatMap даёт точный тип результата.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:29 +00:00
coopops df13f30788 [E14][@dark-sun] refactor(participant/wallet): WalletPage + WalletProgramWidget в канон MONO Platform v2
Why: первая страница стола пайщика (default route 'wallet') использовала legacy
обвязку — CardStyles import, scoped SCSS с .body--dark и hardcoded rgba цветами,
ColorCard с произвольным цветом по индексу. Канон уже знает программы платформы
(blagorost/wallet/generator) через WalletCard + токены --prog-*; на нём и строим.

What:
- WalletPage.vue: убран import 'src/shared/ui/CardStyles', scoped SCSS на
  токенах --p-6/--p-4; вырезана легаси-карточка «Минимальный остаток»
  (она не отображалась — не было разметки в template). useHeaderActions
  для Deposit/Withdraw оставлен — канон поддерживает actions через #actions slot.
- WalletProgramWidget.vue: переписан на canon WalletCard + EmptyState.
  Фильтр на канон-набор программ через ZEUS_TO_CANON
  (MAIN→wallet, BLAGOROST→blagorost, GENERATOR→generator);
  MARKETPLACE и прочие исключены — это не из основной тройки платформы
  и должны рендериться отдельным виджетом.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:29 +00:00
coopops 077e716407 [E11][@dark-sun] feat(AppHeader): brand slot — лого + название кооператива на странице без логина
Why: на главную без логина нужно показывать бренд кооператива (логотип + имя)
рядом с глобальными действиями шапки; раньше выводилось только текстовое
название через title-проп без логотипа.

What:
- AppHeader.vue: опциональный slot #brand перед .topbar__crumb;
  hasBrand computed по slots.brand. Когда slot заполнен — крошка не рендерится.
- components.css: стили .topbar__brand (desktop + .topbar--mobile вариант)
  с canon-токенами (--p-fs-body, --p-ink, --p-fs-meta).
- CommonHeader.vue: на !loggedIn заполняет #brand src/assets/logo.svg
  + <b>{coopTitle}</b>; передача title-пропа убрана.
- default.vue: .fixed-top-right { top: 51px } → top: var(--p-topbar-h)
  (canon 56px) — выравнивание FAB под точную высоту шапки.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:08 +00:00
coopops 5b24a78237 [E11][@dark-sun] refactor(CommandPalette): иерархия рабочих столов и страниц вместо generic pages/actions/recent
Существующая модель cmdk в проекте (entities/CmdkMenu/model/store.ts) —
иерархия рабочих столов и их страниц. Активный стол sticky сверху с
бейджем «Активный», без запроса показывается иерархия (стол + indented
страницы), с запросом — плоский список со столом-префиксом у каждой
страницы; стол отдельной строкой появляется только если запрос явно
начинается с его имени или содержит «стол»/«workspace».

Переписал canon CommandPalette под эту модель:
- Props: `workspaces: CommandPaletteWorkspace[]` вместо
  `commands: CommandItem[]`. Каждый workspace = `{ name, title, icon,
  isActive?, pages: CommandPalettePage[] }`. Page = `{ name, title,
  icon?, shortcut? }`.
- Emits: `select-workspace(name)`, `select-page(workspaceName, pageName)`
  — props-only, навигация и filtering по ролям/conditions остаются
  заботой connected-обёртки (миграция legacy CmdkMenu — отдельная story).
- Sticky-баннер активного стола, plus accent-soft фон + outline-обводка
  на selected, ↑↓ работает плоско поверх иерархии (стол → страницы → стол
  → страницы).

Mock-data в /_dev/ui/index.vue обновлён: три стола (Председатель/Пайщик/
Отчётность) со своими страницами вместо плоского списка команд.

Старый widgets/Desktop/CmdkMenu пока живёт параллельно — переключим в
ходе миграции Wave 3.
2026-05-27 09:17:08 +00:00
coopops 459888fea7 [E11][@dark-sun] feat: NotificationCenter, CommandPalette, DetailsDrawer — эпик «Навигация и нотификации»
Реализованы три props-only доменных компонента из E11:
- NotificationCenter — panel-content для popover в шапке: группировка
  notifications по category (system/financial/voting/message), unread-bullet
  через BaseBadge, кнопка «Прочитать все», EmptyState и «Показать все».
  Relative-date форматирование с русским склонением.
- CommandPalette — ⌘K/Ctrl+K с fuzzy-поиском, секции recent/pages/actions,
  ↑↓ навигация, Enter/Esc обработка. localStorage недавних — в connected
  обёртке, компонент props-only.
- DetailsDrawer — side-sheet справа 480px (override через :width), slots
  default/actions/footer, Esc и backdrop close, на xs — fullscreen.

Все три зарегистрированы в boot/ui.ts и локально импортированы в /_dev/ui
с mock-data в секциях 36-38.

E11.4 RailUserCard отложен из-за конфликта имён — существующий компонент
имеет другую роль; нужно согласовать канон-нейминг.
2026-05-27 09:17:08 +00:00
coopops 4c6a21a965 [E10][@dark-sun] fix(BaseDocument): центрировать spinner-лоадер по обеим осям
Заменил `.full-width.text-center` + flex без выравнивания на flex-column
со `min-height: 360px` — лоадер больше не прижат к левому верху карточки.
Заодно поправил опечатку «подговка» → «Формируем документ».
2026-05-27 09:17:08 +00:00
coopops 039ad58055 [E10][@dark-sun] fix(AmountInput): нативный :suffix='symbol' вместо #append slot
Корень: я положил символ валюты в #append slot своим <span>, в обход
встроенного prop suffix. Quasar имеет правила позиционирования именно
для родного .q-field__suffix (см. revert d0a8dc0080 от 2026-05-19,
где было решено оставить Quasar дефолт — «нас устраивает»). Мой span
в append-slot не подчинялся этим правилам и сидел в произвольной
позиции относительно цифр.

Фикс:
- :suffix='symbol' — символ валюты идёт нативным механизмом Quasar.
- Удалил .amount-input__symbol класс и template #append вообще.
- Удалил override align-items: center на q-field__control/__append/
  __after (тоже мешал, как было показано в d0a8dc0080).
- font-weight 500 на цифрах оставил — нормальный вес поля.

Совет на будущее: использовать встроенные q-input props (suffix,
prefix), а не #append/#prepend slots, если можно — Quasar для них
держит готовое выравнивание, проверенное пользователем.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:08 +00:00
coopops dac113f9f0 [E10][@dark-sun] fix(AmountInput): выровнять цифры и RUB-суффикс по центру
Корень: font-weight: 600 + tabular-nums + Quasar dense нативный input
имеют чуть смещённую baseline относительно append-слота, где сидит
RUB. Визуально цифры лежали ниже суффикса.

Фикс:
- font-weight 600 → 500 (нормальный вес поля ввода, без bold-акцента
  на цифрах).
- Явный font-size + line-height на нативном input.
- align-items: center на q-field__control / __append / __after, чтобы
  суффикс и any after-слот (кнопка «макс») центрировались по высоте
  входной полосы.
- align-self: center на самом __symbol — на случай если q-field__append
  кто-то переопределит как stretch.

TODO на следующий подход: BaseDocument loader («Формируем документ…»)
выравнивать по центру рамки документа (сейчас стоит сверху).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:08 +00:00
coopops 7425c66e2a [E10][@dark-sun] fix(OtpInput): убрать TypeScript-касты as из pug-шаблона
[Quasar] boot error: SyntaxError: Unexpected identifier 'as' — runtime-парсер
обрабатывает выражения в pug-template как чистый JS, без TypeScript. Касты
вида `el as HTMLInputElement | null`, `e as InputEvent`, `e as KeyboardEvent`
прямо в атрибутах `:ref` / `@input` / `@keydown` — синтаксическая ошибка во
время бутстрапа Vue, из-за которой /_dev/ui целиком не грузился.

Фикс:
- :ref='(el) => setRef(idx, el)' + функция setRef(idx, el: Element |
  ComponentPublicInstance | null) с кастом в TS-скрипте.
- @input='(e) => onInput(idx, e)' + сигнатура onInput(idx, event: Event)
  с внутренним кастом target.
- @keydown оставил передавать event «как есть» — KeyboardEvent — никакой
  cast не нужен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:08 +00:00
coopops 88a8f54806 [E10][@dark-sun] fix(_dev/ui): локальный импорт E10-компонентов — boot/ui.ts не HMR'ится
Quasar boot-файлы подхватываются только при рестарте dev-сервера. После
коммита 1af0f9c06f глобальные регистрации AmountInput/OtpInput/FilterBar/
FileUploader/VerticalStepper не подтянулись на лету — теги рендерились как
unknown components (пусто внутри секций 31–35 на /_dev/ui).

Фикс: добавил локальные импорты прямо в script setup _dev/ui/index.vue —
тот же паттерн, что у WalletCard, RailUserCard, AuthCard. Глобальная
регистрация в boot/ui.ts остаётся для боевого использования из других
страниц (после следующего рестарта dev'а).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:08 +00:00
coopops 378aa664a4 [E10][@dark-sun] feat: AmountInput, OtpInput, FilterBar, FileUploader, VerticalStepper — эпик «Формы и ввод»
Пять props-only доменных компонентов из shared/ui/domain/:
- AmountInput — денежный ввод с символом валюты, форматированием тысячных,
  precision из marketplace asset config, кнопкой «макс» по balance, опциональной
  подписью баланса. Tabular-nums, right-align, font-weight 600.
- OtpInput — 6 ячеек с автопереходом фокуса, Backspace откатывает на
  предыдущую, paste 6-значного кода распределяется по ячейкам.
  Регулярка /^\d$/, состояния error/disabled.
- FilterBar — search (debounce 300мс) + dropdown-фильтры + chip'ы активных
  значений с remove + «сбросить всё». v-model для values, v-model:search для
  поиска. Активные chip'ы рендерятся под рядом фильтров.
- FileUploader — drag&drop + клик по зоне; валидация accept/maxSize/maxFiles
  → emit error; список загруженных с иконкой типа, именем, размером,
  кнопкой ×; слот progress для connected-обёртки.
- VerticalStepper — состояния pending/current/completed/error; completed
  кликабельны для возврата назад (опционально); опциональные/disabled шаги;
  слот active под телом текущего шага.

Все компоненты — pug, canon-токены --p-*, без store/router/api. Демо-секции
31–35 на /_dev/ui (наш стенд-витрина).

Зарегистрированы в boot/ui.ts и shared/ui/domain/index.ts. ESLint точечно
прошёл, vue-tsc не запускался полностью на desktop (запрет).

Wave 2 / E10 закрыт.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:08 +00:00
coopops 211c17f47c fix(DocumentPreview): убрать txt/image — наши документы это HTML/PDF
Удалены типы 'txt' и 'image' из DocumentPreviewType + соответствующие
ветки рендера (pre.document-preview__txt и img.document-preview__image)
и стили. В реальных потоках платформы документ кооператива всегда
HTML (рендерится в ShadowHtml внутри BaseDocument) или PDF — plain-
text фрагмент в моноширинном pre не используется и выглядит как
техническая ерунда. Аналогично image.

_dev: удалён previewTxtDemo (мусорный EOSIO chain id из debugging
notes → перешитый в фрагмент протокола → теперь полностью убран
по требованию пользователя). Секция 30 показывает только HTML +
loading + error состояния.
2026-05-27 09:17:08 +00:00
coopops 8c5a7bd466 fix(BaseDocument,_dev): vue-tsc, мусорный txt-demo, ширина документа
vue-tsc:
- _dev: убрал txId/explorerUrl из signatureSignedDemo — оба поля
  удалены из Signature ранее (общего эксплорера нет).
- BaseDocument: canonSignatures.map — нормализую is_valid через
  '?? undefined' (бэкенд может вернуть null, canon-компонент ждёт
  boolean | undefined).

DocumentPreview txt demo: вместо мусорного 'EOSIO chain id …
dirty window' (мой случайный кусок из debugging notes) — фрагмент
протокола собрания пайщиков ПК «Восход».

ComplexDocument: .col-md-7 → .col-md-10. Это контейнер, в котором
лежит BaseDocument в реальных страницах документов. 7/12 = 58%
ширины было визуально 'приплюснуто'.
2026-05-27 09:17:08 +00:00
coopops db80cf0362 feat(ui): DocumentSignatures + перевод BaseDocument на canon
Реальный формат подписей кооперативного документа — это не «pending/
signed/rejected» из абстрактного SignatureCard, а IDocumentAggregate с
полями doc_hash + signatures[] (signer_certificate, public_key,
signature, is_valid). Каждая подпись разворачивается в детали.

Что сделано:
- Новый canon-компонент DocumentSignatures (story 9.5) в shared/ui/
  domain — props-only, принимает уже резолвнутые signerName и hash-
  совпадение, эмитит download/verify. Поверх Quasar — собственный
  expand на ref<Set<number>>, чтобы стиль был полностью canon.
- BaseDocument теперь рендерит DocumentSignatures вместо своего
  q-card.verify-card + q-list + q-expansion-item на teal/red badges.
  Адаптер canonSignatures маппит signer_certificate → ФИО через
  getNameFromCertificate, чтобы canon-компонент не знал про сертификаты.
- SignatureCard: удалены поля txId/explorerUrl и ссылка «Открыть в
  explorer» — общего эксплорера в платформе нет.

Demo: секция 29 → DocumentSignatures (валидный + с битой подписью),
секция 30 → DocumentPreview (HTML заголовок сокращён, чтобы не
обрывался при узкой колонке).
2026-05-27 09:17:08 +00:00
coopops 7524fd6941 feat(ui): волна 2 / E9 — документы и подписи
4 props-only canon-компонента в src/shared/ui/domain/, регистрация
в boot/ui.ts, demo-секции 26-29 в _dev/ui:

- DocumentRow (story 9.1) — строка документа в списке: иконка типа
  с tint'ом (pdf neg-soft, docx info-soft, html primary-soft), title,
  status через BaseBadge, дата/автор/описание, slot actions, emit open.
- SignatureCard (story 9.3) — подписавший (Avatar+ФИО+AccountBadge),
  статус (BaseChip pending/signed/rejected), для signed — хеш в моно-
  блоке + ссылка на explorer, для rejected — BaseBanner с причиной.
- ActivityTimeline (story 9.4) — вертикальный таймлайн событий с
  цветными иконками по типу (sign/reject/create/update/comment/
  transfer), groupByDate группирует по «Сегодня/Вчера/конкретная
  дата».
- DocumentPreview (story 9.2) — html (через DOMPurify), pdf через
  iframe, image через img, txt через pre. Стейты loading/error.

Все props-only, темо-зависимые значения только через --p-* токены.
2026-05-27 09:17:08 +00:00
coopops 29cd2cd5e5 fix(_dev/ui): «Член кооператива» → «Пайщик» в PersonCard demo
Канон-терминология blago/ЦК: пайщик, не член. Прецедент закреплён
в auto-memory feedback_kooperatsiya_pai.
2026-05-27 09:17:08 +00:00
coopops cfec8afe70 fix(IdentityPanel): compact тоже в карточке — padding+border+bg
Раньше compact-вариант рендерился без обрамления — avatar упирался
в левый край контейнера, визуально «проваливался» из общего стека
full-карточек (Screenshot_2026-05-21_21-51-09).

Compact теперь: padding 8/12px (тоньше чем full 16px), тот же border
+ surface + radius. В одну строку avatar + ФИО + AccountBadge + status,
но визуально это всё-таки карточка, не голая строка.
2026-05-27 09:17:08 +00:00
coopops 9f24394398 fix(ContactSheet): label и значение в отдельных строках
Body раньше был обычным block-контейнером, а .head и .value-row —
оба display: inline-flex. Без явного block-форматирования inline-flex
элементы становятся inline и рендерились в одну строку. Визуально:
Emailivanov@example.ru, Телефон+7 (903)..., Telegram@ivanov — слипшиеся.

Body теперь display: flex + flex-direction: column, gap canon — 8px
в comfortable, 4px в compact. Head/value-row переведены на обычный
display: flex (вместо inline-flex), gap внутри сохранён.

Прецедент 2026-05-21 — Screenshot_2026-05-21_21-42-16, 21-42-31.
2026-05-27 09:17:08 +00:00
coopops 824fd3341e chore: убрать закоммиченный symlink node_modules 2026-05-27 09:17:07 +00:00
coopops cc05cd4ba3 fix(domain): E8 — copy-кнопки, отступы и compact-layout
- AccountBadge: copy-кнопка не сжимается (flex-shrink: 0), размер 20×20 + icon 14px — раньше 18×18 + 12px тонула рядом с текстом badge.
- DataRow: column-gap var(--p-3, 12px) → var(--p-5, 20px) + min-label-width 140 → 160px + padding-right на label. Между label и value был визуально слипшийся стык.
- ContactSheet: comfortable margin-top между label и value 2px → 8px (compact 4px). Раньше label «налипал» на значение.
- IdentityPanel compact: переделан в flat flex-row (avatar + ФИО + AccountBadge + status badge) — раньше grid с .body загонял имя и AccountBadge в две строки и avatar «уезжал» от имени. Spec 8.1 требует «только avatar + ФИО + EntityIdBadge в одну строку».
2026-05-27 09:17:07 +00:00
coopops 61e8bdf083 feat(ui): волна 2 / E8 — идентификация и контакты
5 props-only canon-компонентов в src/shared/ui/domain/, регистрация
в boot/ui.ts, demo-секции 21-25 в _dev/ui:

- AccountBadge (story 8.5) — on-chain account name в mono-шрифте,
  copyable + опциональный explorer link. Назван AccountBadge вместо
  EntityIdBadge: имя EntityIdBadge занято под numeric ID пайщика
  в capital. Decision зафиксирован в epics.md и planning-artifacts.
- DataRow (story 8.3) — `label: value` пара для реестровых карточек,
  mono-режим, copyable, slot value-override, hint.
- ContactSheet (story 8.2) — email/phone/address/tg/web с автоиконками,
  кликабельными mailto:/tel:/t.me ссылками, copy и verified-чек.
- IdentityPanel (story 8.1) — Avatar + ФИО + AccountBadge + статус
  (active/blocked/pending) + role + actions-slot; compact/full.
- PersonCard (story 8.4) — Avatar + ФИО + role + AccountBadge +
  ContactSheet + slot meta; compact/comfortable.

Все props-only: useStore/useRouter/useApi внутри запрещены. Темо-зависимые
значения только через --p-* токены, никаких body--dark селекторов.
2026-05-27 09:17:07 +00:00
coopops 98e82eeced fix(BaseCheckbox): приглушить заливку галочки через --p-primary-press
q-checkbox по умолчанию красит .q-checkbox__bg в --q-primary, который в
dark теме равен #2dd4bf — на маленьком квадрате 18px этот яркий бирюзовый
смотрится ядовито. Переопределяем заливку filled-состояния на
--p-primary-press (#134e4a light / #14b8a6 dark) — глубже и спокойнее,
чек-символ остаётся белым.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:07 +00:00
coopops c247eb169f fix(tokens): приглушить --p-accent + унифицировать .agreement-link
Старое значение --p-accent (#D84315 light / #FF7043 dark) — слишком
насыщенное, выглядит ядовито рядом с тёплой палитрой ink/canvas. Меняем
на тёплую терракоту: #B85C38 (light) / #E89472 (dark) — тот же warm-tone,
но без перевозбуждения красного канала.

.agreement-link (псевдо-ссылки на просмотр документа в согласиях) была
hardcoded в #1c64f2 (синий) light / #ff9f43 (жёлто-оранжевый) dark и не
совпадала со ссылкой «Устав кооператива». Унифицируем: все ссылки внутри
согласий теперь var(--p-accent), визиt-цвет тот же — без фиолета.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:07 +00:00
coopops 991ea1d299 fix(ReadStatement): чекбоксы согласий — выравнивание + gap + accent ссылка
BaseCheckbox block-вариант: убран собственный display:flex + align-items:
flex-start, который перебивал внутренний flex Quasar и сдвигал label вверх
относительно inner. Теперь Quasar сам центрирует inner на первой строке.
Добавлен padding-left: 8px на .q-checkbox__label — больше воздуха между
галочкой и текстом согласия.

ReadStatement: ссылка «Устав кооператива» — --p-primary → --p-accent
(тёплый оранжевый #D84315 / #FF7043 dark) как акцентный цвет палитры.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:07 +00:00
coopops 3e01d8c915 feat(UserDataForm): canon-стиль полей + тип аккаунта на BaseRadioCard
UserDataForm/UserDataForm.vue:
- Выбор типа аккаунта: три q-btn(glossy, height:75px) → три BaseRadioCard
  с title/description; больше нет teal-«кнопок-плиток»
- Возврат к выбору типа: flat q-btn → BaseButton(ghost) с arrow_back

Sub-forms (Individual / Entrepreneur / Organization + Create*):
- q-input/q-select: standout="bg-teal text-white" → outlined color='primary'
  (canon-tone, единый primary-акцент вместо устаревшего teal)
- .q-gutter-sm.q-mt-md → .user-data-stack (flex column, gap токены)
- OrganizationDataForm: кнопка «совпадает» — color='teal' → flat primary

Валидация q-form.validate() и :rules сохранены — это всё ещё q-input
с rules-массивом, только tone и контейнер переехали на canon-токены.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:07 +00:00
coopops 4348cd0ae7 feat(ui): BaseCheckbox + BaseRadioCard и миграция SignUp-шагов
- BaseCheckbox: обёртка q-checkbox с canon-стилями, block-вариант для длинных
  согласий (чекбокс прижат к верху, label многострочный)
- BaseRadioCard: карточка-опция с радио-индикатором справа, props title /
  description / meta + slot fallback'и; используется для выбора программы

Миграции SignUp:
- ReadStatement: q-checkbox → BaseCheckbox(block); согласия в .agreements-стек
- SelectProgram: q-list+q-radio → список BaseRadioCard
- GenerateAccount: q-input → BaseInput(readonly, mono) с copy в #append;
  q-checkbox → BaseCheckbox; автоселект через querySelector('input').select()
  вместо ref.select() (BaseInput не пробрасывает внутренний API q-input)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:07 +00:00
coopops 58530d9db8 fix(desktop): .ws-switcher__title padding-top 2px
Воздух над заголовком стола внутри WorkspaceSwitcher — заголовок не упирается
в верхнюю границу caption «ПК «ВОСХОД»».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:07 +00:00
coopops 65b55d7e4b fix(desktop): .rail__top padding 16px 8px + убрать disabled-стиль у WorkspaceSwitcher
- .rail__top: padding 18px 20px → 16px 8px (унификация боковых отступов)
- WorkspaceSwitcher: убраны :disabled-атрибут и hover:not(:disabled) — у пайщика, которому доступен только один стол, кнопка выглядит как обычно (без not-allowed-cursor, без приглушённого цвета). Меню q-menu просто не открывается через v-if='workspaces.length > 1'. Тихое игнорирование клика вместо явного запрета.
2026-05-27 09:17:07 +00:00
coopops 7578d1cfd3 fix(desktop): WorkspaceSwitcher — фильтр столов по ролям + 3-строчный title
WorkspaceSwitcher показывал ВСЕ workspaceMenus, включая столы выше роли пользователя (например, «Стол председателя» видел рядовой пайщик), что приводило к ошибке access-denied при попытке переключения.

- Добавлена фильтрация по иерархии: chairman ⊇ member ⊇ user. Председатель видит столы любых ролей; член совета — user+member; пайщик — только user. Workspace без meta.roles (или с пустым массивом) — публичный для всех (та же логика, что в legacy WorkspaceMenu, плюс корректное наследование прав).
- Title допускает перенос на 3 строки (line-clamp 3) — «Стол вычислительных ресурсов» теперь умещается полностью
- Расширена кнопка switcher'а (margin сторон 8→4 в LeftDrawerMenu), даёт больше места под текст
2026-05-27 09:17:07 +00:00
coopops 7f2d809821 fix(desktop,signup): WorkspaceSwitcher polish + Bank toggle QR↔реквизиты
WorkspaceSwitcher:
- Brand-строка теперь собирается из vars.short_abbr + «vars.name» (получается «ПК «ВОСХОД»»). Раньше показывал только short_abbr — название кооператива было «потеряно»
- Title допускает перенос на 2 строки (line-clamp 2) — длинные названия типа «Стол вычислительных ресурсов» больше не обрезаются ellipsis'ом с одного слова
- Chevron приклеен к верху строки (align-self start) — корректно при двухстрочном title
- Меню workspace: canon padding (8/12) вокруг q-item, отступ между avatar-section и текстом — иконка и название больше не упираются в края
- Затемнение фона при открытом меню (rgba(9,9,11,0.32) overlay через Teleport в body) — меню больше не сливается с контентом за drawer'ом

Bank.vue (карточка платежа):
- Убрали q-expansion-item (раскрытие вниз внутри карточки выглядело перегружено)
- Кнопка-toggle: «Показать реквизиты» рядом с «Скачать QR». Клик переключает зону под сводкой между QR и списком 9 BaseInput'ов с copy-кнопками
- В режиме реквизитов: кнопки меняются на «Скопировать всё» (primary) + «Показать QR» (secondary)
- QR-canvas использует v-show чтобы сохранять состояние при toggle назад; реквизиты — v-if чтобы не держать BaseInput'ы в DOM
2026-05-27 09:17:07 +00:00
coopops 144cc901ff feat(desktop): WorkspaceSwitcher + фикс ReadStatement стилей + плоский Bank.vue
ReadStatement: backend-HTML генерирует с inline-стилями и Quasar text-h* классами, которые перебивали :deep правила. Усилили все ключевые селекторы !important + добавили охват .text-h1/h2/h3/h4/h5/h6 и .text-right (под мета-блок «УТВЕРЖДЕНО…»).

Bank.vue (карточка платежа):
- убрали отдельные surface-карточки вокруг сводки и QR (получалось «карточка-в-карточке-в-карточке»)
- сводка → плоский <dl> со строкой-разделителем снизу (border-bottom var(--p-line))
- QR → просто центрированный canvas без обёртки/фона/рамки; белый цвет внутри canvas — функциональное требование контрастного сканирования, не дизайн-выбор
- detail-expansion → без своего фона, только border-top/bottom

WorkspaceSwitcher (новый widget src/widgets/Desktop/WorkspaceSwitcher/):
- Карточка-кнопка в шапке drawer: иконка кооп + caption «ПК «ВОСХОД»» + bold «Стол совета» (текущий) + chevron
- Клик → q-menu со списком всех workspaceMenus (Стол пайщика, Стол совета, Стол председателя и т.д.); выбор → selectWorkspace + goToDefaultPage
- Подключён через #brand slot AppDrawer'а в LeftDrawerMenu, заменяет дефолтный brand-row
2026-05-27 09:17:07 +00:00
coopops 40492a40cd feat(signup): canon-типографика заявления + рефакторинг карточки платежа
ReadStatement (этап 6.1):
- Backend генерирует HTML документа — локально нормализуем через :deep
- h1 уменьшен с монструозного дефолта до canon h3 (20px), центрирован
- h2/h3, p, strong, ul/ol, table, hr — типографика по var(--p-*)
- мета-блок .approved/.meta-right (УТВЕРЖДЕНО…) — ink-2, мельче, плотнее
- Чекбоксы соглашений снизу не трогали (как просил пользователь)

Bank (этап 6.2 — карточка платежа PayWithProvider):
- QR-код вынесен в фокус: сначала сводка (получатель/сумма/назначение) → QR → действия → реквизиты для ручного перевода скрыты под q-expansion-item
- Полные 9 полей (ИНН/БИК/КПП/корр-счёт/...) переведены на BaseInput(readonly, mono) с copy-кнопкой в #append; раскрываются только при необходимости
- Кнопки «Скачать QR» (primary) и «Скопировать реквизиты» (secondary) — BaseButton
- Удалены: q-btn(push), case-mixed «скопировать реквизиты», стиль #qr через global селектор
2026-05-27 09:17:07 +00:00
coopops fd0609de19 feat(invite): этап 5 — миграция Invite на canon AuthCard + BaseInput/BaseButton
- InvitePage: row/col-обёртка → .invite-page flex-center + canon padding (как у SignInPage)
- Invite widget: AuthCard со штатными title/subtitle (вместо CAPS-заголовка с background-clip градиент-текста)
- BaseInput(mono, readonly) для приватного ключа — canon-оформление key/hash полей
- BaseButton(ghost) для копирования + BaseButton(primary) для «Установить ключ»
- Удалены: градиент-текст, semi-transparent blue/green tint карточки вокруг инструкции и чекбокса, hover translateY + box-shadow glow, custom border-radius/box-shadow для q-input и кнопок, мобильные @media-блоки (canon-токены и AuthCard сами адаптивны)
2026-05-27 09:17:07 +00:00
coopops 791c7e1707 fix(signup): canon-рамка для контейнера подписи вместо неонового glow
- .signature-container: 3px primary border + 3-слойный box-shadow glow → 1px dashed по var(--p-line-2), surface-2 фон, без свечения; на hover лишь окрашивается рамка
- .signature-hint: переехала в центр контейнера, canon ink-2 цвет, убран frosted-chip с blur/shadow — теперь спокойная метка «Оставьте собственноручную подпись в рамке», pointer-events: none пропускает клики на canvas
- min-height снижен 300→220, padding на canon-токены
2026-05-27 09:17:07 +00:00
coopops 71331854f0 fix(signup): отступ сверху, canon фон q-stepper, ширина email-поля
- SignUp.vue: добавлена обёртка .signup-page (padding + center + min-height) — как у SignInPage; раньше страница была прижата к шапке
- AuthCard maxWidth: 1000 → 720 — историческое 1000 для трёх-колоночной UserDataForm; для остальных шагов выглядит абсурдно широко
- q-stepper: убраны собственный фон/тень/padding (не «карточка в карточке»), линии-разделители переведены на var(--p-line)
- EmailInput: max-width поля 360px — email не должен растягиваться во всю ширину карточки
2026-05-27 09:17:07 +00:00
coopops 4a6af072ac fix(authcard): убрать оборванный barrel-экспорт после удаления старого AuthCard
vue-tsc ругался: shared/ui/index.ts:21 — Cannot find module './AuthCard'.
- shared/ui/index.ts: убран `export * from './AuthCard'`
- LoginRedirectPage.vue: импорт переключён на canon `shared/ui/domain/AuthCard`, CAPS-заголовок заменён на canon `title` prop
- Invite.vue: импорт переключён на canon (полная миграция Invite — этап 5)
2026-05-27 09:17:06 +00:00
coopops c418a544ac feat(signup): этап 4 — миграция SignUp на canon AuthCard + BaseButton/BaseInput
- canon `shared/ui/domain/AuthCard` переписан на pug
- SignUp.vue переключён со старого «глянцевого» AuthCard на canon (title prop вместо CAPS-заголовка)
- старый `shared/ui/AuthCard` удалён (использовался только в SignUp)
- 7 подэкранов SignUp (EmailInput, SetUserData, SelectProgram, GenerateAccount, SelectBranch, ReadStatement, SignStatement): q-btn → BaseButton (ghost для «назад», primary для «Продолжить»)
- EmailInput: q-input → BaseInput, валидация rules переведена на computed :error
- q-input в GenerateAccount оставлен из-за зависимости от ref.select()
2026-05-27 09:17:06 +00:00
coopops 5480c1cd04 content(brand): og-image из poster-logo-horizontal + новый длинный дескриптор
- og-image.png — взят brand-постер ~/blago/production/shared/poster-logo-horizontal.png,
  отресайзен до 1000px по ширине и центрирован на полотне 1200×630
  с белыми краями по вертикали. Шрифт/композиция родные бренда.
- description / og:description / twitter:description: «Цифровой Кооператив —
  система управления хозяйством. Регистрация пайщиков, заказы на поставку
  и приобретение имущества, собрания совета, общие собрания пайщиков,
  взаимные расчёты на блокчейне, автоматический документооборот и бухбаланс
  на основе простой электронной подписи.»
2026-05-27 09:17:06 +00:00
coopops fea2e32b59 content(brand): обновить превью и мета — система управления хозяйством на платформе «Благо»
- og-image.png перерисован: «Цифровой Кооператив» / «Система управления
  хозяйством» / «на платформе БЛАГО — кооперативной экономики для жизни» /
  «Регистрация пайщиков · заказы на поставку и приобретение имущества ·
  расчёты на блокчейне».
- description / og:description / twitter:description выровнены под этот
  же копирайт. Слово «взаимоотношения с пайщиками» убрано как слишком
  узкое — управление хозяйством включает в себя ещё заказы, реестры
  и документы.
2026-05-27 09:17:06 +00:00
coopops 265e2783af feat(brand): системный логотип ЦК + чистка ассетов + OG-превью
- src/assets/logo.svg, public/favicon.svg, public/logo.svg — единая SVG
  логотипа кооператива (path с «ц» внутри). Скачано с
  https://цифровой-кооператив.рф из шапки сайта.
- src/assets/logo.svg: fill -> currentColor чтобы лого внутри
  canon .rail__brand наследовал --p-primary (для тёмной темы тоже).
- LeftDrawerMenu подключает лого через slot #brand-icon AppDrawer
  (импорт ?raw + v-html). Внутри зелёного квадрата шапки рейла теперь
  наш системный знак, а не дефолтный q-icon "dashboard".
- index.html: favicon → /favicon.svg, apple-touch-icon тоже на SVG.
  Удалены неработающие ссылки на /favicon.ico и /apple-touch-icon-*.png.
- index.html: добавлены description + расширенные og:* и twitter:*
  с конкретным описанием и og-image (1200×630), чтобы ссылки в чатах
  выглядели прилично.
- public/og-image.png — превью 1200×630, ImageMagick-сгенерированный
  композит из логотипа + текста.
- Удалены неиспользуемые ассеты:
  - public/icons/, public/pwa/ — старые favicon/PWA-PNG'и всех размеров;
  - src/assets/* — 40 легаси SVG/PNG (anime, blockchain, dacom*, flow*,
    header-logo, quasar-logo-vertical, system*, club*, welcome.jpeg и т.д.).
  Оставлены только src/assets/pin.svg (используется в Map.vue) и
  src/assets/logo.svg (новый).
2026-05-27 09:17:06 +00:00
coopops 41dcd40098 fix(layout): убрать TS-каст из pug-шаблона AppDrawer
Pug-template передаёт выражения в Vue compiler как строки; конструкции
вида (entry as RailItem).route не превращаются в JS и падают
SyntaxError в браузере: «Unexpected identifier 'as'».

Касты унесены в <script>: добавлены computed flatItems и
normalizedGroups, шаблон работает с уже типизированными массивами без
inline-TS.
2026-05-27 09:17:06 +00:00
coopops c3649ca972 fix(layout): этап 3.3 — все canon-компоненты на pug + фикс импорта cmdk
- LeftDrawerMenu: импорт useCmdkStore → useCmdkMenuStore (правильное имя).
  Без этого ломался ESM build всего drawer'а: «no exported member
  useCmdkStore» — RailUserCard не монтировался, поэтому и chevron свёртки
  «не работал», и Найти/Пополнить не реагировали.
- AppDrawer.vue, AppHeader.vue, RailUserCard.vue переведены на pug
  (lang="pug"). В проекте везде pug — никакого исторического HTML в
  canon-компонентах быть не должно.
2026-05-27 09:17:06 +00:00
coopops abd634beca fix(layout): этап 3.2 — pug + v-model:collapsed + рабочие триггеры
- q-drawer width 240 → 248px (совпадает с canon --p-rail-w, кошелёк больше
  не уплывает справа из-за обрезки на 8px)
- LeftDrawerMenu: убран весь :deep override на .rail__usercard/.rail__signout,
  возвращаем canon margin/padding как есть
- LeftDrawerMenu: добавлен v-model:collapsed на RailUserCard с сохранением
  в localStorage — chevron «свернуть/развернуть» снова работает
- LeftDrawerMenu: кнопка Найти теперь дёргает cmdkStore.openDialog() напрямую
  (фейковый KeyboardEvent не доходил до глобального обработчика)
- LeftDrawerMenu: «Пополнить» → useDepositDialog().open(); DepositButton +
  WithdrawButton рендерятся скрыто как держатели q-dialog (q-portal в body)
- Header.vue и LeftDrawerMenu.vue переписаны на pug (как остальной проект)
2026-05-27 09:17:06 +00:00
coopops 26a25e1131 fix(layout): этап 3.1 — RailUserCard в footer + canvas-фон + отступы рейла
- AppDrawer: плоский items оборачивается в .rail__nav, появляется gap 4-8px до cmdk
- cmdk margin 16→8px, чтоб выровнять с rail__nav (8px от стенки)
- LeftDrawerMenu footer: WalletCardMini+LogoutButton → один canon RailUserCard
  (avatar+balance в primary-soft + Пополнить + встроенный signout)
- :deep override на .rail__usercard margin 16→12/8 и .rail__signout padding 12/20→12/12
- quasar-canon: body--light/dark + q-layout/q-page-container/q-page красятся
  через --p-canvas, чтобы фон страницы соответствовал шапке и рейлу
2026-05-27 09:17:06 +00:00
coopops e7a352af22 feat(layout): этап 3 — WalletCardMini + canon LogoutButton в footer AppDrawer
LeftDrawerMenu:
- legacy `MicroWallet` (ColorCard teal с большой карточкой пайщика + ИП-badge
  + Deposit/Withdraw micro-кнопки) → canon `WalletCardMini` из
  widgets/wallet-card-mini (тонкая карточка из shared/ui/domain/WalletCard
  compact-variant, читает walletStore сама).
- Убран toggle «свернуть/развернуть нижнюю секцию» — нижняя секция теперь
  всегда видна (короткий компактный блок: WalletCardMini + Выйти).
- Удалены неиспользуемые `onMounted`/`ref` импорты, slide-анимация и CSS
  toggle-кнопки.

LogoutButton:
- pug + q-item с красной полупрозрачной плашкой → плоский ghost-button в
  стиле rail-пункта AppDrawer: прозрачный фон в покое, на hover —
  `--p-neg-soft` фон + `--p-neg` текст/иконка.
- Из uppercase «ВЫЙТИ» в title-case «Выйти» (canon — никаких uppercase).

Визуально левый drawer теперь полностью в canon-семье: единый rail с
пунктами + поиском + компактным wallet-балансом + ghost-кнопкой выхода.

Закрывает этап 3 из 5 в волне «приземление DS на реальный layout».
Дальше — SignUp (этап 4) и Invite (этап 5).
2026-05-27 09:17:06 +00:00
coopops b524374b23 feat(layout): этап 2 — AppHeader в реальном layout (заменяет MainHeader)
widgets/Header/CommonHeader/Header.vue теперь использует canon AppHeader
из shared/ui/layout. Высота шапки 56px (--p-topbar-h), border-bottom
hairline, без shadow и фоновых градиентов.

Слоты:
- #crumb: BackButton (для авторизованных), либо title с названием
  кооператива из system.info.vars для гостей/install.
- #actions: headerActions injection через useHeaderActionsReader (всё что
  страницы инжектят — Deposit/Withdraw, SettingsDropdown и пр. — рендерим
  как было).
- #notifications: NotificationCenter (только loggedIn + isClient).
- #theme: ToogleDarkLight — пока legacy, сохраняет storage + PWA-цвет;
  наш canon ThemeToggle их не делает (привяжу позже отдельным эпиком).
- #profile: BaseButton primary для гостей — Регистрация/Вход (в зависимости
  от текущего route).

Удалены:
- MainHeader.vue (вся логика scroll arrows + carousel actions group ушла —
  canon `.topbar__actions { gap: 8px }` достаточно; overflow-scroll
  будет вернут отдельно если потребуется на узких экранах).
- HeaderStyles.scss (стили scroll-arrow и q-toolbar overrides больше
  не нужны — canon-разметка через `<header class="topbar">`).

q-header Quasar остаётся wrapper'ом (sticky-poзиционирование в q-layout),
внутри — AppHeader даёт canon-разметку.
2026-05-27 09:17:06 +00:00
coopops 8280cb938e feat(layout): этап 1 — AppDrawer в реальном layout (заменяет LeftDrawerMenu)
Переписан widgets/Desktop/LeftDrawerMenu/LeftDrawerMenu.vue: внутри теперь
canon `AppDrawer` из shared/ui/layout с rail-видом MONO v2 (логотип ПК +
короткое имя кооператива в шапке рейла, ⌘K-поиск, пункты-router-link'и
с иконкой/бэйджем, sticky-footer).

Адаптер store→canon:
- items: desktop.activeSecondLevelRoutes → RailItem[], с прежней
  фильтрацией по roles + meta.conditions + meta.hidden (один-к-одному
  логика из SecondLevelMenuList).
- activeKey: вычисляется из router.currentRoute с поддержкой группового
  паттерна project-* → projects-list.
- @select: router.push({name, params:{coopname}}) или
  actionsStore.executeAction(meta.action), закрытие drawer на mobile.
- @cmdk: эмиттим keyboard event ⌘K → CmdkMenu (он смонтирован глобально
  в default.vue) подхватывает.

Footer-слот: пока сохранён legacy MicroWallet + LogoutButton + toggle
свернуть/развернуть (заменим на WalletCardMini в этапе 3).

default.vue: q-drawer width 200 → 240 (canon-ширина rail'а).

Что НЕ затронуто этим коммитом: верхняя шапка (legacy MainHeader),
правый drawer для actions, q-footer для гостей. Их меняем отдельными
этапами 2–3.
2026-05-27 09:17:06 +00:00
coopops b327f1b103 fix(ui): убрать конфликт с Quasar — .row → .u-row
Корневая причина «оторванной» галочки в ResetKeyForm: я ранее задал в
mono-platform/components.css глобальное `.row { display: flex;
align-items: center; gap: 12px; }`. Quasar внутри `q-checkbox` (а также
q-radio, q-toggle, q-btn-toggle и др.) сам добавляет class="row" на
корень компонента и использует его как inline-flex — мой `gap: 12px`
раздвигал `.q-checkbox__inner` и `.q-checkbox__label` на 12px по всему
приложению.

Решение:
- Переименовать утилиту `.row` → `.u-row` (та же семантика, тот же набор
  модификаторов --wrap, --gap-2/4/6). У Quasar теперь свободный .row.
- Обновить все 6 использований в /_dev/ui.
- Откатить предыдущий :deep(.q-checkbox__label) padding-left:4px workaround
  в ResetKeyForm — лечил симптом, причина устранена.

Затрагивает только нашу dev-витрину и canon-utility; legacy pug-виджеты
с `.row.justify-center` продолжают работать через стандартный Quasar
flex-grid класс.
2026-05-27 09:17:06 +00:00
coopops 69e71871e8 fix(ui): ResetKeyForm — сжать зазор между галочкой и текстом «Я сохранил ключ»
Quasar по умолчанию даёт ~12–16px padding на .q-checkbox__label, в
compact-форме сохранения ключа это смотрелось «оторвано». Уменьшаем до 4px
через :deep override на классе .rk-form__confirm.
2026-05-27 09:17:05 +00:00
coopops 05c9fdf072 feat(dev): секция 20 — ResetKey двухэтапный flow в /_dev/ui
Три кадра в одной секции: LostKey (ввод email), ResetKeyForm check-mail
(уведомление «письмо отправлено»), ResetKeyForm save-key (демонстрация
сохранения ключа с моковым сгенерированным аккаунтом). Submit на третьем
кадре регенерирует мок-ключ для повторной проверки.

Заменяет ошибочную секцию ChangeKey из предыдущей итерации.
2026-05-27 09:17:05 +00:00
coopops 1d8268b41e fix(ui): откатить ChangeKey + мигрировать настоящий ResetKey flow на base-компоненты
ChangeKey-фича (формы current/new/confirm WIF + успешный диалог) была
ошибочной интерпретацией E7. Реальный flow «сменить ключ» в продукте —
это **двухэтапный ResetKey**:

  1. LostKey: пайщик вводит email (он потерял ключ — текущего WIF нет),
     бэк присылает письмо со ссылкой `?token=...`.
  2. ResetKey: при заходе по ссылке клиент сам генерирует новый ключ
     прямо в браузере (через `useCreateUser().generateAccount()`),
     показывает приватный ключ readonly с «Скопировать», требует
     подтверждения «Я сохранил ключ» и вызывает on-chain
     `resetKey({token, public_key})`.

Текущий ключ ввести нельзя (его нет). Новый ключ ввести нельзя (только
генерируется). Никакого подтверждения нового ключа — он один и тот же.

Что сделано:
- Удалена ошибочная `features/User/ChangeKey/`.
- Старый `widgets/Registrator/ResetKey/ui/ResetKey.vue` (pug + сырые
  q-input/q-checkbox + кастомные градиентные стили) разбит на:
  * `ResetKeyForm.vue` — presentation (props-driven, режимы
    `check-mail` / `save-key`), без useRouter/useApi — пригоден для
    /_dev/ui витрины.
  * `ResetKey.vue` — connected обёртка, читает `route.query.token`,
    генерит account через `useCreateUser`, диспатчит `resetKey` через
    `useResetKey`, на успех → router.push на signin.
- Новая UI собрана на BaseForm + BaseInput readonly mono + BaseButton +
  BaseBanner + AuthCard.

/_dev/ui секция 20 теперь показывает три кадра подряд: LostKey (email
шаг), ResetKey check-mail, ResetKey save-key с моковым сгенерированным
ключом.

FR11, UX-DR9, UX-DR15.
2026-05-27 09:17:05 +00:00
coopops 2e7a6547fb fix(ui): BaseInput/BaseSelect — явный проброс слотов вместо v-for in $slots
Generic-проброс `<template v-for="(_, slotName) in $slots" #[slotName]>`
рушится в Quasar 2.19 рендере: при наличии scoped-слота #append вылетает
`Cannot read properties of null (reading 'key')` в renderSlot + Vue warns
по `_isVue`/`constructor` свойствам. Заменяем на явный список слотов
(prepend / append / before / after / hint у q-input; +option, selected-item
у q-select) — синтаксис, который Quasar держит корректно.

Кейс всплыл на ChangeKeyForm: #append с q-icon (toggle visibility) ронял
страницу /_dev/ui целиком.
2026-05-27 09:17:05 +00:00
coopops aa24f3d9b2 feat(dev): секция 20 — ChangeKey flow в /_dev/ui (визуальная проверка E7)
Подключение ChangeKeyForm + ChangeKeySuccessDialog к dev-витрине.
Submit запускает заглушку useChangeKey().changeKey() (≈600 мс), по успеху
открывает диалог с новым WIF. Generate генерирует валидный K1 ключ через
SDK и заполняет оба новых поля. Confirmed сбрасывает форму.
2026-05-27 09:17:05 +00:00
coopops a444648db1 feat(ui): E7.2 ChangeKeySuccessDialog — подтверждение через BaseDialog без backdrop-exit
Диалог успеха смены ключа: новый WIF в моноширинном блоке с copy-кнопкой,
BaseDialog с closeOnBackdrop=false, closeOnEscape=false, hideCloseButton=true.
Закрытие только через primary-кнопку «Я сохранил ключ»; до копирования
кнопка показывает guard-баннер вместо немедленного закрытия. По confirmed
эмитим событие — реальный flow подключит router.push на дашборд.

FR11, UX-DR11, UX-DR15. Story 7.2 из epics.md.
2026-05-27 09:17:05 +00:00
coopops 39a03c48eb feat(ui): E7.1 ChangeKeyForm — смена ключа на BaseForm + BaseInput + BaseBanner
Форма смены ключа пайщика на новых компонентах дизайн-системы.
BaseBanner severity=warn с предупреждением о потере, checklist последствий,
поля current/new/confirm WIF моноширинные с toggle visibility, ghost-кнопка
«Сгенерировать» через PrivateKey.generate из @wharfkit/session.

Бизнес-логика — заглушка с TODO на привязку on-chain change-key action
(pattern см. features/User/ResetKey/api) и TODO на OtpInput шаг подтверждения
(приходит в Эпике 10).

FR11, UX-DR9, UX-DR15. Story 7.1 из epics.md.
2026-05-27 09:17:05 +00:00
coopops d70ecb3878 fix(ui): Avatar tone — единая семантика neutral/primary/accent, убрать ink
Bug после rename --p-accent → --p-primary: класс .avatar--accent смотрел
на --p-primary-soft (canon teal), а должен был — на --p-accent-soft
(warm orange по новой семантике). И вариант ink (белый bg + dark text)
визуально выпадал из палитры — крайний xl-аватар на демо-витрине казался
«не пришей к звезде рукав».

Новая семантика AvatarTone:
- neutral (default) — серый surface-3 + ink-1 (нейтральный)
- primary — canon-teal soft + canon-teal text (основной выделение)
- accent — warm-orange soft + warm-orange text (редкое выделение,
  например подсветка пайщика по batch_id)

Удалён вариант ink — не использовался в реальном коде.
В dev-витрине xl-аватар: tone="ink" → tone="primary".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:05 +00:00
coopops ed93765fb0 revert(ui): убрать override центрирования suffix/prefix — оставить Quasar дефолт
Override (padding-top:0 + align-self:center на dense) не отрабатывал —
Quasar в dense держит prefix/suffix по baseline вводимого значения,
и пользователю это устраивает. Убираем неработающий override, чтобы
не мешался в дальнейших правках.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:05 +00:00
coopops a252fe00ec fix(ui): BaseDialog — отступ через margin-top второго ребёнка
Inline `style="margin:0"` на <p>-описании перебивал мой margin-bottom
через CSS (специфичность inline > selector). Переключаюсь на
`> :first-child + *` margin-top — отступ крепится ко второму ребёнку,
не зависит от inline-стилей первого.

Работает универсально: <p> + <BaseInput>, <p> + <BaseForm>, любая
другая пара «описание + контент».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:05 +00:00
coopops 0fbf764437 feat(ui): --p-accent (warm orange) + suffix-центр + точная вёрстка gap
1. --p-accent (canon token): #D84315 (light) / #FF7043 (dark).
   Совпадает с legacy Quasar $accent (warm orange, Material Deep Orange).
   Проброшен в --q-accent через CSS var — legacy color="accent",
   var(--q-accent), text-accent продолжают работать без изменений,
   но теперь следуют за canon-темой (light/dark). Резерв под редкое
   выделение: ссылки batch_id и т.п.
2. quasar-canon.css: .q-field--dense .q-field__suffix/prefix —
   padding-top:0 + align-self:center. Quasar по умолчанию ставит
   padding-top: 24px под floating-label, отчего «RUB» визуально
   опускался к низу control'а. Теперь в одну линию со значением.
3. BaseForm: убран gap 12px из .base-form__body — reserve-hint-space
   у q-input даёт ~24px снизу под error/hint, дополнительный gap
   делал расстояние между инпутами неприятно большим.
4. BaseDialog: убран универсальный gap из body, оставлен margin-bottom
   только на первом <p>/.intro — между описанием и первым инпутом
   воздух нужен, между остальными детьми reserve-hint-space хватает.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:05 +00:00
coopops 5ed4b25634 fix: убрать случайный symlink node_modules из индекса
Промахнулся `git add -A` — захватил worktree-symlink node_modules
(он pnpm-shared с основным репо). .gitignore покрывает node_modules/
но симлинк-файл — отдельный путь.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:05 +00:00
coopops fa423c1caa refactor(ui): --p-accent → --p-primary — устранить конфликт с Quasar primary
Контекст: после проброса --q-primary = var(--p-primary) (4fddfe2343)
наш токен --p-accent и Quasar primary стали обозначать ОДИН и тот же
canon-teal — два имени для одного цвета это когнитивный шум, и при
росте платформы будет путать. Имя `accent` оставляем свободным под
будущий ВТОРОЙ цвет (warm-orange, для редкого выделения вроде ссылок
по batch_id) — это отдельная задача когда понадобится.

Переименовано везде (109 ссылок в 6 файлах):
- --p-accent          → --p-primary
- --p-accent-hover    → --p-primary-hover
- --p-accent-press    → --p-primary-press
- --p-accent-soft     → --p-primary-soft
- --p-accent-line     → --p-primary-line
- --p-accent-strong   → --p-primary-strong
- --p-ink-on-accent   → --p-ink-on-primary

Файлы: tokens.css, quasar-canon.css, components.css, RailUserCard,
AuthCard, _dev/ui (включая user-facing labels палитры «Accent → Primary»
и заголовок секции «Акцент → Основной цвет»).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:05 +00:00
coopops 23389a8a42 feat(ui): canon-цвет через --q-primary CSS var; вернуть dense + rounded
- tokens.css: проброс canon-палитры в Quasar brand-переменные
  (--q-primary/negative/positive/warning/info = соответствующие
  --p-* токены). Резолвится lazy → автоматически следует за темой.
  Теперь Quasar САМ красит focus q-field, q-btn color="primary",
  bg-primary utility-классы в canon-цвета без CSS-overrides;
- quasar-canon.css: убраны border-color overrides на ::before
  (default/focus/error) — Quasar управляет цветом через --q-primary
  и собственный механизм --focused/--highlighted. Оставлены только
  background, text-color и rounded углы (border-radius var(--p-r-sm));
- BaseInput/Select: вернули `dense` (компактный 40px) и явный
  `color="primary"` (canon teal через --q-primary).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:37 +00:00
coopops 07cfff6341 fix(ui): BaseDialog — уменьшить отступ между body и footer-кнопками
Между нижним инпутом и footer-кнопками был ~32px (q-card-section
padding-bottom 20 + q-card-actions padding-top 12) — выглядело
растянуто. Сводим к 16px (8+8) — кнопки сидят ближе к контенту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:37 +00:00
coopops d58cecc55d fix(ui): q-field overrides — оставить только цвет, всё остальное дефолт Quasar
Регрессия мерцания границы и приплюснутости — следствие борьбы наших
overrides с встроенной Quasar анимацией. Возвращаем дефолт: Quasar
сам решает геометрию (height/padding/border-radius/font), анимации,
opacity ::before и focus-индикацию. Наша задача — только покрасить
в canon-цвета.

Удалено из .q-field--outlined:
- height/padding/border-radius/font-size/font-family overrides;
- transition border-color (Quasar сам делает свой transition);
- opacity:1 !important на ::before (был костыль от мерцания);
- box-shadow:none на --focused/--highlighted;
- display:none на ::after underline;
- override .q-field--dense;
- override .q-field--float .q-field__label (Quasar анимирует label сам);
- focus-shadow / outline reset на native input.

Оставлено (только цвет):
- background: var(--p-surface);
- border-color: line-1 / accent (focus) / neg (error);
- color текста, placeholder, label, prefix/suffix, hint/error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:37 +00:00
coopops d32e63643c fix(ui): мерцание границы q-input на focus + unused import в BaseTable
- Quasar при focus анимирует opacity ::before (прячет старую границу
  перед показом ::after underline). Так как ::after мы спрятали,
  получался видимый gap ~200-400 ms «граница исчезла → появилась
  с подсветкой». Фикс: opacity:1 !important на ::before,
  transition только по border-color.
- BaseTable.vue: убран неиспользуемый импорт BaseTableColumn.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:37 +00:00
coopops a9e6487ee7 fix(ui): убрать dense из BaseInput/Select; gap в Dialog/Form body; wallet 17px
- BaseInput/BaseSelect: убран `dense` — возвращаемся к дефолтному
  outlined Quasar (control ~56px). Floating-label получает достаточно
  воздуха, текст значения визуально центрируется без сжатия;
- BaseDialog: `.base-dialog__body` теперь flex-column с gap 16px —
  описание/инпуты/таблицы внутри диалога получают единый воздух
  без правки потребителя (пример: «Паевой взнос: укажите сумму…» →
  отступ до инпута появляется автоматически);
- BaseForm: `.base-form__body` gap 4px → 12px — те же требования
  внутри формы;
- WalletCard: `.wallet__metric-val` 19→17px — итоговое значение
  для аккуратного баланс-блока FULL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:37 +00:00
coopops 5462e59b47 fix(ui): убрать stack-label, двойную focus-подсветку и mini-wallet из шапки
1. Stack-label: BaseInput/BaseSelect → только `dense`. Quasar
   стандартно сам корректно держит floating-label в dense+outlined,
   без CSS-override метка работает по документации Quasar.
2. Двойная focus-подсветка: убран наш box-shadow var(--p-focus-ring)
   поверх Quasar внутренней focus-индикации (виден был как «белый
   ринг» на тёмной + наш teal). Теперь только border-color меняется
   через ::before (canon-минимализм). Дополнительно `box-shadow: none
   !important` на --focused/--highlighted control и `display:none` на
   ::after underline (на случай dense Quasar variants).
3. Mini-wallet в правом верхнем углу AppHeader: слот #wallet удалён
   из API (баланс уже отображается в RailUserCard в drawer'е,
   дубль в шапке избыточен). Убран из dev-витрины каркаса.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:37 +00:00
coopops a566b96609 fix(ui): WalletCard и mini-wallet — снизить визуальный вес цифр
Регрессия после Quasar wrapper-pivot: метрики кошельков выглядели
крупно по сравнению с заголовком/sub-label, а padding'и были
избыточны для compact-варианта.

- .wallet (FULL): padding 18→14px по верт, иконка 44→40px,
  metric-val 22→19px, ccy 13→12px;
- .wallet--row (compact): padding 14→10px, иконка 36→32px,
  metric-val 18→16px — выглядит как row в списке, а не как карточка;
- .rail__balance-val (mini-wallet в drawer): 22→18px — теперь не
  доминирует над identity-блоком пайщика.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 80d1fe9e8f fix(ui): canon q-field/q-btn пропорции — убрать раздавливание floating-label
Корень регрессии: жёсткий height: 40px в .q-field--outlined раздавил
вертикальную разметку q-input (Quasar держит padding-top:24px под
floating-label слот; при сжатии текст значения сдвигается). Те же
эффекты для select.

- BaseInput/BaseSelect → dense + stack-label по умолчанию;
  Quasar сам корректно держит 40px в dense, метка статически стоит
  над инпутом без floating-механики;
- .q-field--outlined: убран принудительный height; .q-field--dense
  получает только padding-x, высоту контролирует Quasar;
- .q-btn: font-weight 500→450, min-height 40→36px, padding 16→14px —
  кнопки больше не выглядят «жирно/тяжело»;
- .q-btn--outline: hairline-граница через ::before (Quasar pattern),
  без двойной рамки;
- размеры sm/lg/dense пропорционально уменьшены;
- .q-chip: высота 24→26px, padding 10→12px, font-weight 500→450;
  + .q-chip__icon размер 14px (для точечных чипов).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 7118328ba9 refactor(desktop/base): остальные base/ и AuthCard → обёртки Quasar
BaseSelect → q-select (outlined, reserve-hint-space, map-options/emit-value
для совместимости с {value,label} options API).

BaseCard → q-card flat с q-card-section head/body. Variants flat/inset/quiet
override-ят через scoped style.

BaseChip → q-chip с color mapping (variant → primary/positive/negative/
warning/info). Square для canon-look.

BaseBadge → q-badge с тем же color mapping. Dot-вариант через scoped style
(8×8 круг).

BaseBanner → q-banner rounded dense с border-left по variant.

BaseDialog → q-dialog + q-card с size→maxWidth mapping. closeOnBackdrop/
closeOnEscape пробрасываются через :persistent / :no-*-dismiss.

BaseTable → q-table с трансформацией columns в QTableProps['columns'].
Hide-pagination, binary-state-sort. Cell-slots работают через body-cell-{key}.

AuthCard (domain) — был на native canon `.card` div. Переведён на q-card flat
для консистентности. Усиленный shadow + accent-stripe сверху сохранены.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 65db1b6660 refactor(desktop/base): BaseInput/Button/Form → обёртки Quasar
BaseInput теперь рендерит <q-input outlined reserve-hint-space no-error-icon>
с проброшенными slots. reserve-hint-space от Quasar резервирует место
под error/hint встроенно — раньше делали вручную через NBSP. Все Quasar
features (rules через v-bind \$attrs, mask, ref API, native slots) работают
прозрачно. Props API совместим (modelValue, label, hint, error, prefix,
suffix, mono, readonly, disabled, autocomplete, name, id, type).

BaseButton — q-btn с variant→Quasar mapping:
  primary → :unelevated color="primary" (заливка accent)
  secondary → :outline (border + surface)
  ghost → :flat (transparent)
  danger → :flat color="negative" (transparent + neg-cвет)
:no-caps :ripple=false для canon-look. Props API совместим.

BaseForm — q-form-обёртка с сводным error-баннером (q-banner) и slot footer.
banner—neg styling прилетает из quasar-canon.css через .bg-negative-soft.

Глобальная стилизация Quasar в quasar-canon.css даёт canon-look всем
этим компонентам и существующим q-input/q-btn/q-card по всему проекту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops aba3fc53f0 feat(desktop/css): canon-стилизация Quasar поверх (quasar-canon.css)
Архитектурный pivot: вместо native HTML5-компонентов под canon-классами
стилизуем Quasar поверх. Причины:
- Сохраняется весь Quasar API из коробки (rules/mask/dense/slots/refs)
- Существующий код (сотни q-input, q-btn, q-card, q-select) автоматом
  получает canon-look без правок
- Меньше регрессий — Quasar отвечает за accessibility/keyboard/focus

Файл src/css/mono-platform/quasar-canon.css содержит overrides под canon
для: q-field--outlined (q-input/q-select), q-btn (unelevated/outline/flat),
q-card, q-chip, q-badge, q-banner, q-dialog/q-menu, q-item, q-table,
q-checkbox/q-toggle.

Подключен в quasar.config.cjs css[] после components.css. tokens.css
без изменений — все цвета/радиусы/тени тянутся как переменные.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 096f4c4ecc fix(desktop): удалить legacy глобальный .card { transform: scale(1.03) }
Истинный источник «прыгалки» AuthCard на hover: глобальное правило
`.card:hover { transform: scale(1.03) }` в src/app/styles/style.css
(и его дубль в pages/Marketplace/MainPage/ui/MainPage.vue, тоже не scoped)
бьёт по любому элементу с классом card. AuthCard имеет корневой
<div class="card auth-card"> — каждый hover дёргает её на 3%.

Поиск показал: класс `card` без модификаторов в templates никто кроме
новых canon-компонентов (BaseCard, AuthCard) не использует. Этот глобал —
мёртвый legacy, удаляем безопасно.

NBSP-фикс layout-shift из предыдущего коммита (2a2d9a786a) остаётся
актуальным — он лечил другую (на уровне поля) проблему.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 02fa115365 fix(desktop/ui): добить layout-shift, ужать gap полей, accent-стрип AuthCard, затемнить canvas
1) BaseInput — layout shift жив: text-interpolation `{{ error || hint || ' ' }}`
   рендерила обычный пробел, браузер схлопывает text-node с одним whitespace
   в 0-width content → div сжимается → форма прыгает. Заменили на
   `<span>&nbsp;</span>` (NBSP в DOM, не схлопывается).

2) BaseInput — canon `.field { margin-bottom: 16px }` создавал слишком
   большой разрыв между полями (16px margin + 10px form gap + 17px message =
   ~43px). Override `.field { margin-bottom: 0 }` в scoped — расстояние
   управляется gap'ом BaseForm.

3) AuthCard — статичный accent-стрип 3px сверху (`::before`). Визуальный
   якорь без анимации — даёт бренд-маркер «здесь главное».

4) tokens.css light — canvas #fafafa → #f4f4f5 (cool-gray), canvas-2
   #f4f4f5 → #ebebed. Surface остаётся #ffffff, теперь карточка чётче
   проступает на фоне.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops d8b5afe7e4 fix(desktop/ui): убрать layout shift в BaseInput и оживить AuthCard на светлой
BaseInput: контейнер field__message теперь рендерится всегда с min-height
calc(--p-fs-meta * 1.4) — раньше div появлялся/исчезал на каждом keystroke
при валидации (см. LostKey emailError), форма прыгала вверх-вниз.
Когда нет ни ошибки, ни хинта — рендерится NBSP, высота сохраняется.

AuthCard: добавлен box-shadow поверх canon-минималистского .card —
canvas #fafafa и surface #ffffff на светлой почти неразличимы, без тени
карточка выглядит плоской. На тёмной отдельный shadow усилен (поверх
canon `--p-shadow-card`, который оптимизирован под data-cards, не hero).
border-color подкручен с --p-line на --p-line-1 для большей чёткости.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 324d4fdab5 feat(desktop/LostKey): E6 Story 6.3 — миграция «Перевыпуск ключа» на canon
Scope-adjustment: plan'овая Story 6.3 предполагала миграцию SignUp как
второго auth-flow, но Register — multi-step wizard (10+ файлов в
pages/Registrator/SignUp/), который требует VerticalStepper из Эпика 10
и не помещается в Волну 1. Вместо этого мигрируем LostKey — простую
одно-полевую auth-страницу, того же auth-домена.

LostKey (widget): pug → html, старый AuthCard (q-card с shimmer) →
canon AuthCard. q-input → BaseInput с inline email-валидацией;
q-btn → BaseButton; всё обёрнуто в BaseForm с loading/error.
Бизнес-логика (useLostKey.startResetKey + router redirect на resetkey)
сохранена 1:1.

LostKeyPage: footer-кнопка «Назад» через ghost BaseButton в footer-slot
AuthCard; центровка через flexbox.

Не мигрировано в Волне 1 (известный gap):
- SignUp wizard (требует VerticalStepper из E10)
- ResetKey / Invite (на старом AuthCard, мигрируются в Волне 2)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops d5dfc3d645 feat(desktop/SignIn): E6 Story 6.2 — миграция Login на canon-компоненты
LoginForm (feature): q-input → BaseInput, q-btn → BaseButton, обёрнут в
BaseForm с управлением loading/error. Бизнес-логика (useLoginUser, redirect,
OpenReplay tracking, NotificationPermissionDialog) сохранена 1:1.
Ошибки входа теперь показываются через сводный error BaseForm
(плюс legacy FailAlert остаётся как fallback).

SignIn (widget): импорт AuthCard переключён на новый canon-композит
(src/shared/ui/domain/AuthCard), убран pug-template и градиентные стили.
Добавлен footer-slot — пробрасывается в AuthCard.

SignInPage: переписан с pug → html, footer-кнопки «Потеряли ключ?» /
«Нет аккаунта?» вынесены в footer-slot AuthCard через BaseButton ghost-sm.
Центровка через flexbox вместо QRow grid.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 8591edb942 feat(desktop/AuthCard): E6 Story 6.1 — AuthCard композит + AuthLayout
AuthCard (src/shared/ui/domain/AuthCard) — canon .card + center-aligned
head (title/subtitle/slot head) + body-slot + опциональный footer-slot.
Старый src/shared/ui/AuthCard (q-card с shimmer-градиентом) остаётся
для не-мигрированных страниц (SignUp wizard, ResetKey, Invite).

AuthLayout (src/app/layouts/AuthLayout.vue) — wrapper-обёртка для full-page
auth-страниц: фон var(--p-canvas), центрирование, тогл темы в правом верхнем.

Витрина: dev-секция 19 с примером login-формы (BaseForm + BaseInput ×2 +
BaseButton + footer-ссылки).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 294480ee5a feat(desktop/widgets/wallet-card-mini): E5 Story 5.2 — connected-обёртка WalletCard
WalletCardMini подключает WalletCard к Pinia (useWalletStore.program_wallets) +
System store (info.symbols.root_govern_symbol) + useRouter (клик →
name: 'wallet'). Программный mapping: wallet→MAIN, blagorost→BLAGOROST,
generator→GENERATOR (Zeus.ProgramType). Loading определяется автоматически
по наличию данных в store (или пробрасывается через prop).

Подключено в слот wallet AppHeader на dev-витрине (секция 16 — каркас) +
выделенная секция 18 с тремя программами. На dev-странице покажет
skeleton-loading т.к. loadUserWallet не вызывался; на реальных страницах
после init процесса — настоящий баланс.

Со-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops e4af71873e fix(desktop/_dev/ui): корректные подписи программ Благорост/Генератор в витрине WalletCard
Благорост — капитализация РИД (не «накопления на жильё»), Генератор —
генерация РИД (не «доходы от программы»). Кошелёк остаётся «свободный
остаток». Черновые ярлыки риском попадают в production через копипасту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops f76e2c72f5 feat(desktop/WalletCard): E5 Story 5.1 — props-only компонент с программными акцентами
Канонический .wallet с компактным (--row) и full-вариантами, программные
акценты через --prog-blagorost / --prog-wallet / --prog-generator (UX-DR20).
Витрина на /_dev/ui секция 17: три программы × оба варианта + loading +
empty + locked-line.

Compact (.wallet--row): 36px icon, 18px sum, 14px padding — для слота шапки.
Full (.wallet): 44px icon, 22px sum, 18px padding — для дашборда.

Connected-обёртка widgets/wallet-card-mini (Story 5.2) делается отдельным
коммитом, требует интеграции с реальным wallet-store и MARKETPLACE_ASSET_CONFIG.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 816212cee0 revert(desktop/RailUserCard): убрать transition свёртки — baseline-skipping
Анимация max-height + opacity при свёртке давала видимый прыжок крупной
суммы баланса (`font-size: 22px` + `display: flex; align-items: baseline`):
во время промежуточных кадров baseline переcчитывался относительно
схлопывающегося контейнера → текст «прыгал в середину» и обратно.

Возвращаемся к canon `display: none`. Resize моментальный, без skipping.
Поведение rail (top опускается, bottom-chevron остаётся через flex-spacer)
работает и без анимации — пользователь это и подтвердил в фидбеке.

Chevron rotation через :deep(.q-icon) и balance-route не трогаем —
они независимы.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:35 +00:00
coopops 44f121a043 feat(desktop/_dev/ui): TokenPalette + chevron full-width fix
1. **Палитра системы.** Новая секция «00» в шапке dev-страницы со всеми
   основными --p-* токенами:
   - Акцент (accent / hover / press)
   - Поверхности (canvas / canvas-2 / surface / 2 / 3)
   - Текст (ink / ink-on-accent)
   - Линии (line / 1 / 2) — текстовый input для rgba
   - Статусы (pos / neg / warn / info)
   - Программные тинты (blagorost / wallet / generator)

   Color-picker для hex-токенов, текстовый ввод для rgba. Live-override
   через `document.documentElement.style.setProperty()` — inline-style на
   :root перебивает CSS-правила, перекрашивание мгновенное. Подсветка
   переопределённых токенов + счётчик в шапке + кнопка «Сбросить» (через
   removeProperty). Watch на Quasar Dark — при смене темы пикеры
   обновляются на новые resolved-значения.

2. **Chevron в свёрнутом виде — full-width.** Из-за того что для плавной
   анимации primary остаётся в DOM (max-height:0 вместо display:none),
   у него сохранялся `flex: 1` и он съедал ширину карточки → chevron не
   мог растянуться через canon-правило `width: 100%`. В `.is-collapsed`
   добавили `flex: 0 0 0` + `width: 0` для primary — chevron теперь
   корректно занимает всю ширину.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:35 +00:00
coopops fb43d9c2d0 fix(desktop/RailUserCard): chevron rotation, плавная свёртка, balance-route
По фидбеку:

1. **Chevron не переворачивается.** Canon-правило
   `.rail__usercard__collapse svg { transform: rotate(180deg) }` рассчитано
   на `<svg>`, а Quasar `<q-icon>` рендерит `<i class="q-icon">`. Прокидываем
   transition + rotate через scoped `:deep(.q-icon)` — chevron теперь крутится
   при свёртке/развёртке.

2. **«Карточка улетает»** — резкий jump при `display:none` создаёт визуальный
   прыжок. Подменяем display:none на `max-height: 0` + `opacity: 0` с
   transition'ом. Логика canon-селектора `.is-collapsed` не нарушается,
   просто добавлена плавность. Если структурное поведение rail (top
   опускается, bottom-chevron остаётся на месте) всё равно неудобно —
   перевёрстаем во вторую итерацию.

3. **Клик по балансу → маршрут кошелька.** Новый prop `balanceRoute` —
   если задан, блок `.rail__balance` оборачивается в `<router-link :to>`.
   Параллельно эмитим `balance-click` — для аналитики или функционального
   handler'а. На витрине привязал к `/_dev/ui#wallet` чтобы проверить.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:35 +00:00
coopops 0e60b5444a feat(desktop/domain): RailUserCard + фикс RouteMeta для dev-роута
1. `RouteMeta.icon` объявлен обязательным в `src/env.d.ts` — добавили
   `icon: 'fa-solid fa-flask'` для dev-роута, иначе vue-tsc валится.

2. `RailUserCard` (`shared/ui/domain/RailUserCard`) — доменный
   общеплатформенный компонент мини-кошелька в нижней части drawer'а.
   Это будущий E11 (Волна 2), но логично вытащить раньше, чтобы основной
   layout можно было заменить целиком уже сейчас.

   Реализован по canon строго: rail__usercard с usertop+balance+actions,
   collapse-chevron справа с поворотом arrow при свёртке (через
   `.is-collapsed` модификатор + canon CSS), отдельный `rail__signout`
   блок (выходит вне `.rail__usercard` — рендерится сразу после, в том
   же `aside.rail` через footer-slot).

   Контракт строго dumb по архитектуре: только props/emits/slots.
   - props: name, role, avatarSrc, balance, symbol, balanceLabel,
     lockedBalance, lockedLabel, primaryActionLabel, collapsed,
     showSignout, signoutLabel
   - emits: primary-action, update:collapsed (v-model), signout
   - slots: usertop-extra, actions

   Coнnected-обёртки (с инжектом store/auth) — на уровне страницы,
   не здесь.

3. Витрина `/_dev/ui`:
   - Секция 12 (AppDrawer): заменили временный огрызок rail-demo__user
     на полноценный `<RailUserCard show-signout>` через footer-slot.
   - Секция 16 (Каркас приложения): аналогично — теперь у rail полноценный
     footer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:35 +00:00
coopops 9ebed71283 feat(desktop/layout): E4 — AppDrawer + AppHeader + PageHead + PageTabs
E4 «Layout-каркас» Волны 1: 4 структурные обёртки в `src/shared/ui/layout/`.
По архитектуре — только props/emits/slots, без store/router/api, без знания
о данных. Cтраницы передают menu-items, breadcrumbs, табы и активные ключи
сверху.

Состав:
- `AppDrawer` — canon `.rail`: solid surface, активный пункт soft-accent
  пилюлей + 2-px рейл слева. Поддерживает плоский список + секции
  (RailSection с eyebrow-меткой), opt-in ⌘K-кнопку, slot `footer` для
  user-card / logout.
- `AppHeader` — canon `.topbar`: бургер · crumb · `topbar__actions` ·
  `topbar__right`. Поддерживает простой title и multi-level breadcrumb.
  Глобальные действия через слоты `notifications`/`theme`/`wallet`/`profile`
  или общий `right`.
- `PageHead` — canon `.page-head`: eyebrow + title + subtitle + slot
  `actions`. Используется когда страница БЕЗ подстраниц.
- `PageTabs` — canon `.tabbar` + `.tab`: вкладки подстраниц с tab__count,
  опциональный hairline-разделитель и `tabbar__actions` справа.

Также в составе коммита:
- Убрали AuthCard из base — он композит (cap + body + head + footer),
  переедет в E6 Auth flow рядом с SignUp/ResetKey/Invite.
- BaseForm: gap полей 16→10px (тесные формы как в каноне).
- Витрина `/_dev/ui`: 4 новые секции (12-15) на каждый layout-компонент +
  секция 16 «Каркас приложения» — AppDrawer + AppHeader + PageTabs в живой
  связке без legacy QLayout/QDrawer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:35 +00:00
coopops 1148eac2ed fix(desktop/mono-platform): убрать HTML-теги <style> из canon CSS
Скрипт извлечения из bundled HTML оставил `  <style>...` в начале и
`</style>` в конце обоих файлов. CSS-парсер ловил `<` как ошибочный
селектор и пропускал блок до ближайшей `}` — съедая весь первый блок
правил.

В tokens.css первым блоком был `:root, [data-theme="light"]` со светлой
палитрой — она целиком игнорировалась. `[data-theme="dark"]` ниже
парсился нормально, поэтому тёмная тема работала, а светлая — нет:
кнопки primary/ghost/danger оставались с transparent background и
сливались с canvas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:35 +00:00
coopops e741b5646a feat(desktop): MONO Platform v2 — базовые компоненты дизайн-системы
Заносим эталонную дизайн-систему в проект — только базовые элементы для
дальнейшей компоновки. Source — MONO Design System.html.

Архитектура (по `_bmad-output/planning-artifacts/architecture.md`):
- `src/css/mono-platform/{tokens.css,components.css}` — canon токены и стили
  компонентов 1:1 из эталона; импортируются первыми в quasar.config css[].
- `src/shared/ui/base/<Имя>/{Vue,types,index}` — 14 базовых обёрток (Vue 3 +
  TS, native HTML над canon BEM, no Quasar-deps кроме Dark API в ThemeToggle).
  FSD-граница: только props/emits/slots, никаких store/router/api.
- `src/boot/ui.ts` — глобальная регистрация 14 базовых компонентов.
- `src/boot/theme.ts` — sync `html[data-theme]` ↔ Quasar Dark.
- Inter + JetBrains Mono в index.html.

14 базовых:
BaseButton, BaseInput, BaseSelect, BaseCard, BaseTable, BaseChip, BaseBadge,
BaseDialog, BaseBanner, BaseForm, EmptyState, Avatar, ThemeToggle, AuthCard.

Витрина `/_dev/ui` (dev-only) — каждая обёртка отдельной секцией над чистым
canon-канвасом, без layout-обёрток. Маршрут смонтирован на корневом уровне,
ВНЕ DynamicLayoutWrapper, чтобы legacy default.vue не перебивал стили.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:35 +00:00
ant 937d4e3fb6 Merge pull request 'Дизайн-волны 1–2: канон UI, формы, документы, навигация, шапка, кошелёк' (#25) from worktree-design-wave1 into dev
Build bootstrap container / build (push) Successful in 4m8s
Reviewed-on: #25
2026-05-27 09:15:38 +00:00
coopops bcb76b4cd9 merge: разрешены конфликты с dev — boot-скрипт под dev, кошельки под canon
Typecheck / desktop (pull_request) Failing after 8m35s
Typecheck / controller (pull_request) Successful in 13m22s
- clean_reboot.sh: версия origin/dev (унификация трёх reboot-скриптов)
- WalletProgramWidget.vue: canon-версия (WalletCard + EmptyState)
- ParticipantWalletsPage.vue: canon-токены --p-pos/--p-warn + value-blocked

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:14:37 +00:00
coopops a267a8a24b [design-wave1][@dark] feat: тактильная отдача (вибрация) при перезагрузке страницы
Boot-файл haptics: при reload страницы на устройстве с Vibration API (мобильный
браузер / PWA на Android) коротко вибрирует (15 мс). Reload определяется через
Navigation Timing (с фолбэком на legacy API), чтобы не срабатывать на первой
загрузке/навигации. Полностью безопасно для десктопа и обычных сайтов: где
Vibration API нет — вызов не делается, любая ошибка проглатывается, на загрузку
приложения не влияет. Только клиент (гард для SSR).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:05:34 +00:00
coopops 8135ea46a5 [design-wave1][@dark] fix: убрать паразитный горизонтальный скролл левого дровера
В левом дровере был маленький горизонтальный скроллбар, и трекпадом его можно
было «оттянуть», обнажая правую границу — дровер выглядел скроллируемым, хотя не
должен. Причина: у .rail собственный border-right (1px), который при content-box
даёт ~1px overflow внутри контента дровера. Фикс в .app-left-drawer: overflow-x
hidden + overscroll-behavior-x contain на .q-drawer__content, .rail приведён к
box-sizing border-box и width 100%.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:53:46 +00:00
coopops 50e60f5750 [design-wave1][@dark] fix: повестка — держать лоадер на «Утвердить» 3с после голоса
Если проголосовать и сразу нажать «Утвердить», утверждение падает с ошибкой —
бэкенд ещё не учёл голос из блокчейна. После успешного голоса держим состояние
загрузки пункта (а значит и кнопку «Утвердить» в :loading) ещё VOTE_SETTLE_MS
(3с), за это время голос успевает обработаться. На ошибке загрузка снимается
сразу. Заодно ручное мутирование processingDecisions + setTimeout-хак заменены
аккуратным реактивным хелпером setProcessing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:49:25 +00:00
coopops 92bd6919d8 [design-wave1][@dark] fix: повестка — скрывать пункт только при утверждении, не при голосовании
Повестка совета показывает все НЕутверждённые вопросы. Голос «за»/«против» не
должен убирать пункт — он остаётся неутверждённым, лишь помечается отметкой
голоса. Убрал ошибочное actedDecisionIds.add из onVoteFor/onVoteAgainst
(оставлен только тихий рефетч). Скрытие через actedDecisionIds оставлено только
в onAuthorizeDecision — там пункт исполняется и должен уйти из повестки без
возврата от отстающего поллинга.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:46:11 +00:00
coopops 63fdb74db5 [design-wave1][@dark] fix: повестка совета — без моргания и без «исчез/вернулся» при голосовании
Две проблемы при голосовании/утверждении в повестке совета:

1. Моргание всей страницы: QuestionsTable подменял список тремя
   скелетонами при любом loading=true, а пост-экшен loadDecisions вызывался
   без hidden → loading=true → список «моргал» в скелетоны и обратно.
   Скелетоны теперь показываются только на первой загрузке (loading &&
   !decisions.length), а пост-экшен рефетчи переведены в тихий режим (hidden).

2. «Исчез → вернулся → исчез»: повестка на бэкенде уже не отдаёт пункт, по
   которому проголосовал/утвердил, но данные из блокчейна доходят с задержкой,
   и поллинг успевал вернуть отработанный пункт. Добавлен локальный набор
   actedDecisionIds — проголосованный/утверждённый пункт прячется сразу и не
   возвращается, что бы ни подтянул отстающий рефетч. Просто, без оптимистичных
   merge-оверлеев.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:43:01 +00:00
coopops 5d837f2c20 [design-wave1][@dark] fix: push-подписка не должна вешать UI и шуметь красной ошибкой
Две независимые проблемы при выдаче разрешения на уведомления:

1. Фриз приложения: shouldShowDialog был computed с побочным эффектом —
   внутри геттера вызывался updateSupport(), присваивавший store.support
   новый объект на каждое чтение. После выдачи разрешения это давало каскад
   инвалидаций/ре-рендеров, забивавший главный поток (роутер менял URL, DOM
   «застывал»). Геттер сделан чистым, updateSupport() вынесен в showDialog().

2. Красная ошибка «Service Worker не готов в течение 5 секунд»: в dev без PWA
   SW намеренно не регистрируется, поэтому navigator.serviceWorker.ready
   никогда не резолвится. Теперь сначала проверяем getRegistrations() — при
   отсутствии SW выходим сразу и тихо (warn вместо error), а subscribe()
   возвращает false без FailAlert. Push при недоступности SW полностью
   изолирован и не влияет на работу приложения.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:34:59 +00:00
coopops 426a06a076 [design-wave1][@dark] feat: скрывать «Совершить взнос»/«Получить возврат» до подписи соглашения кошелька
Кнопки взноса и возврата на столе пайщика недоступны, пока пайщик не
подписал главное соглашение цифрового кошелька (type='wallet') — как и
сама карточка кошелька, которая не появляется без подписанного соглашения.

Геттер isWalletAgreementSigned в сторе Wallet читает тот же список
соглашений пайщика, что и RequireAgreements.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:22:42 +00:00
coopops c2bb718895 [design-wave1][@dark] copy: явная опциональность шага «Импорт пайщиков» (онбординг председателя)
«(опционально)» в заголовок + пояснение в описании: если есть пайщики —
импортируйте CSV, если нет — двигайтесь дальше. Чтобы шаг не воспринимался
обязательным.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:05:49 +00:00
coopops 04cf9dc3c4 [design-wave1][@dark] fix: ключ в Invite — выделенная панель вместо readonly-инпута
readonly outlined q-input рендерится с dashed-рамкой (дефолт Quasar) и
обрезал значение (…UJR4) — выпадало из канона и выглядело плоско.
Сделал ключ surface-2 панелью: eyebrow-лейбл + copy-иконка в шапке,
mono-значение целиком (word-break) ниже. Даёт вес и убирает «ёлочку».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:01:41 +00:00
coopops d243bf5621 [chatcoop-hide-matrix-room-id][@ant] fix(security): не отдавать matrixRoomId на фронт — комнаты секретаря и транскрипции по opaque id
Typecheck / desktop (pull_request) Successful in 13m48s
Typecheck / controller (pull_request) Successful in 13m9s
matrixRoomId — это одновременно идентичность и capability: зная его, можно
попытаться войти в комнату. Раздавая его на фронт (страница «Комнаты секретаря»
и список транскрипций отдавали его в каждой строке), мы по сути публиковали
handle для входа в приватные комнаты любому, кто видит раздел. Приватность тогда
теряет смысл.

Комнаты секретаря:
- ChatcoopSecretaryRoom и RemoveSecretaryRoomInput переведены с matrixRoomId на
  внутренний opaque id реестра (DB-ключ, по нему войти в Matrix-комнату нельзя);
- удаление резолвится по id → matrixRoomId на бэкенде; мутация больше не
  возвращает matrixRoomId;
- фронт: row-key/удаление по id, убран показ matrixRoomId в таблице;
- попутно фикс латентного бага из PR #34: parseKind не знал про 'secretary' и
  мапил такие комнаты в capital_project — из-за чего editable=false и удаление
  падало с «удалять можно только комнаты секретаря».

Транскрипции:
- CallTranscription больше не отдаёт matrixRoomId (на фронте он не отображался,
  но уходил в payload по всем комнатам, включая приватные). Остаётся roomId
  (LiveKit room name — внутренний, не Matrix-handle);
- input-фильтр GetTranscriptionsInput.matrixRoomId сохранён: это вход, не утечка,
  и им пользуется blago-cli;
- blago-cli: рендер транскрипций больше не зависит от tr.matrixRoomId (строка
  «Matrix room» убрана, иначе pull и restore рассогласовались бы — restore не
  имеет контекста комнаты). Заодно поправлен fallback курсоров.

Вне scope намеренно: chatcoopListNonProjectCommunicationRooms и per-room запросы
оставлены с matrixRoomId — их зовёт только blago-cli (доверенный инструмент
председателя), на фронт они не уходят, и matrixRoomId там — операционный ключ
синхронизации.

logger.ts: при CONTROLLER_SCHEMA_GEN не подключаем файловые транспорты winston
(codegen рядом с работающим контейнером падал на EACCES в root-овый logs/).

schema.gql + zeus (sdk и controller) перегенерированы и закоммичены — иначе CI
typecheck собирает sdk против старого zeus и падает.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:59:34 +00:00
coopops 5dec964a83 [design-wave1][@dark] fix: канон-композиция карточки «Сохраните ключ» (Invite)
Прошлый вариант разъезжался: «Скопировать» висел сиротой справа, провалы
между блоками, узкая потерянная кнопка, карточка 560px — разрежено.
Переделал по канону:
- копирование ключа — иконкой content_copy в append самого поля (+tooltip)
- кнопка «Установить ключ» — full-width (block), как «Войти» в LoginForm
- ширина карточки 480 как у остальных auth-карточек
- чекбокс слева, плотный ритм без лишних margin (reserve-hint-space)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:57:58 +00:00
coopops dd2ebc2983 [design-wave1][@dark] copy: полные примеры ОПФ+ в placeholder (с «Социального Комплекса»)
Голый «Потребительский Кооператив» в примере провоцировал вводить просто
ОПФ без расширения. Дал полный пример «...Социального Комплекса» во всех
трёх падежах — чтобы вводили расширенную ОПФ+, а не базовую форму.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:51:44 +00:00
coopops 3f6c2538fc [design-wave1][@dark] copy: шаг «Настройка фабрики документов» вместо «Переменные документов»
Это не переменные, а постоянные параметры, которыми настраивается фабрика
документов кооператива. Переименовал label и intro шага.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:49:33 +00:00
coopops 81cb16eb61 [design-wave1][@dark] revert: убрать глобальный margin-bottom у .q-field--with-bottom
Микроотступ стал лишним и снова раздвигал поля слишком широко — откатываю.
Разделение полей обеспечивает сам reserve-hint-space, дополнительный
глобальный отступ не нужен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:48:17 +00:00
coopops 5de5e07dae [design-wave1][@dark] fix: примеры-подсказки в placeholder вместо hint (SetVariablesForm)
Постоянный hint под полем занимал место и отвлекал. Перенёс примеры
наименований ОПФ+ в placeholder (видны только в пустом поле), убрал hint.
Добавил reserve-hint-space всем q-input формы — строка под ошибку
резервируется без текста, валидация не вызывает layout shift.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:42:45 +00:00
coopops 5470e9ad89 [design-wave1][@dark] fix: системный микроотступ под полями с hint/error
Без зазора текст hint/error верхнего поля почти касался границы нижнего.
Вернул маленький отступ, но системно и один раз — глобально в quasar-canon:
margin-bottom: var(--p-1) только полям .q-field--with-bottom (те, что
резервируют нижнюю строку). Работает везде (BaseForm, формы установки,
регистрация), в стеках не складывается в дыру, инлайновые поля не трогает.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:22:06 +00:00
claude 8ac3f06928 booter fix 2026-05-27 12:17:17 +05:00
coopops 6c1b6262b2 [design-wave1][@dark] fix: убрать двойной зазор между полями форм установки
reserve-hint-space у q-input/BaseInput уже даёт ~24px снизу под error/hint;
дополнительный gap в стеках полей складывался с ним → избыточное расстояние.
Убрал gap (канон BaseForm__body):
- CreateOrganizationDataForm / IndividualDataForm (.user-data-stack)
- SetVariablesForm (.vars-section__fields)
- RequestKeyForm (.request-key)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:13:35 +00:00
coopops 618349ce91 [design-wave1][@dark] fix: dense-инпуты в формах установки кооператива
Канон-плотность полей (dense) была только в BaseInput; общие формы
CreateOrganizationDataForm и IndividualDataForm + поля шагов установки
рендерили крупные не-dense q-input. Добавил dense:
- CreateOrganizationDataForm (22 поля) — данные организации
- IndividualDataForm (6 полей) — члены совета
- email-инпуты SetInitForm/SetSovietForm и поля SetVariablesForm

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:09:47 +00:00
coopops 63d95f429d [design-wave1][@dark] feat: страница установки кооператива в канон
- InstallCooperativePage: VerticalStepper вместо q-stepper, canon-панель
  с accent-стрипом, экран завершения в каноне (без градиентов/shimmer)
- RequestKeyForm: BaseInput (mono) + BaseButton
- SetInitForm: canon info-нотки, кнопки Назад/Далее на BaseButton
- SetSovietForm: canon-карточки членов совета вместо q-card/q-badge
- SetVariablesForm: canon-секции, q-input outlined с правилами, BaseButton
- Invite: осмысленное тело AuthCard при отсутствии токена приглашения

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 05:36:48 +00:00
coopops 39a9727a93 [design-wave1][@dark] fix: убрать двойной отступ сверху на странице расширения
.extension-page уже внутри .catalog-shell__content с padding var(--p-6);
собственный padding давал двойной зазор от шапки до кнопки «Назад».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:23:54 +00:00
coopops 1841c3ebaa [design-wave1][@dark] style: панель вокруг контента расширения + «Отменить» в настройках
- Весь контент страницы расширения обёрнут в канон-панель (surface+border),
  больше не лежит на голом фоне
- В режиме настроек добавлена кнопка «Отменить» (CancelButton) рядом с
  «Сохранить» — явный выход без сохранения; кнопка «Назад» страницы тоже работает

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:13:32 +00:00
coopops 46617a1430 [design-wave1][@dark] style: страница расширения — назад на уровень страницы, центровка лого/столов, вкл+удалить в строку
- Кнопка «Назад» убрана из шапки (useBackButton снят), добавлена канон-кнопка
  под шапкой на самой странице
- Логотип AutoAvatar центрирован в колонке
- DesktopsList: центрированный inset-блок (выделяется), а не плоский список
- ext-actions toggle: «включено» и «удалить» снова в одной строке

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:07:41 +00:00
ant 4c4c611a3d [nodemon-restart-storm][@ant] fix: debounce nodemon + SIGTERM в controller/nodemon.json — устранить restart-storm, вешавший coopback на bind-mount
Без delay nodemon реагировал на каждое fs-событие пачки (правки/сборка/git на хосте) десятками рестартов в секунду; pstree.remy без `ps` в образе виснет на обходе /proc (накапливались зомби-сканеры), холодный старт ts-node прерывался, порт 2998 не занимался — бэкенд молча лежал. delay:2000 схлопывает пачку в один рестарт, signal:SIGTERM даёт чистое завершение дочернего процесса.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:04:05 +00:00
coopops a42210e5b8 [design-wave1][@dark] feat: страница расширения и его настройки в канон
- ExtensionPage: двухколоночный канон-layout с padding; q-img заменён на
  генеративный логотип AutoAvatar (animated), консистентно с каталогом
- ExtensionInfo: канон-заголовок + статус-бейдж, теги убраны
- ExtensionActions: компоновка кнопок в столбец на токенах, teal→primary
- ExtensionInstall: BaseCard вместо .info-card/text-h5
- DesktopsList: text-grey/subtitle → канон-токены
- SaveButton/SettingsButton/InstallButton: teal→primary, no-caps

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:00:52 +00:00
coopops 7e4cb15207 [design-wave1][@dark] style: анимация логотипа по наведению на всю карточку
Триггер ринг-анимации перенесён с hover самого SVG на hover карточки
(.app-card:hover :deep(.ring-seg)).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:54:11 +00:00
coopops 0759793fb0 [design-wave1][@dark] feat: анимация логотипа приложения по наведению
AutoAvatar получил режим animated: инлайн-SVG вместо <img>, каждый
сегмент-дуга обёрнут в .ring-seg. По hover логотипа сегменты на ~1.3с
расходятся — прямые и зеркальные (matrix-flip) от одного поворота идут
в разные стороны, со стаггером по индексу. CoopCard не затронут (img/blob).
Уважает prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:51:43 +00:00
coopops 1b9fbabac5 [design-wave1][@dark] style: убрать фиксированный резерв высоты заголовка карточки
Статус идёт сразу под заголовком (динамично) — без дыры под однострочными
названиями. Обрезку в 2 строки оставили, чтобы длинный заголовок не ломал сетку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:46:22 +00:00
coopops 717eae8540 [design-wave1][@dark] style: выровнять карточки приложений при переносе заголовка
Заголовок резервирует 2 строки (clamp+min-height), аватар по верху —
статус и описание на всех карточках начинаются на одной линии, не прыгают.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:43:45 +00:00
coopops c456602763 [design-wave1][@dark] style: компоновка карточки приложения — заголовок и статус рядом с логотипом
Верхний ряд: логотип + (заголовок, под ним статус); описание во всю
ширину ниже; футер «Подробнее» прижат к низу для выравнивания карточек.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:40:13 +00:00
coopops a36f5a63a9 [design-wave1][@dark] style: приглушить генеративный логотип каталога
Палитра колец → мягкие приглушённые тона + CSS saturate(.5)/opacity .85,
чтобы знак читался как тихая текстура, а не неоновое пятно.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:35:26 +00:00
coopops 6eced283ec [design-wave1][@dark] feat: генеративный логотип (DiceBear rings) в карточке приложения
- AutoAvatar параметризован: props size/radius/background/ringColor
  с обратно-совместимыми дефолтами (CoopCard не затронут)
- ExtensionCard: буква-монограмма заменена на уникальный rings-логотип,
  seed = имя расширения; цвет кольца детерминирован по seed из палитры

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:29:36 +00:00
coopops 6890b13387 [design-wave1][@dark] feat: каталог приложений в канон — табы на 2-й уровень, карточки без изображений
- «Магазин приложений» → «Каталог приложений» (drawer + breadcrumb)
- Витрина/Установленные перенесены из топбара в SecondLevelTabs
- ExtensionCard переверстан: убраны изображение и теги, добавлены
  монограмма-плитка, статус-бейдж, обрезка описания, футер «Подробнее»
- Quasar .row/.col-md-3 → канон auto-fill грид

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:19:38 +00:00
ant d8c24ada78 [fix-ext-config-lost-update][@ant] fix: убрать lost-update конфигов расширений в периодических задачах — cron-планировщики перезаписывали config устаревшим in-memory снимком с boot и стирали онбординг-флаги/хэши председателя
extensionRepository.update заменяет config JSONB целиком; chairman/powerup/meet-tracker писали захваченный при initialize снимок поверх свежих данных. Теперь read-modify-write по актуальному config из БД с наложением только владомых полей.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:09:00 +00:00
coopops 18f8e2a86c [design-wave1][@dark] fix: вёрстка формы провайдера платежей + текст контактов
- PaymentProviderForm: убраны вложенные q-gutter (кривой левый отступ
  селекта), standout=bg-teal → outlined, text-grey-7 → токен; чистый
  канон-столбец (селект max-width 480, hint ink-2, действия в ряд, no-caps).
- ChangeContacts: текст уточнён — контакты видны всем (раздел «Контакты
  кооператива» пайщикам + подвал сайта незарегистрированным), не только
  незарегистрированным.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 17:03:04 +00:00
coopops abaf0cbdab [design-wave1][@dark] feat: канон для остальных страниц стола председателя
Кооперативные участки, Регистрационные взносы, Ключ кооператива,
Провайдер платежей, Контакты кооператива:
- убран дублирующий заголовок страницы (есть крошка в шапке);
- описание перенесено с голого фона на канон-поверхность .banner;
- page-shell/hero/surface-card с хардкод-px → канон-отступы и токены;
- q-input/q-select standout=bg-teal → outlined color=primary.
Также удалён лишний хвост текста на Стартовых страницах.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:54:16 +00:00
coopops 3560839ef8 [design-wave1][@dark] fix: убрать дубль заголовка на Стартовых страницах и Членах совета
Заголовок страницы уже показан крошкой в шапке — повтор h2 убран.
Пояснительный текст вынесен с голого фона на канон-поверхность (.banner
с инфо-иконкой). ApprovalsPage не трогаем — там заголовка страницы нет.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:48:07 +00:00
coopops 4a4024f3f4 [design-wave1][@dark] feat: канон для Запросов одобрений, Стартовых страниц, Членов совета
- ApprovalsPage: канон-отступы страницы, фильтр-селект teal standout → outlined.
- SystemSettingsPage: hero-card/surface-card с хардкод-px → канон page-head
  (h2 + sub на токенах) + q-card(flat); канон-отступы.
- MembersPage: тот же канон-паттерн вместо hero-card с хардкодами.
- DefaultPagesForm: 4× q-select standout=bg-teal → outlined color=primary.
Виджеты таблиц/состава совета без экзотики — канон-тема через quasar-canon.css.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:39:24 +00:00
coopops fee0a27b3f [design-wave1][@dark] fix: убрать Vue-warn о class в WorkspaceSwitcher
Шаблон двухкорневой (кнопка + Teleport затемнения), class из родителя
(LeftDrawerMenu) не наследовался автоматически. inheritAttrs:false +
v-bind=$attrs на корневую кнопку — атрибуты направлены явно.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:36:25 +00:00
coopops ac20b270eb [design-wave1][@dark] style: согласовать типографику диалога повестки
Подписи секций «Вопрос на повестке»/«Проект решения» переведены в
надстрочные метки (uppercase, трекинг, ink-3) — отдельный ярус ниже
заголовка окна, чтобы не казаться конкурирующим заголовком рядом с
крупным заголовком самого документа. Текст вопроса — читаемый body.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:28:55 +00:00
coopops 90c733120d [design-wave1][@dark] fix: канон-отступы и чистка онбординга председателя
- ConnectPage: добавлены канон-отступы страницы (var(--p-6)/var(--p-4)) —
  класс .padding в проекте не определён, контент липнул к краям.
- OnboardingStepsCard: убран q-card (двойной паддинг), текст-интро
  растянут на полную ширину (снят max-width 70ch), вокруг кнопки шага
  убрана лишняя рамка (.step__content → .step__action, только отступ).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:20:05 +00:00
coopops b6b1f06948 [design-wave1][@dark] feat: онбординг председателя в канон (вертикальный степпер + текст + диалог повестки)
- OnboardingStepsCard: под заголовком «Адаптируйте кооператив…» добавлен
  поясняющий текст (что происходит дальше: решения совета в электронной
  форме, вступление в «Восход», импорт пайщиков, общее собрание) + плашка
  срока адаптации с отсчётом.
- Чек-лист шагов переведён на канон-вертикальный степпер (.stepper--v):
  пройденные — галочка/заливка primary, текущий — подсветка номера,
  действия — канон BaseButton; agenda/import/meet типы сохранены.
- Диалог «Предложение повестки» (объявить собрание совета) переведён
  с ModalBase на канон BaseDialog + BaseButton.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:06:00 +00:00
coopops 32afc40147 [design-wave1][@dark] feat: хлебные крошки в шапке + скрыта кнопка «Перевести»
- CommonHeader: крошка строится из route.matched (путь внутри стола),
  напр. «Отчётность › Календарь»; корень стола не выводится; плоские
  страницы дают одну крошку как прежде; pageTitleOverride сохранён.
- CoopWalletsPage: переводы временно скрыты под флагом transferEnabled=false
  (открывались с L3-кошельков пайщиков; вернём для кооп-кошельков позже).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:59:08 +00:00
coopops 93d2c6ce77 [design-wave1][@dark] style: канон-токены в редакторе отчёта (ReportEditorDialog)
- editor-container/action-panel/backdrop/validation-badge/mark-hint: hex и rgba → --p-* токены
- text-grey-8 → t-muted; box-shadow/overlay через токены с fallback

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:30:47 +00:00
coopops 099f7b5aa2 [design-wave1][@dark] style: канон-токены на списочных страницах бухстола + cells
- Operations/Postings/Accounts: канон page-padding, убран двойной q-mt-md, text-grey-* → t-faint/t-muted, hex-границы → токены
- Settings: канон-padding, save-bar на токены (--p-surface/--p-line/--p-pos), text-grey-7 → t-muted
- AccountIdCell/WalletIdCell: text-grey-6 → t-faint; DirectionCell: move-иконка на --p-ink-3
- PROCESS_COLORS доменного цвет-кодирования операций сохранены (это фича)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:28:50 +00:00
coopops 6e9d466172 [design-wave1][@dark] style: канон-токены на столе бухгалтера — кошельки, календарь, отчётность
- CoopWalletsPage/ParticipantWalletsPage: rgba/var(--q-*) → канон-токены (--p-pos/--p-warn/--p-ink-*)
- ReportsCalendar/CalendarCell: статусы и грид на канон-палитру, убраны body--dark дубли
- Documents подстраницы: убран двойной padding, q-chip→BaseBadge, таблицы в канон-карточку

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:24:33 +00:00
coopops 511e9ab2ad [design-wave1][@dark] feat: канон-меню второго уровня (SecondLevelTabs) на столе бухгалтера
- SecondLevelTabs на глобальных .tabbar/.tab — sub-навигация между топбаром и контентом
- WalletsPage/DocumentsPage shell-страницы: вкладки в .tabbar вместо RouteMenuButton в топбаре
- TransferWalletsButton: канон header-кнопка (q-btn primary)
- WalletTransferDialog: на BaseDialog + BaseButton, токены вместо rgba

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:19:33 +00:00
coopops 9373dfe847 [design-wave1][@dark] fix: канон-поля ZodForm и убран дубль заголовка «Настройки»
ZodForm: убран standout="bg-teal text-white" с динамического поля —
QInput/QSelect теперь канон (outlined+dense+color=primary+reserve-hint-space),
как BaseInput. SCSS на канон-токены (вместо неопределённого --q-primary-rgb).
ExtensionSettings: снят заголовок «Настройки» внутри карточки — он дублировал
«Настройки аренды» в шапке.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:01:36 +00:00
coopops e163bda08f [design-wave1][@dark] fix: фильтры лога на BaseInput вместо сырого q-input
Канон-инпут вместо q-input: BaseInput расширен поддержкой type="date"
и clearable (+ emit clear) — это нужно для фильтра по датам. Фильтры
в ExtensionLogsList переведены на BaseInput.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:55:44 +00:00
coopops 5257d5c6e6 [design-wave1][@dark] feat: настройки и лог расширения в канон
ExtensionSettings: EmptyState + BaseCard вокруг ZodForm вместо самописных
header/empty-state. ExtensionLogsList (используется только powerup):
q-table → список канон-карточек с фильтром по датам, скелетоном,
«Загрузить ещё» и EmptyState; слот #log-item и API сохранены.
Канон-падинги страниц настроек/лога аренды.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:52:04 +00:00
coopops 6184102022 [design-wave1][@dark] feat: Стол вычислительных ресурсов в канон — раскладка, виджеты, лог
Монитор ресурсов: баннер-предупреждение на всю ширину сверху, ряд
«Как это работает» (2/3) + кошелёк AXON (1/3), ряд CPU/NET/RAM.
Убран 3D-флип карт (он же «прыгал») — детали раскрываются inline.
Все виджеты/страницы/лог сведены к канон-токенам surface/line/ink.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:44:33 +00:00
coopops ba1e17c991 [design-wave1][@dark] fix: отступы действий платежей и ширина даты в документах
- Реестр платежей: кнопки действий больше не block (не заполняют ячейку
  от левого края, не липнут к бейджу статуса). Стопка inline-flex,
  равная ширина кнопок (align-items:stretch), прижата к правому краю
  колонки (col-action 168px, text-align:right) — слева остаётся воздух.
- Реестр документов: col-date 116→156px — дата «26.05.2026 08:25» больше
  не налезает на заголовок документа; min-width 720→780.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:29:48 +00:00
coopops fdd38951b5 [design-wave1][@dark] fix: реестры платежей/документов и форма взноса — ширины и кнопки
- Bank.vue: панель платежа max-width 380px по центру — длинное «Назначение»
  больше не раздувает диалог «Совершите взнос» до max-content; значение
  переносится по строкам (overflow-wrap: anywhere).
- Кнопки статуса платежа SetOrderPaid/RefundedStatusButton → BaseButton
  (primary/danger, block), подписи «Подтвердить»/«Отклонить»; диалог
  вынесен из-под кнопки.
- Реестр платежей: кнопки действий друг под другом (.cell-actions),
  колонка действий 156px, дата без переноса (.col-date 132px), min-width
  таблицы уменьшен.
- Реестр документов: сужены колонки подписей (220→150), действий
  (110→56), id/дата; min-width 880→720 — освобождено место под
  наименование, таблица не вылазит за страницу.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:24:31 +00:00
coopops fca4e7fc9c [design-wave1][@dark] fix: реестры документов и платежей — канон-поля страницы
Таблицы документов и платежей уже на канон-токенах и самообрамлены
(.table-wrap). Не хватало полей страницы как в реестре пайщиков:
- PaymentsPage рендерил виджет голым, впритык к краям → q-page + 24/16px.
- ListOfDocumentsPage: q-page.padding (16px) → канон-24px.
- Убрана лишняя .row.justify-center обёртка в ListOfDocumentsWidget
  (.col-12 и так во всю ширину).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:04:26 +00:00
coopops fa21066161 [design-wave1][@dark] fix: диалоги «Добавить» и «Импорт» пайщиков в канон
AddUserDialog перестроен в канон-визард по образцу CreateMeetForm:
шапка-bar (surface+line) вместо bg-gradient-dark, VerticalStepper из трёх
шагов — Электронная почта → Тип и данные → Вступительный взнос, BaseButton
в подвале, outlined-инпуты. UserDataForm переиспользован целиком (общий,
не тронут). Опция начисления взноса — канон-блок вместо q-item/q-card.

ParticipantsImportDialog: добавлено описание-интро (раньше отсутствовало),
ModalBase/bg-gradient-dark заменён на канон-bar, выбор типа аккаунтов через
BaseRadioCard, dropzone на токенах вместо хардкод-цветов, секции и действия
в канон-обёртках, BaseButton вместо цветных q-btn. Логика парсинга/импорта
и таблицы превью/результатов сохранены.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:53:06 +00:00
coopops 4b1bedd2e8 [design-wave1][@dark] fix: реестр пайщиков — канон-поля и дыхание вместо встык-вёрстки
Страница получила канон-отступ (--p-6 / --p-4 на мобилке), таблица обёрнута
в обрамлённую surface-карточку (--p-line, --p-r-lg) вместо edge-to-edge.
Развёрнутые данные пайщика — спокойная вложенная панель на --p-surface-2
с отступами --p-5/--p-6, читаемой шириной формы (640px) и вертикальным
ритмом между полями. Снят full-height у таблицы (мешал внутри карточки).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:37:15 +00:00
coopops be7fc22928 [design-wave1][@dark] refactor: реестр пайщиков в канон + убран переключатель Данные/Документы
Развёрнутая строка пайщика теперь показывает только его данные;
документы пайщика живут в отдельном «Реестре документов» и здесь не дублируются.
ParticipantDetails упрощён (без q-tabs/ListOfDocumentsWidget), tab-логика
вычищена из таблицы и страницы. Мобильная ParticipantCard переведена на канон-токены
(убраны хардкод-цвета, CardStyles и q-badge статуса → disabled-checkbox).
Таблица сохранена, сортировка по дате вступления (DESC) без изменений.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 08:01:45 +00:00
coopops 0a229df206 [design-wave1][@dark] fix: повестка — номер вопроса на зелёной плашке вместо иконки, клик копирует
Убран EntityIdBadge. Зелёная плашка-аватар слева теперь несёт сам номер вопроса
(он же Decision ID): клик по плашке копирует id (тултип «Скопировать №»), hover —
инверсия в сплошной teal. Заголовок снова просто текст вопроса.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 07:14:43 +00:00
coopops c67fb4703d [design-wave1][@dark] fix: повестка — бейдж ID в строку заголовка перед текстом вопроса
EntityIdBadge перенесён из отдельной строки под ФИО в начало строки заголовка:
[#id] → сразу текст вопроса. Inline, vertical-align middle. Раньше висел снизу
и пусто растягивал верхнюю строку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 07:00:08 +00:00
coopops d213b1639d [design-wave1][@dark] feat: повестка — копируемый бейдж Decision ID под ФИО
EntityIdBadge с id вопроса под заголовком/ФИО: отображает #<id>, по клику копирует
чистый id (copy-on-click). Канон-компонент, клик не раскрывает карточку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 06:39:47 +00:00
coopops fc8a060d71 [design-wave1][@dark] fix: повестка — срок «Истекает …» в нижнюю полоску рядом с «Утвердить»
«Истекает через месяц» перенесён из строки под заголовком в постоянную нижнюю
полоску (footer): срок слева, «Утвердить» справа (только председателю). У обычного
пайщика — только срок, узкая панелька.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 06:35:02 +00:00
coopops e8466143a6 [design-wave1][@dark] fix: повестка — «Истекает …» и убран избыточный бейдж статуса
Срок теперь подписан «Истекает через месяц» (было просто «через месяц» — неясно).
Удалён статус-чип «Вы за/против» — направление голоса уже видно по подсветке кнопок,
бейдж был избыточным. Убраны связанные computeds и стили.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 06:31:13 +00:00
coopops 009cc42afd [design-wave1][@dark] fix: повестка — переосмысленная компоновка карточки вопроса
Кнопки голосования прижаты к правому краю строки. «Утвердить» вынесена вниз
отдельной строкой-footer (справа, hairline сверху) — больше не теснит данные.
Срок «через месяц» и статус-чип «Вы за» перенесены влево под заголовок/ФИО.
Клик по строке раскрывает документ, по кнопкам голосования и footer — нет.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 06:24:50 +00:00
coopops cfe0835eea [design-wave1][@dark] fix: повестка — органы управления в одну строку с инфо, не остров по центру
Карточка вопроса перестроена в единую горизонтальную строку: иконка + вопрос/ФИО
слева (flex), компактный блок органов управления (голосование + «Утвердить»),
срок + статус-чип и шеврон — справа, друг рядом с другом. Убрана центрированная
vote-зона с пустотой по бокам. Клик по строке раскрывает документ, по органам
управления (@click.stop) — нет. Подсказка «Утвердить» возвращена в tooltip.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 06:18:00 +00:00
coopops a86711c111 [design-wave1][@dark] fix: повестка — раскрытие документа по клику на шапку, не на органы управления
Шапка карточки (вопрос/ФИО/срок/статус) кликабельна целиком и раскрывает документ,
справа шеврон-индикатор. Органы управления голосованием — отдельная зона ниже шапки
(сиблинг, не аккордеон): клик по ним голосует/утверждает и документ не раскрывает.
Убрана отдельная кнопка «Документ» — раскрытие теперь по клику на шапку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 05:34:55 +00:00
coopops 9332bc63c8 [design-wave1][@dark] fix: голосование повестки на верхнем уровне + header-CTA совета
QuestionCard: VotingButtons + «Утвердить» вынесены на верхний уровень карточки —
голосовать можно без раскрытия; раскрытие («Документ») открывает только содержимое
документа. Истечение срока остаётся в шапке.
VotingButtons: вернул чек-индикатор «принято советом» (закрашивается при принятии)
вместо иконки verified.
Header-CTA совета (Предложить/Добавить/Импорт): убран push (q-btn--push не попадал
под канон-правило заливки primary → белый шрифт на нетиловом фоне), подписи с заглавной.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 04:39:25 +00:00
coopops 8217cd5299 [design-wave1][@dark] feat: повестка совета в канон — единый карточный список
QuestionsTable: q-table → карточный список (скелетоны + EmptyState).
QuestionCard: канон-поверхность, BaseButton «Утвердить», токен-чип статуса
вместо q-badge, локальное состояние раскрытия, без @import CardStyles и хардкод-hex.
VotingButtons: токены pos/neg вместо .text-red/.text-green/#666.
Страница: канон-padding вместо q-card flat.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 04:26:00 +00:00
coopops 44a0f7b132 [design-wave1][@dark] fix: единый регистр названий столов в переключателе
Названия столов в реестре workspace'ов заведены вразнобой («Стол
благороста», «Стол вычислительных ресурсов» vs «Стол Совета»).
Добавил text-transform: capitalize на подписи в WorkspaceSwitcher
(заголовок текущего стола + пункты выпадающего меню) и WorkspaceMenu
(карусель + диалог выбора) — первая буква каждого слова заглавная,
одинаково для всех и для будущих столов, без правки строк-источников.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:58:18 +00:00
coopops d120fff065 [design-wave1][@dark] fix: «Контакты» — единая сетка полей вместо сегментов
По фидбеку переработана композиция:
- убраны заголовки секций «Контакты»/«Руководство» и per-строчные
  иконки телефона/почты/адреса (из-за них значения «прыгали» вправо
  относительно ИНН/ОГРН);
- ИНН и ОГРН теперь в одну строку (адаптивная сетка полей);
- председатель поднят наверх к реквизитам, поле названо просто
  «Председатель совета»;
- контакты — те же поля, телефон/email как ссылки (tel/mailto), всё на
  единой левой кромке; одна тонкая линия делит реквизиты и контакты.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:45:45 +00:00
coopops 6c38298545 [design-wave1][@dark] fix: «Контакты» — единая раскладка строк во всех секциях
Реквизиты/Руководство больше не разносят подпись и значение к
противоположным краям (на полной ширине это давало пустой провал и
оторванные значения). Все секции теперь как ContactSheet: подпись
сверху, значение под ней, слева, hairline между строками — один ритм
по всей карточке.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:40:12 +00:00
coopops a8f9bf3c36 [design-wave1][@dark] feat: страница «Контакты» в канон + удалён мёртвый MeetQuorumIndicator
- ContactsPage: три цветные ColorCard (orange/indigo/teal/blue) заменены
  на единую спокойную canon-поверхность с секциями через hairline
  (Реквизиты / Контакты / Руководство). Контакты — через canon
  ContactSheet (копирование, mailto/tel). Заголовок и строки на токенах
  (--p-ink/-2/-3, --p-surface/--p-line/--p-r-lg), полная ширина (padding
  24/16px). Убраны rgba-фоны и .q-dark-хаки.
- Удалён неиспользуемый виджет MeetQuorumIndicator (нет импортов;
  явка/кворум живут в MeetInfoCard).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:36:09 +00:00
coopops c099373d00 [design-wave1][@dark] feat: тело деталей собрания в канон — повестка/голосование/результаты
Убрал токсичный teal из state-gated виджетов деталей собрания:
- MeetDetailsAgenda/Voting/Results переписаны на canon-поверхности
  (--p-surface/--p-line/--p-r-lg), section-heading с иконкой вместо
  центрированной градиентной линии; color-mix/--q-primary/.q-dark удалены.
- Голосование: q-btn color=primary → BaseButton variant=primary;
  опции голоса на токенах pos/neg/neutral-soft, баннер «уже голосовали»
  на --p-pos-soft.
- Результаты: итог решения — токен-чип (pos/neg/unknown), счётчики
  голосов на pos/neg/surface-2-soft.
- AgendaNumberAvatar: teal q-avatar → нейтральный квадрат surface-2/ink
  (общий для трёх виджетов — убирает teal сразу везде).
- MeetCardsList: отступ между карточками 16→20px.
- MeetDetailsPage: кастомный empty-state → canon EmptyState, снят
  @import CardStyles.

MeetQuorumIndicator оставлен как есть — мёртвый код (нет использований).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:28:50 +00:00
coopops 4d52302389 [E14][@dark-sun] feat(собрания): полная ширина страниц; тело деталей (MeetInfoCard) на canon
- ListOfMeetsPage / MeetDetailsPage: убран max-width:960px center — контент
  во всю ширину с canon-паддингом (24px/16px), как документы/платежи.
- MeetInfoCard: переписан с токсичного teal на canon — единая calm-поверхность
  с тремя секциями через hairline (Даты / Ведущие / Явка и кворум) вместо
  трёх вложенных цветных карточек; заголовки ink, проценты ink (не teal);
  дубль заголовка «Общее собрание № N» убран (он в шапке). Убраны
  var(--q-primary), color-mix, .body--dark, хардкод-rgba.
- MeetDetailsInfo: контейнер с .card-container → canon.
- MeetDetailsActions: q-btn color=primary → canon BaseButton.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:00:40 +00:00
coopops 093a31a9b5 [E14][@dark-sun] feat(собрания): повестка на шаге проверки канон; карточка списка канон; шапка деталей — back-link вниз, имя в заголовок
- CreateMeetForm: повестка на шаге «Проверка» — аккуратные canon-карточки
  (Вопрос / Проект решения / Приложения) вместо скомканного списка.
- MeetCompactCard: переписана на canon — нейтральный hover (без
  токсичного teal-свечения), ink-заголовок, canon-плитки/иконка;
  убраны @extend .card-container, var(--q-primary), .body--dark, color-mix.
- MeetStatusBanner: нейтральный canon-контейнер (surface-2 + line),
  цвет статуса несёт иконка; убраны хардкод-rgba и .body--dark.
- MeetCardsList: empty-state на canon EmptyState, skeleton на .skel.
- Детали собрания: кнопка «Назад» убрана из топбара (снят useBackButton),
  добавлен canon back-link под шапкой слева; название собрания выводится
  в заголовок шапки через новый desktopStore.pageTitleOverride
  (приоритет над route.meta.title; транзиентно, чистится при уходе).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:46:01 +00:00
coopops f560e7d2d0 [E14][@dark-sun] feat(собрания): форма созыва на canon-степпер; кнопка в шапке к канону; скрыта «Сверить»
- BaseDocument: кнопка «Сверить» временно скрыта через :hide-verify
  (обработчик @verify остаётся, вернуть = убрать флаг).
- CreateMeetButton: приведена к одобренному canon-паттерну (primary
  solid на десктопе, micro flat accent + tooltip на мобильном),
  убран legacy push/teal с белым текстом.
- ListOfMeetsPage: инжект кнопки переведён с useHeaderActions на
  Teleport #header-actions-host.
- CreateMeetForm: полностью переписана с перегруженной тёмной модалки
  на canon-визард — maximized-диалог + VerticalStepper (3 шага:
  Параметры → Повестка → Проверка). Canon outlined-поля, повестка
  аккуратными карточками, шаг проверки со сводкой. Убраны
  bg-gradient-dark / standout bg-teal / .body--dark / хардкод rgba.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:04:51 +00:00
coopops 32f1a89bc9 [E14][@dark-sun] feat(стол пайщика): скелетоны вместо спиннера в таблицах документов и платежей
Добавлен canon-компонент TableSkeleton (shared/ui/base) — повторяет
структуру .table-wrap/.table с реальными заголовками и мерцающими
плейсхолдерами (.skel) в ячейках. Каркас не дёргается при подгрузке
данных (calm-data, UX-DR2/UX-DR23). Заменил перекрывающий q-spinner
в DocumentsTable и ListOfPaymentsWidget.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:49:27 +00:00
coopops 24601722f7 [E14][@dark-sun] feat(стол пайщика): документы — ID копируемым бейджем, отдельные колонки Дата/Документ, подписи бейджами; поиск минимальный на мобильном
- DocumentsTable: наименование берём из meta.title (чистое, без даты/.pdf
  суффикса full_title); отдельная колонка Дата (meta.created_at) с сортировкой
  по block_num, по умолчанию свежие сверху; колонка Документ — только заголовок.
- ID — копируемый EntityIdBadge (показывает короткий хеш, копирует полный
  doc_hash по клику + иконка-affordance).
- Подписи — отдельные BaseBadge на подписанта (новый helper
  getSignersListFromDocumentPackage возвращает массив).
- EntityIdBadge канонизирован: токены вместо rgba/--q-accent/.q-dark; добавлен
  copyValue (показать одно — скопировать другое).
- SearchHeaderAction: на мобильном — минимальная round-dense иконка без подписи
  «Поиск» (label только на десктопе).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:18:33 +00:00
coopops fe0d23bd92 [E14][@dark-sun] feat(стол пайщика): документы и платежи — канон-таблица .table-wrap вместо q-table
Переделка после ревью: q-table давал не-канон вид, дёрганье при раскрытии
(virtual-scroll пересчитывал размеры + colspan не совпадал с числом колонок)
и уродливые мобильные карточки в grid-режиме.

- Статическая канон-таблица .table-wrap/.table (бордер+радиус+surface как
  карточка), table-layout:fixed → колонки не разъезжаются при раскрытии.
- Раскрытие строки — CSS-only (expand-row td colspan по числу колонок),
  без virtual-scroll → нет дёрганья шапки/колонок.
- Пагинация load-more (.table-foot + BaseButton «Загрузить ещё» + «1–N из M»)
  вместо infinite virtual-scroll.
- Мобильный: горизонтальный скролл таблицы (.table-scroll) вместо grid-карточек
  PaymentCard/DocumentCard — карточки удалены.
- Статусы платежей — BaseBadge (pos/warn/neg/info/neutral); направление —
  иконка + цвет --p-pos/--p-neg; хеш документа — mono.
- Действия платежей (SetOrderPaid/Refunded) скрыты на столе пайщика
  (hideActions=true) — они в реестре платежей; download документов сохранён.
- EmptyState + спиннер первой загрузки.

Виджеты общие с админ-контуром: load-more и горизонтальный скролл там тоже
уместнее jittery virtual-scroll.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:59:47 +00:00
coopops a23efb69a1 [E14][@dark-sun] feat(стол пайщика): документы и платежи в канон — статус-бейджи, токены, header-action на Teleport
- ListOfPaymentsWidget + PaymentCard: статус платежа через BaseBadge
  (canon-варианты pos/warn/neg/info/neutral) вместо хардкод q-color
  teal/orange/red/blue; убраны битый inline-стиль $toolbar-min-height,
  inline max-width, глобальный .q-list style-leak; цвета/тени/радиусы —
  на токены (--p-line/--p-ink-3/--p-shadow-card/--p-surface-2).
- DocumentsTable + DocumentCard: убраны битый inline-height, inline
  max-width, .q-dark-селекторы, --q-gray, хардкод rgba; тени/линии — токены;
  глобальный q-table__top style → scoped :deep; CardStyles import убран.
- ListOfDocumentsWidget: brand-leak toggle-color teal → primary.
- DocumentsPage: SearchHeaderAction со старого useHeaderActions store на
  canon Teleport (#header-actions-host), кнопка сама гейтится по features.search.
- DocumentsPage/PaymentsPage: канон-паддинг страницы.

q-table-движок (virtual-scroll/infinite-load/раскрытие строк/mobile-grid)
сохранён — виджеты общие с админ/председательскими контурами, регресс недопустим.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:31:56 +00:00
coopops 21be815e3b [E14][@dark-sun] fix(шапка): правая группа прилипала влево на страницах без действий — вернул margin-left:auto
Когда страница не телепортирует действия, .topbar__actions скрывается
display:none через :has(пустой host), но adjacent-селектор
.topbar__actions + .topbar__right всё равно матчился и обнулял margin-left
правой группы — она уезжала влево к крошке. Возвращаем auto в :has-правиле
(его специфичность выше). Также убрал лид-надпись на странице реквизитов.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:24:00 +00:00
coopops 29dab94a9a [E14][@dark-sun] feat(стол пайщика): удостоверение и реквизиты в канон — IdentityPanel/DataRow/BaseCard, header-action на Teleport
- ProfilePage (Удостоверение): IdentityPanel-шапка + BaseCard-секции (Учётная
  запись/Личные данные/Документы и реквизиты) на DataRow; убран CardStyles,
  хардкод rgba и .q-dark.
- PaymentMethods widget (Реквизиты): BaseCard на метод + DataRow + EmptyState
  вместо .info-label/.info-value и хардкод-цветов.
- PaymentMethodsPage: кнопка добавления реквизитов переведена с useHeaderActions
  store на canon Teleport (#header-actions-host), micro на мобильном.
- AddPaymentButton: триггер под canon micro-паттерн (как Deposit/WithdrawButton).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 12:13:32 +00:00
Alex Ant f20989e319 chore(release): publish
Build bootstrap container / build (push) Successful in 3m10s
Release / release (push) Successful in 25m29s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / trigger-mono-docs (push) Successful in 1s
Release / publish-packages (push) Successful in 14m27s
2026-05-25 16:55:38 +05:00
Alex Ant 2d3cd34e82 chore(release): publish
Build bootstrap container / build (push) Successful in 3m25s
Release / release (push) Successful in 25m24s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-05-25 16:55:18 +05:00
claude 46cb511e43 fix: remove stray node_modules symlink committed into branch
Симлинк node_modules -> /home/admin/mono-ai-2/node_modules был случайно
закоммичен и ломал pnpm install (ENOTDIR) на любой машине без этого пути.
Правило .gitignore 'node_modules/' (со слешем) ловит только каталог, не
симлинк-файл — добавлено правило 'node_modules' без слеша.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:22:31 +05:00
Alex Ant 1461ec466e Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev 2026-05-25 15:52:33 +05:00
coopops 61d53e7ea0 Merge remote-tracking branch 'origin/main' into dev
Build bootstrap container / build (push) Failing after 2m13s
2026-05-25 10:46:47 +00:00
coopops 25a917866c ci(release): вернуть webhook второй документации + переименовать токен в DOCS_DISPATCH_TOKEN
При удалении publish-docs я снёс не только мёртвый gh-pages-push, но и реальную
публикацию ВТОРОЙ документации (доки mono) через DOCS_DEPLOY_WEBHOOK_URL —
она отдельная от coopenomics и должна уезжать синхронно. Возвращаю её отдельным
lean-джобом trigger-mono-docs (только webhook, без gh-pages и mkdocs-сборки —
приёмник деплоит сам).

Токен GITEA_DISPATCH_TOKEN → DOCS_DISPATCH_TOKEN: Gitea резервирует префикс
GITEA_ для имён секретов, такой секрет не создать.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:40:30 +00:00
coopops c73e9b9e80 ci(release): дёргать coopenomics на ветке master (дефолтная), не main
Дефолтная ветка C9S/coopenomics — master; workflow_dispatch надо слать на
ref=master, иначе Gitea вернёт 404 по ветке.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:28:24 +00:00
coopops d7ad0d7251 ci(release): убрать publish-docs (gh-pages), триггер coopenomics через Gitea workflow_dispatch
После переезда на Gitea два downstream-джоба были наследием GitHub:
- publish-docs пушил site/ в gh-pages github.com под secrets.GITHUB_TOKEN
  (на Gitea — гитейный токен, невалиден) → Authentication failed. Pages
  устарел, доки теперь деплоит C9S/coopenomics. Джоб удалён целиком.
- trigger-coopenomics-docs слал repository_dispatch на github.com через
  peter-evans (COOPENOMICS_PAT пуст). Репо переехало в C9S/coopenomics на
  Gitea, а Gitea не имеет API для repository_dispatch — только
  workflow_dispatch. Заменено на curl к Gitea API workflow_dispatch с
  secret GITEA_DISPATCH_TOKEN; целевой workflow получит входы mono_sha/mono_ref.

publish-packages не трогаю — там нужен лишь NPM_TOKEN в секретах (E404 на
scoped-пакетах = отсутствует npm-авторизация).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:25:13 +00:00
ant 3f5859eb3e Merge pull request 'feat(chatcoop): комнаты секретаря + синхронизация непроектных комнат в blago' (#34) from feat/chatcoop-secretary-rooms into dev
Reviewed-on: #34
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-05-25 09:53:32 +00:00
coopops 46e3686550 [E14][@dark-sun] feat(шапка): крошка-название страницы слева + фикс переноса заголовка ModalBase
Крошка: в #crumb рядом с BackButton выводится route.meta.title текущей
страницы (тот же, что подсвечен в меню) — именно для неё действия и
сдвинуты вправо. Длинное название обрезается ellipsis в .topbar__crumb b.

ModalBase: q-bar получил авто-высоту, заголовок переносится (white-space
normal + overflow-wrap), крестик прижат к верху — на узком экране титул
больше не обрезается сверху.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 09:49:19 +00:00
Alex Ant a625fc79e1 Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev 2026-05-25 14:42:03 +05:00
coopops fb545b4122 [E14][@dark-sun] fix(WalletPage): micro-кнопки взноса/возврата в шапке на мобильном
Why: полноразмерные DepositButton/WithdrawButton в узкой мобильной шапке
раздувались — текст переносился в 2 строки, кнопки вылезали за высоту
topbar. micro-вариант (иконка + tooltip, flat/dense) и предназначен
для слота шапки.

What: Teleport-кнопки получают :micro='isMobile' (useWindowSize, <768px).
Мобильный — компактные иконки; десктоп — полные кнопки с подписью.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 09:41:18 +00:00
coopops e20ddb4a9c [E14][@dark-sun] fix(WithdrawButton): «получить возврат» → «Получить возврат» — заглавная
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 09:39:51 +00:00
claude 393ea148f0 chore(compose): run dev-сервисы от uid 1000, чтобы codegen не плодил root-файлы
coopback/cooparser/desktop монтируют ./:/app, а node:22-slim без USER пишет
от root — codegen (generate-client, schema.gql, logs, quasar-кэш) кладёт
root-owned файлы в дерево и ломает git checkout (git работает от ant без sudo).
Добавлен user: "1000:1000" + HOME=/tmp, чтобы вывод контейнеров принадлежал
хостовому ant. node_modules/.pnpm-store уже ant:ant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:54:31 +05:00
Alex Ant 9029fa1f9d chore(release): publish
Build bootstrap container / build (push) Successful in 3m17s
Release / release (push) Successful in 25m39s
Release / publish-docs (push) Failing after 9m10s
Release / trigger-coopenomics-docs (push) Failing after 5s
Release / publish-packages (push) Failing after 9m23s
2026-05-25 12:41:40 +05:00
coopops e640b6add6 chore(controller): синхронизировать controller/zeus с регенерацией
Typecheck / desktop (pull_request) Successful in 13m19s
Typecheck / controller (pull_request) Successful in 12m49s
generate-client пишет копию zeus и в components/controller/zeus — обновляю вместе
со schema.gql, чтобы не оставлять дрифт сгенерированных артефактов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 06:29:06 +00:00
coopops b2c56651f9 chore(controller,sdk): регенерация schema.gql + zeus под комнаты секретаря
Typecheck / desktop (pull_request) Successful in 13m29s
Typecheck / controller (pull_request) Successful in 13m0s
generate-schema + generate-client после добавления резолверов SecretaryRooms и
chatcoopListNonProjectCommunicationRooms. Без этого @coopenomics/sdk:build падал
в CI на отсутствующих в zeus типах (ChatcoopSecretaryRoom / CreateSecretaryRoomInput
/ RemoveSecretaryRoomInput / ChatcoopNonProjectCommunicationRoom) — общий build-шаг
обеих джоб typecheck. Локально sdk build (prebuild tsc --noEmit) зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 06:28:39 +00:00
coopops 5c6062420f feat(desktop/chatcoop): страница «Комнаты секретаря» на столе связи
Typecheck / desktop (pull_request) Failing after 7m26s
Typecheck / controller (pull_request) Failing after 6m28s
- entity SecretaryRoom (api/model/store) поверх SDK chatcoopListSecretaryRooms /
  chatcoopCreateSecretaryRoom / chatcoopRemoveSecretaryRoom
- SecretaryRoomsPage: список комнат реестра (тип, наличие секретаря, шифрование),
  создание комнаты (публичная/приватная) и удаление комнат секретаря
- маршрут chatcoop-secretary-rooms, доступ роли chairman+member
- системные/проектные комнаты — read-only (удаление только у kind secretary)

Затипизируется после generate-schema/generate-client (новые SDK-операции).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 05:56:57 +00:00
coopops d1ce3924d5 feat(sdk,blago-cli): SDK-операции комнат секретаря + pull непроектных транскрипций
SDK (рукописные обёртки над Zeus — затипизируются после generate-schema/client):
- queries: ListNonProjectCommunicationRooms, ListSecretaryRooms
- mutations: CreateSecretaryRoom, RemoveSecretaryRoom
- селекторы ChatcoopNonProjectCommunicationRoom / ChatcoopSecretaryRoom

blago-cli:
- pullNonProjectCommunicationArtifacts: переписка + транскрипции + memo комнат
  пайщиков/совета/секретаря в отдельную верхнюю папку rooms/<folder>/{messages,meetings}/
- курсор transcriptionLastEndedExclusiveByRoom (по matrixRoomId)
- системные комнаты — стабильные папки komnata-paishchikov / komnata-soveta;
  комнаты секретаря — slug + хвост хеша matrixRoomId

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 05:49:06 +00:00
coopops d7c2c90290 feat(chatcoop): комнаты секретаря + синхронизация непроектных комнат в blago
Задача 1 (синхронизация в blago):
- roomKind 'secretary' в реестре управляемых Matrix-комнат
- репозиторий: findAll + findNonProjectCommunicationRooms (всё кроме capital_project)
- inter-порт listNonProjectCommunicationRooms + тип InterNonProjectCommunicationRoomRef (kind)
- query chatcoopListNonProjectCommunicationRooms (members/council/secretary) для blago-cli

Задача 2 (комнаты секретаря, backend):
- SecretaryRoomManagementService: создание комнаты (public/private, всегда plaintext,
  force-join только секретаря, создатель — модератор) и удаление (kick секретаря + дерегистрация)
- SecretaryRoomsResolver: chatcoopListSecretaryRooms / chatcoopCreateSecretaryRoom /
  chatcoopRemoveSecretaryRoom; доступ chairman+member
- matrix-api: inviteUser / kickUser
- конфиг SECRETARY_ROOM_MATRIX

Принцип: секретарь присутствует только в backend-созданных комнатах; force-join в
произвольную чужую комнату запрещён (общий Synapse на все кооперативы).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 05:46:00 +00:00
coopops 5eb03c74c4 ci(release): retry на docker push (3 попытки) — гасит транзиентный DNS
registry-1.docker.io на runner'е изредка не резолвится (DNS-таймаут к
127.0.0.53), из-за чего push валит весь релиз уже после успешной сборки
образов. Обёртка dpush() повторяет push до 3 раз с паузой 10с во всех трёх
push-шагах (контракты, base, сервисы).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 04:18:33 +00:00
Alex Ant 64bb8a0be8 chore(release): publish
Build bootstrap container / build (push) Successful in 3m20s
Release / release (push) Successful in 25m42s
Release / publish-packages (push) Has been skipped
Release / publish-docs (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
2026-05-25 08:55:05 +05:00
Alex Ant a7c8f8cd17 Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev 2026-05-25 08:54:48 +05:00
coopops da00407a7c ci(release): доставить рантайм-либы CDT (libz3-4 и др.) при установке из .deb
clang-9 падал "error while loading shared libraries: libz3.so.4". Пакет cdt
объявляет только libcurl4-gnutls-dev, но бинари тулчейна (objdump -p NEEDED)
требуют libz3.so.4/libtinfo.so.6/libxml2.so.2/libz.so.1. В образе
dicoop/blockchain они были из сборки исходников, из .deb не тянутся —
ставим libz3-4 libtinfo6 libxml2 zlib1g явно.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:54:39 +00:00
Alex Ant cfb9398ec6 chore(release): publish
Build bootstrap container / build (push) Successful in 3m25s
Release / release (push) Failing after 2m29s
Release / publish-packages (push) Has been skipped
Release / publish-docs (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
2026-05-25 08:44:36 +05:00
coopops 966dcc45f2 ci(release): собирать контракты из cdt .deb прямо в job, без вложенного docker
Шаг компиляции контрактов в release.yaml падал под Gitea act_runner:
build-all.sh монтирует $(pwd):/project в sibling-контейнер, но job сам
исполняется в контейнере → хостовый демон не видит путь, /project пуст,
cmake падает "no CMakeLists.txt". Релиз не собирался зелёным (runs #144, #158).

- build_contracts_cdt.sh: чистый cmake/make без docker (общий источник флагов).
- build-all.sh: тонкая docker-обёртка над ним для локальной сборки.
- release.yaml: вместо pull образа + build-all.sh — установка cdt v4.2.0 .deb
  в окружение job'а + симлинк /cdt/build → /usr/opt/cdt/4.2.0 (сводит хардкод
  toolchain-путь CMakeLists без его правки) + вызов build_contracts_cdt.sh.
  Компиляция в самом job-контейнере, без вложенного docker. sudo-агностично —
  одинаково на GitHub-VM и Gitea-контейнере.
- Убран избыточный typecheck-гейт из release; typecheck.yaml остаётся PR-гейтом.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:42:34 +00:00
Alex Ant 25f10e6782 Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev
Build bootstrap container / build (push) Successful in 3m10s
2026-05-24 23:29:58 +05:00
Alex Ant 3d52d816a4 chore(release): publish
Build bootstrap container / build (push) Successful in 3m22s
Release / typecheck (push) Successful in 23m58s
Release / release (push) Failing after 2m27s
Release / publish-packages (push) Has been skipped
Release / publish-docs (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
2026-05-24 23:29:42 +05:00
claude 665c22fc87 fix(boot): унифицировать extra_reboot.sh и clean_reboot.sh с reboot.sh
Build bootstrap container / build (push) Successful in 3m14s
Все три reboot-скрипта теперь идентичны, кроме строки pnpm run boot[:extra|:clean]:
- wipe blockchain-data контейнером (docker run alpine rm) вместо sudo rm —
  работает без sudo на любой ноде независимо от владельца данных (на Pi нет
  passwordless sudo; на проде nodeos пишет данные под root). Единый способ с reboot.sh.
- monoredis в down -v и up -d — без него coopback падает на старте
  (getaddrinfo EAI_AGAIN monoredis → nodemon crash), и провайдер не получает
  org-данные partner1.
- coopback через up -d --force-recreate (перечитывает env), как в reboot.sh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:26:40 +05:00
claude b897627d72 fix(boot): partner1 провижининг под EXTRA_RENT, enrollment в ЦПП Кошелька, идемпотентная activateFeature
Build bootstrap container / build (push) Successful in 3m10s
- booter.ts: installExtraData (регистрация partner1 с auto-approve → триггер аренды
  провайдером) выполняется только под EXTRA_RENT=1. boot:extra используется и для
  других задач (пересев совета/чейна), где аренда VM не нужна.
- infra.ts: partner1 подписывает wallet-соглашение (wallet::signagree, program_id=1)
  при регистрации. Без членства в ЦПП Кошелька provider.performInitialTransfers
  (150 AXON на partner1 перед ACTIVE) падает ассертом eosio.token::is_can_transfer
  «Получатель не является участником целевой потребительской программы кошелька»,
  и инстанс навсегда висит в INSTALL.
- blockchain/index.ts: activateFeature не роняет boot при protocol_feature_exception
  (фича уже активна на не-обнулённом чейне) — ловим и продолжаем.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:13:39 +05:00
ant 72cec26c14 Merge pull request 'Убрать механику blocked из L3-кошельков (резерв возврата → отдельный кошелёк w.wal.wpend)' (#31) from feat/remove-l3-blocked into dev
Build bootstrap container / build (push) Successful in 3m21s
Reviewed-on: #31
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-05-24 17:32:03 +00:00
coopops 7a163389c6 chore(controller,sdk): полная регенерация schema.gql и zeus (generate-schema + generate-client)
Typecheck / desktop (pull_request) Successful in 13m28s
Typecheck / controller (pull_request) Successful in 13m9s
Штатный синк вместо точечной правки: schema.gql приведён в соответствие с
актуальными DTO контроллера (был протухший — накопленный дрейф полей,
не только blocked). zeus перегенерирован из новой схемы; оба
controller/zeus и sdk/src/zeus идентичны.

Результирующие изменения относительно L3-blocked-ветки:
- удалён blocked из Ledger2Wallet/ProgramWallet (цель PR);
- синхронизированы ранее не закоммиченные дрейфы: is_server_init в SystemInit,
  enum-значение AWAITING_AUTHORIZATION, актуализация описаний/ролей @Field.
- blocked сохранён в ChartOfAccountsItem (legacy chart-of-accounts).

Проверено локально: sdk typecheck и controller typecheck зелёные.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:11:32 +00:00
coopops 710e0cda8c [E14][@dark-sun] feat(стол пайщика): кошельки списком во всю ширину + действия шапки через Teleport
Кошельки (по фидбэку — переносы выглядят плохо):
- .wallet-programs из grid (repeat auto-fill minmax) → flex-column: карточки
  идут списком во всю ширину страницы.
- canon .wallet__title/__sub: возвращён nowrap + ellipsis (откат wrap-фикса);
  на широкой строке текст почти всегда влезает, иначе — ellipsis.
- WalletCard + минимум-карточка: нативный tooltip `title` — при наведении
  виден полный текст. Убран .wallet--row reset (базовый снова nowrap).

Действия шапки (кнопки взноса/возврата пропали; нужна новая механика):
- Новый canon-механизм: страница телепортирует свои действия в шапку через
  <Teleport to="#header-actions-host">. Host — постоянный span с
  display:contents в #actions слоте CommonHeader (при loggedIn).
- :has()-правило прячет .topbar__actions, когда внутри только пустой host
  (нет ни store-кнопок, ни телепорта) — чтобы не было пустого разделителя.
- WalletPage переведён на Teleport (DepositButton/WithdrawButton),
  useHeaderActions store-механизм убран со страницы.
- Старый useHeaderActions оставлен для прочих 10 страниц — мигрируем
  и удалим отдельно. DepositButton сам скрыт, пока пайщик не принят
  (status !== 'active') — это защита, не баг.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 16:45:39 +00:00
coopops 5c494db262 fix(sdk): синхронизировать schema.gql и zeus с удалением blocked из L3-кошельков
Typecheck / desktop (pull_request) Successful in 13m29s
Typecheck / controller (pull_request) Successful in 13m8s
CI Typecheck падал на @coopenomics/sdk:build (tsc): селекторы
programWalletSelector/ledger2WalletSelector и зависимые capital-селекторы
больше не содержат blocked, но zeus-типы (MakeAllFieldsRequired) всё ещё
требовали его как обязательное поле.

- schema.gql: убран `blocked: String!` из типов Ledger2Wallet и ProgramWallet
  (точечно, без полной регенерации — она тянет несвязанный дрейф схемы).
- zeus (controller/zeus + sdk/src/zeus): убран blocked из Ledger2Wallet и
  ProgramWallet во всех 4 секциях (ValueTypes/ResolverInputTypes/ModelTypes/
  GraphQLTypes). blocked в ChartOfAccountsItem (legacy chart-of-accounts) и
  статус-комментарии (accepted|blocked) сохранены.
- CoopCard.vue: удалён мёртвый закомментированный блок с wallet.blocked.

Проверено локально: sdk typecheck и controller typecheck зелёные.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:43:07 +00:00
coopops 0969505a71 feat(ledger2): убрать механику blocked из L3-кошельков, перевести возврат на отдельный кошелёк-резерв
Typecheck / desktop (pull_request) Failing after 6m36s
Typecheck / controller (pull_request) Failing after 6m30s
Упраздняем субсчёт "заблокировано" на кошельках. Резерв средств под заявку
на возврат паевого теперь выражается переводом на отдельный COOPERATIVE-кошелёк
w.wal.wpend, а не блокировкой внутри w.wal.share. Все движения — только переводы
и сжигание, без BLOCK/UNBLOCK.

Контракты (ledger2/wallet/capital):
- wallets.hpp: новый кошелёк WITHDRAW_PENDING (w.wal.wpend, COOPERATIVE).
- operations.hpp: из WalletOp удалены BLOCK/UNBLOCK/BURN_BLOCKED (числовые
  значения ISSUE/TRANSFER/BURN/NONE сохранены для совместимости истории).
  Переключены 3 операции возврата: o.wal.wthreq -> TRANSFER share->wpend;
  o.wal.wthdec -> TRANSFER wpend->share; o.wal.wthcpl -> BURN wpend.
- walletop.cpp/revert.cpp: удалены кейсы и валидации блокировки.
- migrate(): свёртка blocked->available по ВСЕМ коопам (выполняется автоматически
  при деплое; сигнатура без аргументов). Поле blocked в структурах таблиц
  оставлено deprecated — физическое удаление = смена layout таблицы на живых
  коопах, отдельный cleanup-деплой.
- capital balances/importcontr: больше не читают blocked.
- p.wal.wthdrw.standard.yaml: стандарт приведён к модели "резерв на кошельке".

cooptypes/SDK:
- operations.ts: WalletOp без BLOCK/UNBLOCK/BURN_BLOCKED; 3 операции возврата
  переключены; wallets.generated.ts регенерён (добавлен w.wal.wpend).
- selectors: убран blocked из programWalletSelector/ledger2WalletSelector.

Backend (controller): убрано поле blocked из GraphQL DTO ProgramWallet и
Ledger2Wallet. TypeORM-колонка/внутренние интерфейсы оставлены deprecated
(без DB-миграции).

Frontend (desktop): убраны все поля "Заблокировано" — WalletProgramWidget,
WalletWidget, ParticipantWalletsPage, CoopWalletsPage, CapitalWalletsCardsWidget,
ContributorsListWidget, optimistic blocked_delta.

Границы (не в этом PR): legacy soviet::progwallets blockbal/unblockbal и
donor-marketplace; legacy ledger v1 chart-of-accounts "Заблокированные средства";
полное удаление поля blocked из C++-структур таблиц.

Требует CI-шага: regen GraphQL schema + zeus client (generate-schema/-client)
для согласования schema.gql и zeus-типов с удалённым полем blocked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:07:11 +00:00
coopops 36694f648d [E11][@dark-sun] fix(CommonHeader): бренд-логотип inline-SVG в зелёном квадрате, как в личном кабинете
Why: на странице без логина логотип был <img :src='logo.svg'> — img рендерит
SVG в изоляции, не наследует currentColor, поэтому показывался чёрно-белым
и не реагировал на смену темы. В личном кабинете (WorkspaceSwitcher)
тот же logo.svg рендерится inline через v-html в зелёном квадрате
и наследует color (logo.svg на fill:currentColor) — «зелёненький
на зелёном фоне», одинаковый в обеих темах.

What: CommonHeader brand-slot переведён на тот же приём —
`logo.svg?raw` + v-html внутри .app-q-header__logo
(background var(--p-primary-soft), color var(--p-primary), 28px квадрат,
16px svg). Переключение темы больше не требуется — зелёный константен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 06:39:59 +00:00
coopops f20764adbd Merge branch 'dev' into worktree-design-wave1
Подтянуть hotfix controller TypeORM, SWC dev-builder и mongo standalone.
2026-05-23 05:33:20 +00:00
coopops 712080ff8f Merge branch 'dev' into worktree-design-wave1
Подтянуть свежие изменения dev в дизайн-ветку перед продолжением миграции стола пайщика.
2026-05-23 05:09:17 +00:00
coopops 76513b8526 [E14][@dark-sun] fix(canon .wallet): просто wrap по пробелам, без overflow-wrap: anywhere
Why: предыдущий фикс `overflow-wrap: anywhere; word-break: break-word`
ломал слова в любом месте даже когда колонка достаточно широкая —
«Минимальный неснижаемый остаток» рендерился по одному слову на строку,
несмотря на ~480px доступной ширины. Реально нужен только wrap по пробелам;
agressive break-word оправдан только для URL-подобных нерасчленяемых строк.

What: убраны `overflow-wrap` и `word-break` из .wallet__title/__sub —
браузер делает естественный wrap по пробелам, длинные заголовки переносятся
только когда не помещаются. `.wallet--row` reset для compact-варианта
оставлен — там по-прежнему single-line+ellipsis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:50:01 +00:00
coopops 0872c4c093 [E14][@dark-sun] fix(canon .wallet): разрешить перенос длинных заголовков на full-варианте + порядок карточек в WalletProgramWidget
Why: canon-стиль .wallet__title и .wallet__sub был с
`white-space: nowrap; overflow: hidden; text-overflow: ellipsis;` — на full-
варианте это резало длинные подписи («Минимальны…», «Возвращается п…»)
даже на просторных экранах, потому что grid-колонка `.wallet__main` сжимается
ради `.wallet__amount` справа. Эта обрезка будет всплывать на любой
длинной строке (метки программ, статусы пайщика, длинные subtitle).
Compact-вариант `.wallet--row` (слот шапки) должен остаться одной строкой.

What:
- .wallet__title/__sub: убран nowrap/ellipsis; добавлено
  `overflow-wrap: anywhere; word-break: break-word; hyphens: auto` (для title)
  и `overflow-wrap: anywhere; word-break: break-word` (для sub) — длинные
  заголовки переносятся на 2+ строки.
- .wallet--row: явно возвращает nowrap+ellipsis (с reset word-break/
  overflow-wrap), чтобы compact-ряды в шапке оставались строго одной строкой.
- WalletProgramWidget: карточка минимального неснижаемого остатка
  перемещена в начало grid'а (была после программ) — это базовая защита
  средств пайщика, логичнее видеть её первой.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:45:30 +00:00
coopops c94cb3759a [E14][@dark-sun] refactor(WalletCard,WalletProgramWidget): «Главный кошелёк» канон-default + минимальный остаток отдельной карточкой
Why:
- Заголовок «Кошелёк» для program='wallet' путал — в столе пайщика этот
  кошелёк семантически главный, и в любых других местах canon (MicroWallet,
  WalletCardMini, _dev/ui) он тоже должен называться полно — «Главный
  кошелёк». Лучше один canon-default, чем локальный override в каждом
  потребителе. Согласовано — переименование canon DEFAULT_TITLES.
- Минимальный неснижаемый остаток — НЕ баланс кошелька, а самостоятельная
  сущность пайщика (паевой взнос, возвращается при выходе). Пристегивать
  его DataRow-строкой под grid'ом — нелогично, как было и раньше в legacy.
  Правильнее — отдельной карточкой в той же сетке кошельков.

What:
- WalletCard.vue: DEFAULT_TITLES.wallet 'Кошелёк' → 'Главный кошелёк'.
  Глобально для всех потребителей canon-компонента.
- WalletProgramWidget.vue: убран TITLE_OVERRIDE и DataRow-блок минимального
  остатка. Карточка остатка теперь рендерится в общем .wallet-programs
  grid'е canon-разметкой `.wallet` с иконкой `savings`, заголовком
  «Минимальный неснижаемый остаток», подзаголовком «Возвращается при выходе
  из кооператива» и зарезервированной суммой. Нейтральная подсветка иконки
  через `.wallet--minimum { --prog-bg, --prog-fg }`, чтобы визуально
  отличалась от программ, но встала в общую сетку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:39:27 +00:00
coopops 40cbd3fef9 [E14][@dark-sun] feat(WalletProgramWidget): «Главный кошелёк» + минимальный неснижаемый остаток
Why: на старом WalletWidget пайщик видел свой минимальный неснижаемый остаток
(паевой взнос, возвращается при выходе из кооператива) — самостоятельная
сущность пайщика, не баланс кошелька. При переходе на canon я её упустил.
Канон-default WalletCard 'wallet' = «Кошелёк» — для стола пайщика этот
кошелёк семантически является главным (свободный остаток ЦК), поэтому
локально перекрываем заголовок на «Главный кошелёк».

What:
- TITLE_OVERRIDE['wallet'] = 'Главный кошелёк' — локальное перекрытие
  заголовка только в этом widget'е, canon DEFAULT_TITLES не трогаем
  (другие потребители WalletCard могут использовать общий «Кошелёк»).
- DataRow «Минимальный неснижаемый остаток» под grid'ом программ
  (только когда session.participantAccount.minimum_amount > 0)
  с hint «Возвращается пайщику при выходе из кооператива».
- Источник остатка — session.participantAccount?.minimum_amount,
  как и в legacy WalletWidget.

Note: locked-line уже рендерится самим canon WalletCard, когда
locked-balance > 0 (logic: hasBlocked ? blocked.amount : undefined
в WalletProgramWidget). Отдельная разметка под «Заблокировано» не нужна.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:30:00 +00:00
coopops cdb12efb62 [E14][@dark-sun] fix(WalletProgramWidget): vue-tsc — flatMap вместо filter+predicate
Why: type predicate `e is CanonProgramEntry` не сходился — optional `locked?: string`
в interface vs required `locked: string | undefined` в литерале map'а. TS считает
эти типы несовместимыми для predicate, хотя они эквивалентны при присваивании.

What: переход на `flatMap<CanonProgramEntry>` с возвратом `[]` для исключаемых
программ. Predicate не нужен, generic flatMap даёт точный тип результата.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:20:59 +00:00
coopops 7bad49522b [E14][@dark-sun] refactor(participant/wallet): WalletPage + WalletProgramWidget в канон MONO Platform v2
Why: первая страница стола пайщика (default route 'wallet') использовала legacy
обвязку — CardStyles import, scoped SCSS с .body--dark и hardcoded rgba цветами,
ColorCard с произвольным цветом по индексу. Канон уже знает программы платформы
(blagorost/wallet/generator) через WalletCard + токены --prog-*; на нём и строим.

What:
- WalletPage.vue: убран import 'src/shared/ui/CardStyles', scoped SCSS на
  токенах --p-6/--p-4; вырезана легаси-карточка «Минимальный остаток»
  (она не отображалась — не было разметки в template). useHeaderActions
  для Deposit/Withdraw оставлен — канон поддерживает actions через #actions slot.
- WalletProgramWidget.vue: переписан на canon WalletCard + EmptyState.
  Фильтр на канон-набор программ через ZEUS_TO_CANON
  (MAIN→wallet, BLAGOROST→blagorost, GENERATOR→generator);
  MARKETPLACE и прочие исключены — это не из основной тройки платформы
  и должны рендериться отдельным виджетом.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:14:41 +00:00
coopops 463801beed [E11][@dark-sun] feat(AppHeader): brand slot — лого + название кооператива на странице без логина
Why: на главную без логина нужно показывать бренд кооператива (логотип + имя)
рядом с глобальными действиями шапки; раньше выводилось только текстовое
название через title-проп без логотипа.

What:
- AppHeader.vue: опциональный slot #brand перед .topbar__crumb;
  hasBrand computed по slots.brand. Когда slot заполнен — крошка не рендерится.
- components.css: стили .topbar__brand (desktop + .topbar--mobile вариант)
  с canon-токенами (--p-fs-body, --p-ink, --p-fs-meta).
- CommonHeader.vue: на !loggedIn заполняет #brand src/assets/logo.svg
  + <b>{coopTitle}</b>; передача title-пропа убрана.
- default.vue: .fixed-top-right { top: 51px } → top: var(--p-topbar-h)
  (canon 56px) — выравнивание FAB под точную высоту шапки.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:14:22 +00:00
coopops f02df5a8d0 [E11][@dark-sun] refactor(CommandPalette): иерархия рабочих столов и страниц вместо generic pages/actions/recent
Существующая модель cmdk в проекте (entities/CmdkMenu/model/store.ts) —
иерархия рабочих столов и их страниц. Активный стол sticky сверху с
бейджем «Активный», без запроса показывается иерархия (стол + indented
страницы), с запросом — плоский список со столом-префиксом у каждой
страницы; стол отдельной строкой появляется только если запрос явно
начинается с его имени или содержит «стол»/«workspace».

Переписал canon CommandPalette под эту модель:
- Props: `workspaces: CommandPaletteWorkspace[]` вместо
  `commands: CommandItem[]`. Каждый workspace = `{ name, title, icon,
  isActive?, pages: CommandPalettePage[] }`. Page = `{ name, title,
  icon?, shortcut? }`.
- Emits: `select-workspace(name)`, `select-page(workspaceName, pageName)`
  — props-only, навигация и filtering по ролям/conditions остаются
  заботой connected-обёртки (миграция legacy CmdkMenu — отдельная story).
- Sticky-баннер активного стола, plus accent-soft фон + outline-обводка
  на selected, ↑↓ работает плоско поверх иерархии (стол → страницы → стол
  → страницы).

Mock-data в /_dev/ui/index.vue обновлён: три стола (Председатель/Пайщик/
Отчётность) со своими страницами вместо плоского списка команд.

Старый widgets/Desktop/CmdkMenu пока живёт параллельно — переключим в
ходе миграции Wave 3.
2026-05-22 18:41:46 +00:00
coopops e14f0b6a1d [E11][@dark-sun] feat: NotificationCenter, CommandPalette, DetailsDrawer — эпик «Навигация и нотификации»
Реализованы три props-only доменных компонента из E11:
- NotificationCenter — panel-content для popover в шапке: группировка
  notifications по category (system/financial/voting/message), unread-bullet
  через BaseBadge, кнопка «Прочитать все», EmptyState и «Показать все».
  Relative-date форматирование с русским склонением.
- CommandPalette — ⌘K/Ctrl+K с fuzzy-поиском, секции recent/pages/actions,
  ↑↓ навигация, Enter/Esc обработка. localStorage недавних — в connected
  обёртке, компонент props-only.
- DetailsDrawer — side-sheet справа 480px (override через :width), slots
  default/actions/footer, Esc и backdrop close, на xs — fullscreen.

Все три зарегистрированы в boot/ui.ts и локально импортированы в /_dev/ui
с mock-data в секциях 36-38.

E11.4 RailUserCard отложен из-за конфликта имён — существующий компонент
имеет другую роль; нужно согласовать канон-нейминг.
2026-05-22 18:24:55 +00:00
coopops bed7ba0a13 [E10][@dark-sun] fix(BaseDocument): центрировать spinner-лоадер по обеим осям
Заменил `.full-width.text-center` + flex без выравнивания на flex-column
со `min-height: 360px` — лоадер больше не прижат к левому верху карточки.
Заодно поправил опечатку «подговка» → «Формируем документ».
2026-05-22 18:13:07 +00:00
coopops 2de5f81997 [E10][@dark-sun] fix(AmountInput): нативный :suffix='symbol' вместо #append slot
Корень: я положил символ валюты в #append slot своим <span>, в обход
встроенного prop suffix. Quasar имеет правила позиционирования именно
для родного .q-field__suffix (см. revert d0a8dc0080 от 2026-05-19,
где было решено оставить Quasar дефолт — «нас устраивает»). Мой span
в append-slot не подчинялся этим правилам и сидел в произвольной
позиции относительно цифр.

Фикс:
- :suffix='symbol' — символ валюты идёт нативным механизмом Quasar.
- Удалил .amount-input__symbol класс и template #append вообще.
- Удалил override align-items: center на q-field__control/__append/
  __after (тоже мешал, как было показано в d0a8dc0080).
- font-weight 500 на цифрах оставил — нормальный вес поля.

Совет на будущее: использовать встроенные q-input props (suffix,
prefix), а не #append/#prepend slots, если можно — Quasar для них
держит готовое выравнивание, проверенное пользователем.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:04:21 +00:00
coopops 3c6fbba4ea [E10][@dark-sun] fix(AmountInput): выровнять цифры и RUB-суффикс по центру
Корень: font-weight: 600 + tabular-nums + Quasar dense нативный input
имеют чуть смещённую baseline относительно append-слота, где сидит
RUB. Визуально цифры лежали ниже суффикса.

Фикс:
- font-weight 600 → 500 (нормальный вес поля ввода, без bold-акцента
  на цифрах).
- Явный font-size + line-height на нативном input.
- align-items: center на q-field__control / __append / __after, чтобы
  суффикс и any after-слот (кнопка «макс») центрировались по высоте
  входной полосы.
- align-self: center на самом __symbol — на случай если q-field__append
  кто-то переопределит как stretch.

TODO на следующий подход: BaseDocument loader («Формируем документ…»)
выравнивать по центру рамки документа (сейчас стоит сверху).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:47:59 +00:00
coopops a67180a153 [E10][@dark-sun] fix(OtpInput): убрать TypeScript-касты as из pug-шаблона
[Quasar] boot error: SyntaxError: Unexpected identifier 'as' — runtime-парсер
обрабатывает выражения в pug-template как чистый JS, без TypeScript. Касты
вида `el as HTMLInputElement | null`, `e as InputEvent`, `e as KeyboardEvent`
прямо в атрибутах `:ref` / `@input` / `@keydown` — синтаксическая ошибка во
время бутстрапа Vue, из-за которой /_dev/ui целиком не грузился.

Фикс:
- :ref='(el) => setRef(idx, el)' + функция setRef(idx, el: Element |
  ComponentPublicInstance | null) с кастом в TS-скрипте.
- @input='(e) => onInput(idx, e)' + сигнатура onInput(idx, event: Event)
  с внутренним кастом target.
- @keydown оставил передавать event «как есть» — KeyboardEvent — никакой
  cast не нужен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 09:38:34 +00:00
coopops 3ed84f5ffc [E10][@dark-sun] fix(_dev/ui): локальный импорт E10-компонентов — boot/ui.ts не HMR'ится
Quasar boot-файлы подхватываются только при рестарте dev-сервера. После
коммита 1af0f9c06f глобальные регистрации AmountInput/OtpInput/FilterBar/
FileUploader/VerticalStepper не подтянулись на лету — теги рендерились как
unknown components (пусто внутри секций 31–35 на /_dev/ui).

Фикс: добавил локальные импорты прямо в script setup _dev/ui/index.vue —
тот же паттерн, что у WalletCard, RailUserCard, AuthCard. Глобальная
регистрация в boot/ui.ts остаётся для боевого использования из других
страниц (после следующего рестарта dev'а).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 09:07:30 +00:00
coopops 1af0f9c06f [E10][@dark-sun] feat: AmountInput, OtpInput, FilterBar, FileUploader, VerticalStepper — эпик «Формы и ввод»
Пять props-only доменных компонентов из shared/ui/domain/:
- AmountInput — денежный ввод с символом валюты, форматированием тысячных,
  precision из marketplace asset config, кнопкой «макс» по balance, опциональной
  подписью баланса. Tabular-nums, right-align, font-weight 600.
- OtpInput — 6 ячеек с автопереходом фокуса, Backspace откатывает на
  предыдущую, paste 6-значного кода распределяется по ячейкам.
  Регулярка /^\d$/, состояния error/disabled.
- FilterBar — search (debounce 300мс) + dropdown-фильтры + chip'ы активных
  значений с remove + «сбросить всё». v-model для values, v-model:search для
  поиска. Активные chip'ы рендерятся под рядом фильтров.
- FileUploader — drag&drop + клик по зоне; валидация accept/maxSize/maxFiles
  → emit error; список загруженных с иконкой типа, именем, размером,
  кнопкой ×; слот progress для connected-обёртки.
- VerticalStepper — состояния pending/current/completed/error; completed
  кликабельны для возврата назад (опционально); опциональные/disabled шаги;
  слот active под телом текущего шага.

Все компоненты — pug, canon-токены --p-*, без store/router/api. Демо-секции
31–35 на /_dev/ui (наш стенд-витрина).

Зарегистрированы в boot/ui.ts и shared/ui/domain/index.ts. ESLint точечно
прошёл, vue-tsc не запускался полностью на desktop (запрет).

Wave 2 / E10 закрыт.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 08:29:49 +00:00
coopops daae6170c7 fix(DocumentPreview): убрать txt/image — наши документы это HTML/PDF
Удалены типы 'txt' и 'image' из DocumentPreviewType + соответствующие
ветки рендера (pre.document-preview__txt и img.document-preview__image)
и стили. В реальных потоках платформы документ кооператива всегда
HTML (рендерится в ShadowHtml внутри BaseDocument) или PDF — plain-
text фрагмент в моноширинном pre не используется и выглядит как
техническая ерунда. Аналогично image.

_dev: удалён previewTxtDemo (мусорный EOSIO chain id из debugging
notes → перешитый в фрагмент протокола → теперь полностью убран
по требованию пользователя). Секция 30 показывает только HTML +
loading + error состояния.
2026-05-22 06:22:56 +00:00
coopops 46bae0a5de fix(BaseDocument,_dev): vue-tsc, мусорный txt-demo, ширина документа
vue-tsc:
- _dev: убрал txId/explorerUrl из signatureSignedDemo — оба поля
  удалены из Signature ранее (общего эксплорера нет).
- BaseDocument: canonSignatures.map — нормализую is_valid через
  '?? undefined' (бэкенд может вернуть null, canon-компонент ждёт
  boolean | undefined).

DocumentPreview txt demo: вместо мусорного 'EOSIO chain id …
dirty window' (мой случайный кусок из debugging notes) — фрагмент
протокола собрания пайщиков ПК «Восход».

ComplexDocument: .col-md-7 → .col-md-10. Это контейнер, в котором
лежит BaseDocument в реальных страницах документов. 7/12 = 58%
ширины было визуально 'приплюснуто'.
2026-05-22 06:01:55 +00:00
coopops a6c315ba32 feat(ui): DocumentSignatures + перевод BaseDocument на canon
Реальный формат подписей кооперативного документа — это не «pending/
signed/rejected» из абстрактного SignatureCard, а IDocumentAggregate с
полями doc_hash + signatures[] (signer_certificate, public_key,
signature, is_valid). Каждая подпись разворачивается в детали.

Что сделано:
- Новый canon-компонент DocumentSignatures (story 9.5) в shared/ui/
  domain — props-only, принимает уже резолвнутые signerName и hash-
  совпадение, эмитит download/verify. Поверх Quasar — собственный
  expand на ref<Set<number>>, чтобы стиль был полностью canon.
- BaseDocument теперь рендерит DocumentSignatures вместо своего
  q-card.verify-card + q-list + q-expansion-item на teal/red badges.
  Адаптер canonSignatures маппит signer_certificate → ФИО через
  getNameFromCertificate, чтобы canon-компонент не знал про сертификаты.
- SignatureCard: удалены поля txId/explorerUrl и ссылка «Открыть в
  explorer» — общего эксплорера в платформе нет.

Demo: секция 29 → DocumentSignatures (валидный + с битой подписью),
секция 30 → DocumentPreview (HTML заголовок сокращён, чтобы не
обрывался при узкой колонке).
2026-05-22 05:52:19 +00:00
coopops ccfaf3592e feat(ui): волна 2 / E9 — документы и подписи
4 props-only canon-компонента в src/shared/ui/domain/, регистрация
в boot/ui.ts, demo-секции 26-29 в _dev/ui:

- DocumentRow (story 9.1) — строка документа в списке: иконка типа
  с tint'ом (pdf neg-soft, docx info-soft, html primary-soft), title,
  status через BaseBadge, дата/автор/описание, slot actions, emit open.
- SignatureCard (story 9.3) — подписавший (Avatar+ФИО+AccountBadge),
  статус (BaseChip pending/signed/rejected), для signed — хеш в моно-
  блоке + ссылка на explorer, для rejected — BaseBanner с причиной.
- ActivityTimeline (story 9.4) — вертикальный таймлайн событий с
  цветными иконками по типу (sign/reject/create/update/comment/
  transfer), groupByDate группирует по «Сегодня/Вчера/конкретная
  дата».
- DocumentPreview (story 9.2) — html (через DOMPurify), pdf через
  iframe, image через img, txt через pre. Стейты loading/error.

Все props-only, темо-зависимые значения только через --p-* токены.
2026-05-21 17:02:51 +00:00
coopops de245ee810 fix(_dev/ui): «Член кооператива» → «Пайщик» в PersonCard demo
Канон-терминология blago/ЦК: пайщик, не член. Прецедент закреплён
в auto-memory feedback_kooperatsiya_pai.
2026-05-21 16:57:50 +00:00
coopops e7dd0ce641 fix(IdentityPanel): compact тоже в карточке — padding+border+bg
Раньше compact-вариант рендерился без обрамления — avatar упирался
в левый край контейнера, визуально «проваливался» из общего стека
full-карточек (Screenshot_2026-05-21_21-51-09).

Compact теперь: padding 8/12px (тоньше чем full 16px), тот же border
+ surface + radius. В одну строку avatar + ФИО + AccountBadge + status,
но визуально это всё-таки карточка, не голая строка.
2026-05-21 16:52:41 +00:00
coopops e1113391cf fix(ContactSheet): label и значение в отдельных строках
Body раньше был обычным block-контейнером, а .head и .value-row —
оба display: inline-flex. Без явного block-форматирования inline-flex
элементы становятся inline и рендерились в одну строку. Визуально:
Emailivanov@example.ru, Телефон+7 (903)..., Telegram@ivanov — слипшиеся.

Body теперь display: flex + flex-direction: column, gap canon — 8px
в comfortable, 4px в compact. Head/value-row переведены на обычный
display: flex (вместо inline-flex), gap внутри сохранён.

Прецедент 2026-05-21 — Screenshot_2026-05-21_21-42-16, 21-42-31.
2026-05-21 16:44:40 +00:00
coopops 25b6d6c42b chore: убрать закоммиченный symlink node_modules 2026-05-21 16:25:22 +00:00
coopops 05a8ac0a51 fix(domain): E8 — copy-кнопки, отступы и compact-layout
- AccountBadge: copy-кнопка не сжимается (flex-shrink: 0), размер 20×20 + icon 14px — раньше 18×18 + 12px тонула рядом с текстом badge.
- DataRow: column-gap var(--p-3, 12px) → var(--p-5, 20px) + min-label-width 140 → 160px + padding-right на label. Между label и value был визуально слипшийся стык.
- ContactSheet: comfortable margin-top между label и value 2px → 8px (compact 4px). Раньше label «налипал» на значение.
- IdentityPanel compact: переделан в flat flex-row (avatar + ФИО + AccountBadge + status badge) — раньше grid с .body загонял имя и AccountBadge в две строки и avatar «уезжал» от имени. Spec 8.1 требует «только avatar + ФИО + EntityIdBadge в одну строку».
2026-05-21 16:24:45 +00:00
coopops 8344808676 feat(ui): волна 2 / E8 — идентификация и контакты
5 props-only canon-компонентов в src/shared/ui/domain/, регистрация
в boot/ui.ts, demo-секции 21-25 в _dev/ui:

- AccountBadge (story 8.5) — on-chain account name в mono-шрифте,
  copyable + опциональный explorer link. Назван AccountBadge вместо
  EntityIdBadge: имя EntityIdBadge занято под numeric ID пайщика
  в capital. Decision зафиксирован в epics.md и planning-artifacts.
- DataRow (story 8.3) — `label: value` пара для реестровых карточек,
  mono-режим, copyable, slot value-override, hint.
- ContactSheet (story 8.2) — email/phone/address/tg/web с автоиконками,
  кликабельными mailto:/tel:/t.me ссылками, copy и verified-чек.
- IdentityPanel (story 8.1) — Avatar + ФИО + AccountBadge + статус
  (active/blocked/pending) + role + actions-slot; compact/full.
- PersonCard (story 8.4) — Avatar + ФИО + role + AccountBadge +
  ContactSheet + slot meta; compact/comfortable.

Все props-only: useStore/useRouter/useApi внутри запрещены. Темо-зависимые
значения только через --p-* токены, никаких body--dark селекторов.
2026-05-21 15:57:21 +00:00
coopops a192e2c398 fix(BaseCheckbox): приглушить заливку галочки через --p-primary-press
q-checkbox по умолчанию красит .q-checkbox__bg в --q-primary, который в
dark теме равен #2dd4bf — на маленьком квадрате 18px этот яркий бирюзовый
смотрится ядовито. Переопределяем заливку filled-состояния на
--p-primary-press (#134e4a light / #14b8a6 dark) — глубже и спокойнее,
чек-символ остаётся белым.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:37:24 +00:00
coopops d99413f896 fix(tokens): приглушить --p-accent + унифицировать .agreement-link
Старое значение --p-accent (#D84315 light / #FF7043 dark) — слишком
насыщенное, выглядит ядовито рядом с тёплой палитрой ink/canvas. Меняем
на тёплую терракоту: #B85C38 (light) / #E89472 (dark) — тот же warm-tone,
но без перевозбуждения красного канала.

.agreement-link (псевдо-ссылки на просмотр документа в согласиях) была
hardcoded в #1c64f2 (синий) light / #ff9f43 (жёлто-оранжевый) dark и не
совпадала со ссылкой «Устав кооператива». Унифицируем: все ссылки внутри
согласий теперь var(--p-accent), визиt-цвет тот же — без фиолета.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:34:28 +00:00
coopops 77ae6bb47d fix(ReadStatement): чекбоксы согласий — выравнивание + gap + accent ссылка
BaseCheckbox block-вариант: убран собственный display:flex + align-items:
flex-start, который перебивал внутренний flex Quasar и сдвигал label вверх
относительно inner. Теперь Quasar сам центрирует inner на первой строке.
Добавлен padding-left: 8px на .q-checkbox__label — больше воздуха между
галочкой и текстом согласия.

ReadStatement: ссылка «Устав кооператива» — --p-primary → --p-accent
(тёплый оранжевый #D84315 / #FF7043 dark) как акцентный цвет палитры.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:26:19 +00:00
coopops 6016c0597b feat(UserDataForm): canon-стиль полей + тип аккаунта на BaseRadioCard
UserDataForm/UserDataForm.vue:
- Выбор типа аккаунта: три q-btn(glossy, height:75px) → три BaseRadioCard
  с title/description; больше нет teal-«кнопок-плиток»
- Возврат к выбору типа: flat q-btn → BaseButton(ghost) с arrow_back

Sub-forms (Individual / Entrepreneur / Organization + Create*):
- q-input/q-select: standout="bg-teal text-white" → outlined color='primary'
  (canon-tone, единый primary-акцент вместо устаревшего teal)
- .q-gutter-sm.q-mt-md → .user-data-stack (flex column, gap токены)
- OrganizationDataForm: кнопка «совпадает» — color='teal' → flat primary

Валидация q-form.validate() и :rules сохранены — это всё ещё q-input
с rules-массивом, только tone и контейнер переехали на canon-токены.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:16:31 +00:00
coopops eeecab96da feat(ui): BaseCheckbox + BaseRadioCard и миграция SignUp-шагов
- BaseCheckbox: обёртка q-checkbox с canon-стилями, block-вариант для длинных
  согласий (чекбокс прижат к верху, label многострочный)
- BaseRadioCard: карточка-опция с радио-индикатором справа, props title /
  description / meta + slot fallback'и; используется для выбора программы

Миграции SignUp:
- ReadStatement: q-checkbox → BaseCheckbox(block); согласия в .agreements-стек
- SelectProgram: q-list+q-radio → список BaseRadioCard
- GenerateAccount: q-input → BaseInput(readonly, mono) с copy в #append;
  q-checkbox → BaseCheckbox; автоселект через querySelector('input').select()
  вместо ref.select() (BaseInput не пробрасывает внутренний API q-input)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:11:56 +00:00
coopops 127cf36ca9 fix(desktop): .ws-switcher__title padding-top 2px
Воздух над заголовком стола внутри WorkspaceSwitcher — заголовок не упирается
в верхнюю границу caption «ПК «ВОСХОД»».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:03:05 +00:00
coopops f158afe485 fix(desktop): .rail__top padding 16px 8px + убрать disabled-стиль у WorkspaceSwitcher
- .rail__top: padding 18px 20px → 16px 8px (унификация боковых отступов)
- WorkspaceSwitcher: убраны :disabled-атрибут и hover:not(:disabled) — у пайщика, которому доступен только один стол, кнопка выглядит как обычно (без not-allowed-cursor, без приглушённого цвета). Меню q-menu просто не открывается через v-if='workspaces.length > 1'. Тихое игнорирование клика вместо явного запрета.
2026-05-20 18:55:36 +00:00
coopops 521575ab6a fix(desktop): WorkspaceSwitcher — фильтр столов по ролям + 3-строчный title
WorkspaceSwitcher показывал ВСЕ workspaceMenus, включая столы выше роли пользователя (например, «Стол председателя» видел рядовой пайщик), что приводило к ошибке access-denied при попытке переключения.

- Добавлена фильтрация по иерархии: chairman ⊇ member ⊇ user. Председатель видит столы любых ролей; член совета — user+member; пайщик — только user. Workspace без meta.roles (или с пустым массивом) — публичный для всех (та же логика, что в legacy WorkspaceMenu, плюс корректное наследование прав).
- Title допускает перенос на 3 строки (line-clamp 3) — «Стол вычислительных ресурсов» теперь умещается полностью
- Расширена кнопка switcher'а (margin сторон 8→4 в LeftDrawerMenu), даёт больше места под текст
2026-05-20 18:41:42 +00:00
coopops c2eefbf244 fix(desktop,signup): WorkspaceSwitcher polish + Bank toggle QR↔реквизиты
WorkspaceSwitcher:
- Brand-строка теперь собирается из vars.short_abbr + «vars.name» (получается «ПК «ВОСХОД»»). Раньше показывал только short_abbr — название кооператива было «потеряно»
- Title допускает перенос на 2 строки (line-clamp 2) — длинные названия типа «Стол вычислительных ресурсов» больше не обрезаются ellipsis'ом с одного слова
- Chevron приклеен к верху строки (align-self start) — корректно при двухстрочном title
- Меню workspace: canon padding (8/12) вокруг q-item, отступ между avatar-section и текстом — иконка и название больше не упираются в края
- Затемнение фона при открытом меню (rgba(9,9,11,0.32) overlay через Teleport в body) — меню больше не сливается с контентом за drawer'ом

Bank.vue (карточка платежа):
- Убрали q-expansion-item (раскрытие вниз внутри карточки выглядело перегружено)
- Кнопка-toggle: «Показать реквизиты» рядом с «Скачать QR». Клик переключает зону под сводкой между QR и списком 9 BaseInput'ов с copy-кнопками
- В режиме реквизитов: кнопки меняются на «Скопировать всё» (primary) + «Показать QR» (secondary)
- QR-canvas использует v-show чтобы сохранять состояние при toggle назад; реквизиты — v-if чтобы не держать BaseInput'ы в DOM
2026-05-20 18:22:58 +00:00
coopops 3f43a77167 feat(desktop): WorkspaceSwitcher + фикс ReadStatement стилей + плоский Bank.vue
ReadStatement: backend-HTML генерирует с inline-стилями и Quasar text-h* классами, которые перебивали :deep правила. Усилили все ключевые селекторы !important + добавили охват .text-h1/h2/h3/h4/h5/h6 и .text-right (под мета-блок «УТВЕРЖДЕНО…»).

Bank.vue (карточка платежа):
- убрали отдельные surface-карточки вокруг сводки и QR (получалось «карточка-в-карточке-в-карточке»)
- сводка → плоский <dl> со строкой-разделителем снизу (border-bottom var(--p-line))
- QR → просто центрированный canvas без обёртки/фона/рамки; белый цвет внутри canvas — функциональное требование контрастного сканирования, не дизайн-выбор
- detail-expansion → без своего фона, только border-top/bottom

WorkspaceSwitcher (новый widget src/widgets/Desktop/WorkspaceSwitcher/):
- Карточка-кнопка в шапке drawer: иконка кооп + caption «ПК «ВОСХОД»» + bold «Стол совета» (текущий) + chevron
- Клик → q-menu со списком всех workspaceMenus (Стол пайщика, Стол совета, Стол председателя и т.д.); выбор → selectWorkspace + goToDefaultPage
- Подключён через #brand slot AppDrawer'а в LeftDrawerMenu, заменяет дефолтный brand-row
2026-05-20 17:48:25 +00:00
coopops 9ea9467c2c feat(signup): canon-типографика заявления + рефакторинг карточки платежа
ReadStatement (этап 6.1):
- Backend генерирует HTML документа — локально нормализуем через :deep
- h1 уменьшен с монструозного дефолта до canon h3 (20px), центрирован
- h2/h3, p, strong, ul/ol, table, hr — типографика по var(--p-*)
- мета-блок .approved/.meta-right (УТВЕРЖДЕНО…) — ink-2, мельче, плотнее
- Чекбоксы соглашений снизу не трогали (как просил пользователь)

Bank (этап 6.2 — карточка платежа PayWithProvider):
- QR-код вынесен в фокус: сначала сводка (получатель/сумма/назначение) → QR → действия → реквизиты для ручного перевода скрыты под q-expansion-item
- Полные 9 полей (ИНН/БИК/КПП/корр-счёт/...) переведены на BaseInput(readonly, mono) с copy-кнопкой в #append; раскрываются только при необходимости
- Кнопки «Скачать QR» (primary) и «Скопировать реквизиты» (secondary) — BaseButton
- Удалены: q-btn(push), case-mixed «скопировать реквизиты», стиль #qr через global селектор
2026-05-20 17:20:15 +00:00
coopops 9a3ee9afa4 feat(invite): этап 5 — миграция Invite на canon AuthCard + BaseInput/BaseButton
- InvitePage: row/col-обёртка → .invite-page flex-center + canon padding (как у SignInPage)
- Invite widget: AuthCard со штатными title/subtitle (вместо CAPS-заголовка с background-clip градиент-текста)
- BaseInput(mono, readonly) для приватного ключа — canon-оформление key/hash полей
- BaseButton(ghost) для копирования + BaseButton(primary) для «Установить ключ»
- Удалены: градиент-текст, semi-transparent blue/green tint карточки вокруг инструкции и чекбокса, hover translateY + box-shadow glow, custom border-radius/box-shadow для q-input и кнопок, мобильные @media-блоки (canon-токены и AuthCard сами адаптивны)
2026-05-20 16:54:02 +00:00
coopops fd4bb48870 fix(signup): canon-рамка для контейнера подписи вместо неонового glow
- .signature-container: 3px primary border + 3-слойный box-shadow glow → 1px dashed по var(--p-line-2), surface-2 фон, без свечения; на hover лишь окрашивается рамка
- .signature-hint: переехала в центр контейнера, canon ink-2 цвет, убран frosted-chip с blur/shadow — теперь спокойная метка «Оставьте собственноручную подпись в рамке», pointer-events: none пропускает клики на canvas
- min-height снижен 300→220, padding на canon-токены
2026-05-20 16:52:23 +00:00
coopops d41ce5712d fix(signup): отступ сверху, canon фон q-stepper, ширина email-поля
- SignUp.vue: добавлена обёртка .signup-page (padding + center + min-height) — как у SignInPage; раньше страница была прижата к шапке
- AuthCard maxWidth: 1000 → 720 — историческое 1000 для трёх-колоночной UserDataForm; для остальных шагов выглядит абсурдно широко
- q-stepper: убраны собственный фон/тень/padding (не «карточка в карточке»), линии-разделители переведены на var(--p-line)
- EmailInput: max-width поля 360px — email не должен растягиваться во всю ширину карточки
2026-05-20 16:39:11 +00:00
coopops ed9f483e7a fix(authcard): убрать оборванный barrel-экспорт после удаления старого AuthCard
vue-tsc ругался: shared/ui/index.ts:21 — Cannot find module './AuthCard'.
- shared/ui/index.ts: убран `export * from './AuthCard'`
- LoginRedirectPage.vue: импорт переключён на canon `shared/ui/domain/AuthCard`, CAPS-заголовок заменён на canon `title` prop
- Invite.vue: импорт переключён на canon (полная миграция Invite — этап 5)
2026-05-20 16:16:11 +00:00
coopops d974d7698b feat(signup): этап 4 — миграция SignUp на canon AuthCard + BaseButton/BaseInput
- canon `shared/ui/domain/AuthCard` переписан на pug
- SignUp.vue переключён со старого «глянцевого» AuthCard на canon (title prop вместо CAPS-заголовка)
- старый `shared/ui/AuthCard` удалён (использовался только в SignUp)
- 7 подэкранов SignUp (EmailInput, SetUserData, SelectProgram, GenerateAccount, SelectBranch, ReadStatement, SignStatement): q-btn → BaseButton (ghost для «назад», primary для «Продолжить»)
- EmailInput: q-input → BaseInput, валидация rules переведена на computed :error
- q-input в GenerateAccount оставлен из-за зависимости от ref.select()
2026-05-20 16:10:50 +00:00
coopops bc5d7deda5 content(brand): og-image из poster-logo-horizontal + новый длинный дескриптор
- og-image.png — взят brand-постер ~/blago/production/shared/poster-logo-horizontal.png,
  отресайзен до 1000px по ширине и центрирован на полотне 1200×630
  с белыми краями по вертикали. Шрифт/композиция родные бренда.
- description / og:description / twitter:description: «Цифровой Кооператив —
  система управления хозяйством. Регистрация пайщиков, заказы на поставку
  и приобретение имущества, собрания совета, общие собрания пайщиков,
  взаимные расчёты на блокчейне, автоматический документооборот и бухбаланс
  на основе простой электронной подписи.»
2026-05-20 15:56:13 +00:00
coopops 44628dfde4 content(brand): обновить превью и мета — система управления хозяйством на платформе «Благо»
- og-image.png перерисован: «Цифровой Кооператив» / «Система управления
  хозяйством» / «на платформе БЛАГО — кооперативной экономики для жизни» /
  «Регистрация пайщиков · заказы на поставку и приобретение имущества ·
  расчёты на блокчейне».
- description / og:description / twitter:description выровнены под этот
  же копирайт. Слово «взаимоотношения с пайщиками» убрано как слишком
  узкое — управление хозяйством включает в себя ещё заказы, реестры
  и документы.
2026-05-20 15:44:57 +00:00
coopops 4fd79ffda9 feat(brand): системный логотип ЦК + чистка ассетов + OG-превью
- src/assets/logo.svg, public/favicon.svg, public/logo.svg — единая SVG
  логотипа кооператива (path с «ц» внутри). Скачано с
  https://цифровой-кооператив.рф из шапки сайта.
- src/assets/logo.svg: fill -> currentColor чтобы лого внутри
  canon .rail__brand наследовал --p-primary (для тёмной темы тоже).
- LeftDrawerMenu подключает лого через slot #brand-icon AppDrawer
  (импорт ?raw + v-html). Внутри зелёного квадрата шапки рейла теперь
  наш системный знак, а не дефолтный q-icon "dashboard".
- index.html: favicon → /favicon.svg, apple-touch-icon тоже на SVG.
  Удалены неработающие ссылки на /favicon.ico и /apple-touch-icon-*.png.
- index.html: добавлены description + расширенные og:* и twitter:*
  с конкретным описанием и og-image (1200×630), чтобы ссылки в чатах
  выглядели прилично.
- public/og-image.png — превью 1200×630, ImageMagick-сгенерированный
  композит из логотипа + текста.
- Удалены неиспользуемые ассеты:
  - public/icons/, public/pwa/ — старые favicon/PWA-PNG'и всех размеров;
  - src/assets/* — 40 легаси SVG/PNG (anime, blockchain, dacom*, flow*,
    header-logo, quasar-logo-vertical, system*, club*, welcome.jpeg и т.д.).
  Оставлены только src/assets/pin.svg (используется в Map.vue) и
  src/assets/logo.svg (новый).
2026-05-20 15:35:23 +00:00
coopops fca07fc3c7 fix(layout): убрать TS-каст из pug-шаблона AppDrawer
Pug-template передаёт выражения в Vue compiler как строки; конструкции
вида (entry as RailItem).route не превращаются в JS и падают
SyntaxError в браузере: «Unexpected identifier 'as'».

Касты унесены в <script>: добавлены computed flatItems и
normalizedGroups, шаблон работает с уже типизированными массивами без
inline-TS.
2026-05-20 15:20:25 +00:00
coopops dfd1ebd2a9 fix(layout): этап 3.3 — все canon-компоненты на pug + фикс импорта cmdk
- LeftDrawerMenu: импорт useCmdkStore → useCmdkMenuStore (правильное имя).
  Без этого ломался ESM build всего drawer'а: «no exported member
  useCmdkStore» — RailUserCard не монтировался, поэтому и chevron свёртки
  «не работал», и Найти/Пополнить не реагировали.
- AppDrawer.vue, AppHeader.vue, RailUserCard.vue переведены на pug
  (lang="pug"). В проекте везде pug — никакого исторического HTML в
  canon-компонентах быть не должно.
2026-05-20 15:12:38 +00:00
coopops 7d6592d1b6 fix(layout): этап 3.2 — pug + v-model:collapsed + рабочие триггеры
- q-drawer width 240 → 248px (совпадает с canon --p-rail-w, кошелёк больше
  не уплывает справа из-за обрезки на 8px)
- LeftDrawerMenu: убран весь :deep override на .rail__usercard/.rail__signout,
  возвращаем canon margin/padding как есть
- LeftDrawerMenu: добавлен v-model:collapsed на RailUserCard с сохранением
  в localStorage — chevron «свернуть/развернуть» снова работает
- LeftDrawerMenu: кнопка Найти теперь дёргает cmdkStore.openDialog() напрямую
  (фейковый KeyboardEvent не доходил до глобального обработчика)
- LeftDrawerMenu: «Пополнить» → useDepositDialog().open(); DepositButton +
  WithdrawButton рендерятся скрыто как держатели q-dialog (q-portal в body)
- Header.vue и LeftDrawerMenu.vue переписаны на pug (как остальной проект)
2026-05-20 14:40:31 +00:00
coopops e5799ba702 fix(layout): этап 3.1 — RailUserCard в footer + canvas-фон + отступы рейла
- AppDrawer: плоский items оборачивается в .rail__nav, появляется gap 4-8px до cmdk
- cmdk margin 16→8px, чтоб выровнять с rail__nav (8px от стенки)
- LeftDrawerMenu footer: WalletCardMini+LogoutButton → один canon RailUserCard
  (avatar+balance в primary-soft + Пополнить + встроенный signout)
- :deep override на .rail__usercard margin 16→12/8 и .rail__signout padding 12/20→12/12
- quasar-canon: body--light/dark + q-layout/q-page-container/q-page красятся
  через --p-canvas, чтобы фон страницы соответствовал шапке и рейлу
2026-05-20 14:19:18 +00:00
coopops 1e1a70ded2 feat(layout): этап 3 — WalletCardMini + canon LogoutButton в footer AppDrawer
LeftDrawerMenu:
- legacy `MicroWallet` (ColorCard teal с большой карточкой пайщика + ИП-badge
  + Deposit/Withdraw micro-кнопки) → canon `WalletCardMini` из
  widgets/wallet-card-mini (тонкая карточка из shared/ui/domain/WalletCard
  compact-variant, читает walletStore сама).
- Убран toggle «свернуть/развернуть нижнюю секцию» — нижняя секция теперь
  всегда видна (короткий компактный блок: WalletCardMini + Выйти).
- Удалены неиспользуемые `onMounted`/`ref` импорты, slide-анимация и CSS
  toggle-кнопки.

LogoutButton:
- pug + q-item с красной полупрозрачной плашкой → плоский ghost-button в
  стиле rail-пункта AppDrawer: прозрачный фон в покое, на hover —
  `--p-neg-soft` фон + `--p-neg` текст/иконка.
- Из uppercase «ВЫЙТИ» в title-case «Выйти» (canon — никаких uppercase).

Визуально левый drawer теперь полностью в canon-семье: единый rail с
пунктами + поиском + компактным wallet-балансом + ghost-кнопкой выхода.

Закрывает этап 3 из 5 в волне «приземление DS на реальный layout».
Дальше — SignUp (этап 4) и Invite (этап 5).
2026-05-20 14:01:08 +00:00
coopops 61bd6f2000 feat(layout): этап 2 — AppHeader в реальном layout (заменяет MainHeader)
widgets/Header/CommonHeader/Header.vue теперь использует canon AppHeader
из shared/ui/layout. Высота шапки 56px (--p-topbar-h), border-bottom
hairline, без shadow и фоновых градиентов.

Слоты:
- #crumb: BackButton (для авторизованных), либо title с названием
  кооператива из system.info.vars для гостей/install.
- #actions: headerActions injection через useHeaderActionsReader (всё что
  страницы инжектят — Deposit/Withdraw, SettingsDropdown и пр. — рендерим
  как было).
- #notifications: NotificationCenter (только loggedIn + isClient).
- #theme: ToogleDarkLight — пока legacy, сохраняет storage + PWA-цвет;
  наш canon ThemeToggle их не делает (привяжу позже отдельным эпиком).
- #profile: BaseButton primary для гостей — Регистрация/Вход (в зависимости
  от текущего route).

Удалены:
- MainHeader.vue (вся логика scroll arrows + carousel actions group ушла —
  canon `.topbar__actions { gap: 8px }` достаточно; overflow-scroll
  будет вернут отдельно если потребуется на узких экранах).
- HeaderStyles.scss (стили scroll-arrow и q-toolbar overrides больше
  не нужны — canon-разметка через `<header class="topbar">`).

q-header Quasar остаётся wrapper'ом (sticky-poзиционирование в q-layout),
внутри — AppHeader даёт canon-разметку.
2026-05-20 11:54:27 +00:00
coopops f651f05a06 feat(layout): этап 1 — AppDrawer в реальном layout (заменяет LeftDrawerMenu)
Переписан widgets/Desktop/LeftDrawerMenu/LeftDrawerMenu.vue: внутри теперь
canon `AppDrawer` из shared/ui/layout с rail-видом MONO v2 (логотип ПК +
короткое имя кооператива в шапке рейла, ⌘K-поиск, пункты-router-link'и
с иконкой/бэйджем, sticky-footer).

Адаптер store→canon:
- items: desktop.activeSecondLevelRoutes → RailItem[], с прежней
  фильтрацией по roles + meta.conditions + meta.hidden (один-к-одному
  логика из SecondLevelMenuList).
- activeKey: вычисляется из router.currentRoute с поддержкой группового
  паттерна project-* → projects-list.
- @select: router.push({name, params:{coopname}}) или
  actionsStore.executeAction(meta.action), закрытие drawer на mobile.
- @cmdk: эмиттим keyboard event ⌘K → CmdkMenu (он смонтирован глобально
  в default.vue) подхватывает.

Footer-слот: пока сохранён legacy MicroWallet + LogoutButton + toggle
свернуть/развернуть (заменим на WalletCardMini в этапе 3).

default.vue: q-drawer width 200 → 240 (canon-ширина rail'а).

Что НЕ затронуто этим коммитом: верхняя шапка (legacy MainHeader),
правый drawer для actions, q-footer для гостей. Их меняем отдельными
этапами 2–3.
2026-05-20 11:38:56 +00:00
coopops f3d0192e6b fix(ui): убрать конфликт с Quasar — .row → .u-row
Корневая причина «оторванной» галочки в ResetKeyForm: я ранее задал в
mono-platform/components.css глобальное `.row { display: flex;
align-items: center; gap: 12px; }`. Quasar внутри `q-checkbox` (а также
q-radio, q-toggle, q-btn-toggle и др.) сам добавляет class="row" на
корень компонента и использует его как inline-flex — мой `gap: 12px`
раздвигал `.q-checkbox__inner` и `.q-checkbox__label` на 12px по всему
приложению.

Решение:
- Переименовать утилиту `.row` → `.u-row` (та же семантика, тот же набор
  модификаторов --wrap, --gap-2/4/6). У Quasar теперь свободный .row.
- Обновить все 6 использований в /_dev/ui.
- Откатить предыдущий :deep(.q-checkbox__label) padding-left:4px workaround
  в ResetKeyForm — лечил симптом, причина устранена.

Затрагивает только нашу dev-витрину и canon-utility; legacy pug-виджеты
с `.row.justify-center` продолжают работать через стандартный Quasar
flex-grid класс.
2026-05-20 11:18:39 +00:00
coopops 7e429c481b fix(ui): ResetKeyForm — сжать зазор между галочкой и текстом «Я сохранил ключ»
Quasar по умолчанию даёт ~12–16px padding на .q-checkbox__label, в
compact-форме сохранения ключа это смотрелось «оторвано». Уменьшаем до 4px
через :deep override на классе .rk-form__confirm.
2026-05-20 11:05:02 +00:00
coopops 7819d9c5bb feat(dev): секция 20 — ResetKey двухэтапный flow в /_dev/ui
Три кадра в одной секции: LostKey (ввод email), ResetKeyForm check-mail
(уведомление «письмо отправлено»), ResetKeyForm save-key (демонстрация
сохранения ключа с моковым сгенерированным аккаунтом). Submit на третьем
кадре регенерирует мок-ключ для повторной проверки.

Заменяет ошибочную секцию ChangeKey из предыдущей итерации.
2026-05-20 10:44:50 +00:00
coopops c0df025e1f fix(ui): откатить ChangeKey + мигрировать настоящий ResetKey flow на base-компоненты
ChangeKey-фича (формы current/new/confirm WIF + успешный диалог) была
ошибочной интерпретацией E7. Реальный flow «сменить ключ» в продукте —
это **двухэтапный ResetKey**:

  1. LostKey: пайщик вводит email (он потерял ключ — текущего WIF нет),
     бэк присылает письмо со ссылкой `?token=...`.
  2. ResetKey: при заходе по ссылке клиент сам генерирует новый ключ
     прямо в браузере (через `useCreateUser().generateAccount()`),
     показывает приватный ключ readonly с «Скопировать», требует
     подтверждения «Я сохранил ключ» и вызывает on-chain
     `resetKey({token, public_key})`.

Текущий ключ ввести нельзя (его нет). Новый ключ ввести нельзя (только
генерируется). Никакого подтверждения нового ключа — он один и тот же.

Что сделано:
- Удалена ошибочная `features/User/ChangeKey/`.
- Старый `widgets/Registrator/ResetKey/ui/ResetKey.vue` (pug + сырые
  q-input/q-checkbox + кастомные градиентные стили) разбит на:
  * `ResetKeyForm.vue` — presentation (props-driven, режимы
    `check-mail` / `save-key`), без useRouter/useApi — пригоден для
    /_dev/ui витрины.
  * `ResetKey.vue` — connected обёртка, читает `route.query.token`,
    генерит account через `useCreateUser`, диспатчит `resetKey` через
    `useResetKey`, на успех → router.push на signin.
- Новая UI собрана на BaseForm + BaseInput readonly mono + BaseButton +
  BaseBanner + AuthCard.

/_dev/ui секция 20 теперь показывает три кадра подряд: LostKey (email
шаг), ResetKey check-mail, ResetKey save-key с моковым сгенерированным
ключом.

FR11, UX-DR9, UX-DR15.
2026-05-20 10:44:32 +00:00
coopops 5dba22d457 fix(ui): BaseInput/BaseSelect — явный проброс слотов вместо v-for in $slots
Generic-проброс `<template v-for="(_, slotName) in $slots" #[slotName]>`
рушится в Quasar 2.19 рендере: при наличии scoped-слота #append вылетает
`Cannot read properties of null (reading 'key')` в renderSlot + Vue warns
по `_isVue`/`constructor` свойствам. Заменяем на явный список слотов
(prepend / append / before / after / hint у q-input; +option, selected-item
у q-select) — синтаксис, который Quasar держит корректно.

Кейс всплыл на ChangeKeyForm: #append с q-icon (toggle visibility) ронял
страницу /_dev/ui целиком.
2026-05-20 08:47:48 +00:00
coopops 698b1d6333 feat(dev): секция 20 — ChangeKey flow в /_dev/ui (визуальная проверка E7)
Подключение ChangeKeyForm + ChangeKeySuccessDialog к dev-витрине.
Submit запускает заглушку useChangeKey().changeKey() (≈600 мс), по успеху
открывает диалог с новым WIF. Generate генерирует валидный K1 ключ через
SDK и заполняет оба новых поля. Confirmed сбрасывает форму.
2026-05-19 20:03:38 +00:00
coopops 8b3263bf70 feat(ui): E7.2 ChangeKeySuccessDialog — подтверждение через BaseDialog без backdrop-exit
Диалог успеха смены ключа: новый WIF в моноширинном блоке с copy-кнопкой,
BaseDialog с closeOnBackdrop=false, closeOnEscape=false, hideCloseButton=true.
Закрытие только через primary-кнопку «Я сохранил ключ»; до копирования
кнопка показывает guard-баннер вместо немедленного закрытия. По confirmed
эмитим событие — реальный flow подключит router.push на дашборд.

FR11, UX-DR11, UX-DR15. Story 7.2 из epics.md.
2026-05-19 20:03:26 +00:00
coopops 9578dc1b86 feat(ui): E7.1 ChangeKeyForm — смена ключа на BaseForm + BaseInput + BaseBanner
Форма смены ключа пайщика на новых компонентах дизайн-системы.
BaseBanner severity=warn с предупреждением о потере, checklist последствий,
поля current/new/confirm WIF моноширинные с toggle visibility, ghost-кнопка
«Сгенерировать» через PrivateKey.generate из @wharfkit/session.

Бизнес-логика — заглушка с TODO на привязку on-chain change-key action
(pattern см. features/User/ResetKey/api) и TODO на OtpInput шаг подтверждения
(приходит в Эпике 10).

FR11, UX-DR9, UX-DR15. Story 7.1 из epics.md.
2026-05-19 20:03:12 +00:00
coopops 45cfa29940 fix(ui): Avatar tone — единая семантика neutral/primary/accent, убрать ink
Bug после rename --p-accent → --p-primary: класс .avatar--accent смотрел
на --p-primary-soft (canon teal), а должен был — на --p-accent-soft
(warm orange по новой семантике). И вариант ink (белый bg + dark text)
визуально выпадал из палитры — крайний xl-аватар на демо-витрине казался
«не пришей к звезде рукав».

Новая семантика AvatarTone:
- neutral (default) — серый surface-3 + ink-1 (нейтральный)
- primary — canon-teal soft + canon-teal text (основной выделение)
- accent — warm-orange soft + warm-orange text (редкое выделение,
  например подсветка пайщика по batch_id)

Удалён вариант ink — не использовался в реальном коде.
В dev-витрине xl-аватар: tone="ink" → tone="primary".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:46:27 +00:00
coopops d0a8dc0080 revert(ui): убрать override центрирования suffix/prefix — оставить Quasar дефолт
Override (padding-top:0 + align-self:center на dense) не отрабатывал —
Quasar в dense держит prefix/suffix по baseline вводимого значения,
и пользователю это устраивает. Убираем неработающий override, чтобы
не мешался в дальнейших правках.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:42:30 +00:00
coopops 8d3cabab9b fix(ui): BaseDialog — отступ через margin-top второго ребёнка
Inline `style="margin:0"` на <p>-описании перебивал мой margin-bottom
через CSS (специфичность inline > selector). Переключаюсь на
`> :first-child + *` margin-top — отступ крепится ко второму ребёнку,
не зависит от inline-стилей первого.

Работает универсально: <p> + <BaseInput>, <p> + <BaseForm>, любая
другая пара «описание + контент».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:39:29 +00:00
coopops 5ba95a2e6d feat(ui): --p-accent (warm orange) + suffix-центр + точная вёрстка gap
1. --p-accent (canon token): #D84315 (light) / #FF7043 (dark).
   Совпадает с legacy Quasar $accent (warm orange, Material Deep Orange).
   Проброшен в --q-accent через CSS var — legacy color="accent",
   var(--q-accent), text-accent продолжают работать без изменений,
   но теперь следуют за canon-темой (light/dark). Резерв под редкое
   выделение: ссылки batch_id и т.п.
2. quasar-canon.css: .q-field--dense .q-field__suffix/prefix —
   padding-top:0 + align-self:center. Quasar по умолчанию ставит
   padding-top: 24px под floating-label, отчего «RUB» визуально
   опускался к низу control'а. Теперь в одну линию со значением.
3. BaseForm: убран gap 12px из .base-form__body — reserve-hint-space
   у q-input даёт ~24px снизу под error/hint, дополнительный gap
   делал расстояние между инпутами неприятно большим.
4. BaseDialog: убран универсальный gap из body, оставлен margin-bottom
   только на первом <p>/.intro — между описанием и первым инпутом
   воздух нужен, между остальными детьми reserve-hint-space хватает.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:33:11 +00:00
coopops 6d5d614a2c fix: убрать случайный symlink node_modules из индекса
Промахнулся `git add -A` — захватил worktree-symlink node_modules
(он pnpm-shared с основным репо). .gitignore покрывает node_modules/
но симлинк-файл — отдельный путь.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:24:25 +00:00
coopops 0bac215035 refactor(ui): --p-accent → --p-primary — устранить конфликт с Quasar primary
Контекст: после проброса --q-primary = var(--p-primary) (4fddfe2343)
наш токен --p-accent и Quasar primary стали обозначать ОДИН и тот же
canon-teal — два имени для одного цвета это когнитивный шум, и при
росте платформы будет путать. Имя `accent` оставляем свободным под
будущий ВТОРОЙ цвет (warm-orange, для редкого выделения вроде ссылок
по batch_id) — это отдельная задача когда понадобится.

Переименовано везде (109 ссылок в 6 файлах):
- --p-accent          → --p-primary
- --p-accent-hover    → --p-primary-hover
- --p-accent-press    → --p-primary-press
- --p-accent-soft     → --p-primary-soft
- --p-accent-line     → --p-primary-line
- --p-accent-strong   → --p-primary-strong
- --p-ink-on-accent   → --p-ink-on-primary

Файлы: tokens.css, quasar-canon.css, components.css, RailUserCard,
AuthCard, _dev/ui (включая user-facing labels палитры «Accent → Primary»
и заголовок секции «Акцент → Основной цвет»).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:23:58 +00:00
coopops 4fddfe2343 feat(ui): canon-цвет через --q-primary CSS var; вернуть dense + rounded
- tokens.css: проброс canon-палитры в Quasar brand-переменные
  (--q-primary/negative/positive/warning/info = соответствующие
  --p-* токены). Резолвится lazy → автоматически следует за темой.
  Теперь Quasar САМ красит focus q-field, q-btn color="primary",
  bg-primary utility-классы в canon-цвета без CSS-overrides;
- quasar-canon.css: убраны border-color overrides на ::before
  (default/focus/error) — Quasar управляет цветом через --q-primary
  и собственный механизм --focused/--highlighted. Оставлены только
  background, text-color и rounded углы (border-radius var(--p-r-sm));
- BaseInput/Select: вернули `dense` (компактный 40px) и явный
  `color="primary"` (canon teal через --q-primary).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:14:20 +00:00
coopops 9f38148dc4 fix(ui): BaseDialog — уменьшить отступ между body и footer-кнопками
Между нижним инпутом и footer-кнопками был ~32px (q-card-section
padding-bottom 20 + q-card-actions padding-top 12) — выглядело
растянуто. Сводим к 16px (8+8) — кнопки сидят ближе к контенту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:06:27 +00:00
coopops 559493a4b2 fix(ui): q-field overrides — оставить только цвет, всё остальное дефолт Quasar
Регрессия мерцания границы и приплюснутости — следствие борьбы наших
overrides с встроенной Quasar анимацией. Возвращаем дефолт: Quasar
сам решает геометрию (height/padding/border-radius/font), анимации,
opacity ::before и focus-индикацию. Наша задача — только покрасить
в canon-цвета.

Удалено из .q-field--outlined:
- height/padding/border-radius/font-size/font-family overrides;
- transition border-color (Quasar сам делает свой transition);
- opacity:1 !important на ::before (был костыль от мерцания);
- box-shadow:none на --focused/--highlighted;
- display:none на ::after underline;
- override .q-field--dense;
- override .q-field--float .q-field__label (Quasar анимирует label сам);
- focus-shadow / outline reset на native input.

Оставлено (только цвет):
- background: var(--p-surface);
- border-color: line-1 / accent (focus) / neg (error);
- color текста, placeholder, label, prefix/suffix, hint/error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:05:57 +00:00
coopops ed61bb46b6 fix(ui): мерцание границы q-input на focus + unused import в BaseTable
- Quasar при focus анимирует opacity ::before (прячет старую границу
  перед показом ::after underline). Так как ::after мы спрятали,
  получался видимый gap ~200-400 ms «граница исчезла → появилась
  с подсветкой». Фикс: opacity:1 !important на ::before,
  transition только по border-color.
- BaseTable.vue: убран неиспользуемый импорт BaseTableColumn.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:01:31 +00:00
coopops 113a7b8a1b fix(ui): убрать dense из BaseInput/Select; gap в Dialog/Form body; wallet 17px
- BaseInput/BaseSelect: убран `dense` — возвращаемся к дефолтному
  outlined Quasar (control ~56px). Floating-label получает достаточно
  воздуха, текст значения визуально центрируется без сжатия;
- BaseDialog: `.base-dialog__body` теперь flex-column с gap 16px —
  описание/инпуты/таблицы внутри диалога получают единый воздух
  без правки потребителя (пример: «Паевой взнос: укажите сумму…» →
  отступ до инпута появляется автоматически);
- BaseForm: `.base-form__body` gap 4px → 12px — те же требования
  внутри формы;
- WalletCard: `.wallet__metric-val` 19→17px — итоговое значение
  для аккуратного баланс-блока FULL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:00:14 +00:00
coopops 390c5e9a5c fix(ui): убрать stack-label, двойную focus-подсветку и mini-wallet из шапки
1. Stack-label: BaseInput/BaseSelect → только `dense`. Quasar
   стандартно сам корректно держит floating-label в dense+outlined,
   без CSS-override метка работает по документации Quasar.
2. Двойная focus-подсветка: убран наш box-shadow var(--p-focus-ring)
   поверх Quasar внутренней focus-индикации (виден был как «белый
   ринг» на тёмной + наш teal). Теперь только border-color меняется
   через ::before (canon-минимализм). Дополнительно `box-shadow: none
   !important` на --focused/--highlighted control и `display:none` на
   ::after underline (на случай dense Quasar variants).
3. Mini-wallet в правом верхнем углу AppHeader: слот #wallet удалён
   из API (баланс уже отображается в RailUserCard в drawer'е,
   дубль в шапке избыточен). Убран из dev-витрины каркаса.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:52:12 +00:00
coopops 0515994833 fix(ui): WalletCard и mini-wallet — снизить визуальный вес цифр
Регрессия после Quasar wrapper-pivot: метрики кошельков выглядели
крупно по сравнению с заголовком/sub-label, а padding'и были
избыточны для compact-варианта.

- .wallet (FULL): padding 18→14px по верт, иконка 44→40px,
  metric-val 22→19px, ccy 13→12px;
- .wallet--row (compact): padding 14→10px, иконка 36→32px,
  metric-val 18→16px — выглядит как row в списке, а не как карточка;
- .rail__balance-val (mini-wallet в drawer): 22→18px — теперь не
  доминирует над identity-блоком пайщика.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:38:31 +00:00
coopops b7717e0e1f fix(ui): canon q-field/q-btn пропорции — убрать раздавливание floating-label
Корень регрессии: жёсткий height: 40px в .q-field--outlined раздавил
вертикальную разметку q-input (Quasar держит padding-top:24px под
floating-label слот; при сжатии текст значения сдвигается). Те же
эффекты для select.

- BaseInput/BaseSelect → dense + stack-label по умолчанию;
  Quasar сам корректно держит 40px в dense, метка статически стоит
  над инпутом без floating-механики;
- .q-field--outlined: убран принудительный height; .q-field--dense
  получает только padding-x, высоту контролирует Quasar;
- .q-btn: font-weight 500→450, min-height 40→36px, padding 16→14px —
  кнопки больше не выглядят «жирно/тяжело»;
- .q-btn--outline: hairline-граница через ::before (Quasar pattern),
  без двойной рамки;
- размеры sm/lg/dense пропорционально уменьшены;
- .q-chip: высота 24→26px, padding 10→12px, font-weight 500→450;
  + .q-chip__icon размер 14px (для точечных чипов).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:36:29 +00:00
coopops 1ac70c0405 refactor(desktop/base): остальные base/ и AuthCard → обёртки Quasar
BaseSelect → q-select (outlined, reserve-hint-space, map-options/emit-value
для совместимости с {value,label} options API).

BaseCard → q-card flat с q-card-section head/body. Variants flat/inset/quiet
override-ят через scoped style.

BaseChip → q-chip с color mapping (variant → primary/positive/negative/
warning/info). Square для canon-look.

BaseBadge → q-badge с тем же color mapping. Dot-вариант через scoped style
(8×8 круг).

BaseBanner → q-banner rounded dense с border-left по variant.

BaseDialog → q-dialog + q-card с size→maxWidth mapping. closeOnBackdrop/
closeOnEscape пробрасываются через :persistent / :no-*-dismiss.

BaseTable → q-table с трансформацией columns в QTableProps['columns'].
Hide-pagination, binary-state-sort. Cell-slots работают через body-cell-{key}.

AuthCard (domain) — был на native canon `.card` div. Переведён на q-card flat
для консистентности. Усиленный shadow + accent-stripe сверху сохранены.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:12:12 +00:00
coopops 8ddcd30bc9 refactor(desktop/base): BaseInput/Button/Form → обёртки Quasar
BaseInput теперь рендерит <q-input outlined reserve-hint-space no-error-icon>
с проброшенными slots. reserve-hint-space от Quasar резервирует место
под error/hint встроенно — раньше делали вручную через NBSP. Все Quasar
features (rules через v-bind \$attrs, mask, ref API, native slots) работают
прозрачно. Props API совместим (modelValue, label, hint, error, prefix,
suffix, mono, readonly, disabled, autocomplete, name, id, type).

BaseButton — q-btn с variant→Quasar mapping:
  primary → :unelevated color="primary" (заливка accent)
  secondary → :outline (border + surface)
  ghost → :flat (transparent)
  danger → :flat color="negative" (transparent + neg-cвет)
:no-caps :ripple=false для canon-look. Props API совместим.

BaseForm — q-form-обёртка с сводным error-баннером (q-banner) и slot footer.
banner—neg styling прилетает из quasar-canon.css через .bg-negative-soft.

Глобальная стилизация Quasar в quasar-canon.css даёт canon-look всем
этим компонентам и существующим q-input/q-btn/q-card по всему проекту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:09:34 +00:00
coopops 82f44284fa feat(desktop/css): canon-стилизация Quasar поверх (quasar-canon.css)
Архитектурный pivot: вместо native HTML5-компонентов под canon-классами
стилизуем Quasar поверх. Причины:
- Сохраняется весь Quasar API из коробки (rules/mask/dense/slots/refs)
- Существующий код (сотни q-input, q-btn, q-card, q-select) автоматом
  получает canon-look без правок
- Меньше регрессий — Quasar отвечает за accessibility/keyboard/focus

Файл src/css/mono-platform/quasar-canon.css содержит overrides под canon
для: q-field--outlined (q-input/q-select), q-btn (unelevated/outline/flat),
q-card, q-chip, q-badge, q-banner, q-dialog/q-menu, q-item, q-table,
q-checkbox/q-toggle.

Подключен в quasar.config.cjs css[] после components.css. tokens.css
без изменений — все цвета/радиусы/тени тянутся как переменные.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:08:36 +00:00
coopops 6029342c7e fix(desktop): удалить legacy глобальный .card { transform: scale(1.03) }
Истинный источник «прыгалки» AuthCard на hover: глобальное правило
`.card:hover { transform: scale(1.03) }` в src/app/styles/style.css
(и его дубль в pages/Marketplace/MainPage/ui/MainPage.vue, тоже не scoped)
бьёт по любому элементу с классом card. AuthCard имеет корневой
<div class="card auth-card"> — каждый hover дёргает её на 3%.

Поиск показал: класс `card` без модификаторов в templates никто кроме
новых canon-компонентов (BaseCard, AuthCard) не использует. Этот глобал —
мёртвый legacy, удаляем безопасно.

NBSP-фикс layout-shift из предыдущего коммита (2a2d9a786a) остаётся
актуальным — он лечил другую (на уровне поля) проблему.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:06:59 +00:00
coopops 2a2d9a786a fix(desktop/ui): добить layout-shift, ужать gap полей, accent-стрип AuthCard, затемнить canvas
1) BaseInput — layout shift жив: text-interpolation `{{ error || hint || ' ' }}`
   рендерила обычный пробел, браузер схлопывает text-node с одним whitespace
   в 0-width content → div сжимается → форма прыгает. Заменили на
   `<span>&nbsp;</span>` (NBSP в DOM, не схлопывается).

2) BaseInput — canon `.field { margin-bottom: 16px }` создавал слишком
   большой разрыв между полями (16px margin + 10px form gap + 17px message =
   ~43px). Override `.field { margin-bottom: 0 }` в scoped — расстояние
   управляется gap'ом BaseForm.

3) AuthCard — статичный accent-стрип 3px сверху (`::before`). Визуальный
   якорь без анимации — даёт бренд-маркер «здесь главное».

4) tokens.css light — canvas #fafafa → #f4f4f5 (cool-gray), canvas-2
   #f4f4f5 → #ebebed. Surface остаётся #ffffff, теперь карточка чётче
   проступает на фоне.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:54:32 +00:00
coopops d6ab6e3cc6 fix(desktop/ui): убрать layout shift в BaseInput и оживить AuthCard на светлой
BaseInput: контейнер field__message теперь рендерится всегда с min-height
calc(--p-fs-meta * 1.4) — раньше div появлялся/исчезал на каждом keystroke
при валидации (см. LostKey emailError), форма прыгала вверх-вниз.
Когда нет ни ошибки, ни хинта — рендерится NBSP, высота сохраняется.

AuthCard: добавлен box-shadow поверх canon-минималистского .card —
canvas #fafafa и surface #ffffff на светлой почти неразличимы, без тени
карточка выглядит плоской. На тёмной отдельный shadow усилен (поверх
canon `--p-shadow-card`, который оптимизирован под data-cards, не hero).
border-color подкручен с --p-line на --p-line-1 для большей чёткости.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:48:01 +00:00
coopops 5d1d79d9ea feat(desktop/LostKey): E6 Story 6.3 — миграция «Перевыпуск ключа» на canon
Scope-adjustment: plan'овая Story 6.3 предполагала миграцию SignUp как
второго auth-flow, но Register — multi-step wizard (10+ файлов в
pages/Registrator/SignUp/), который требует VerticalStepper из Эпика 10
и не помещается в Волну 1. Вместо этого мигрируем LostKey — простую
одно-полевую auth-страницу, того же auth-домена.

LostKey (widget): pug → html, старый AuthCard (q-card с shimmer) →
canon AuthCard. q-input → BaseInput с inline email-валидацией;
q-btn → BaseButton; всё обёрнуто в BaseForm с loading/error.
Бизнес-логика (useLostKey.startResetKey + router redirect на resetkey)
сохранена 1:1.

LostKeyPage: footer-кнопка «Назад» через ghost BaseButton в footer-slot
AuthCard; центровка через flexbox.

Не мигрировано в Волне 1 (известный gap):
- SignUp wizard (требует VerticalStepper из E10)
- ResetKey / Invite (на старом AuthCard, мигрируются в Волне 2)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:29:04 +00:00
coopops 34e1ea8a37 feat(desktop/SignIn): E6 Story 6.2 — миграция Login на canon-компоненты
LoginForm (feature): q-input → BaseInput, q-btn → BaseButton, обёрнут в
BaseForm с управлением loading/error. Бизнес-логика (useLoginUser, redirect,
OpenReplay tracking, NotificationPermissionDialog) сохранена 1:1.
Ошибки входа теперь показываются через сводный error BaseForm
(плюс legacy FailAlert остаётся как fallback).

SignIn (widget): импорт AuthCard переключён на новый canon-композит
(src/shared/ui/domain/AuthCard), убран pug-template и градиентные стили.
Добавлен footer-slot — пробрасывается в AuthCard.

SignInPage: переписан с pug → html, footer-кнопки «Потеряли ключ?» /
«Нет аккаунта?» вынесены в footer-slot AuthCard через BaseButton ghost-sm.
Центровка через flexbox вместо QRow grid.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:28:31 +00:00
coopops ef80504a80 feat(desktop/AuthCard): E6 Story 6.1 — AuthCard композит + AuthLayout
AuthCard (src/shared/ui/domain/AuthCard) — canon .card + center-aligned
head (title/subtitle/slot head) + body-slot + опциональный footer-slot.
Старый src/shared/ui/AuthCard (q-card с shimmer-градиентом) остаётся
для не-мигрированных страниц (SignUp wizard, ResetKey, Invite).

AuthLayout (src/app/layouts/AuthLayout.vue) — wrapper-обёртка для full-page
auth-страниц: фон var(--p-canvas), центрирование, тогл темы в правом верхнем.

Витрина: dev-секция 19 с примером login-формы (BaseForm + BaseInput ×2 +
BaseButton + footer-ссылки).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:27:11 +00:00
coopops 51f89b3fe4 feat(desktop/widgets/wallet-card-mini): E5 Story 5.2 — connected-обёртка WalletCard
WalletCardMini подключает WalletCard к Pinia (useWalletStore.program_wallets) +
System store (info.symbols.root_govern_symbol) + useRouter (клик →
name: 'wallet'). Программный mapping: wallet→MAIN, blagorost→BLAGOROST,
generator→GENERATOR (Zeus.ProgramType). Loading определяется автоматически
по наличию данных в store (или пробрасывается через prop).

Подключено в слот wallet AppHeader на dev-витрине (секция 16 — каркас) +
выделенная секция 18 с тремя программами. На dev-странице покажет
skeleton-loading т.к. loadUserWallet не вызывался; на реальных страницах
после init процесса — настоящий баланс.

Со-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:00:57 +00:00
coopops 50b95de57b fix(desktop/_dev/ui): корректные подписи программ Благорост/Генератор в витрине WalletCard
Благорост — капитализация РИД (не «накопления на жильё»), Генератор —
генерация РИД (не «доходы от программы»). Кошелёк остаётся «свободный
остаток». Черновые ярлыки риском попадают в production через копипасту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 16:48:01 +00:00
coopops 345b226f84 feat(desktop/WalletCard): E5 Story 5.1 — props-only компонент с программными акцентами
Канонический .wallet с компактным (--row) и full-вариантами, программные
акценты через --prog-blagorost / --prog-wallet / --prog-generator (UX-DR20).
Витрина на /_dev/ui секция 17: три программы × оба варианта + loading +
empty + locked-line.

Compact (.wallet--row): 36px icon, 18px sum, 14px padding — для слота шапки.
Full (.wallet): 44px icon, 22px sum, 18px padding — для дашборда.

Connected-обёртка widgets/wallet-card-mini (Story 5.2) делается отдельным
коммитом, требует интеграции с реальным wallet-store и MARKETPLACE_ASSET_CONFIG.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 16:41:54 +00:00
coopops 8af8bf7512 revert(desktop/RailUserCard): убрать transition свёртки — baseline-skipping
Анимация max-height + opacity при свёртке давала видимый прыжок крупной
суммы баланса (`font-size: 22px` + `display: flex; align-items: baseline`):
во время промежуточных кадров baseline переcчитывался относительно
схлопывающегося контейнера → текст «прыгал в середину» и обратно.

Возвращаемся к canon `display: none`. Resize моментальный, без skipping.
Поведение rail (top опускается, bottom-chevron остаётся через flex-spacer)
работает и без анимации — пользователь это и подтвердил в фидбеке.

Chevron rotation через :deep(.q-icon) и balance-route не трогаем —
они независимы.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:33:04 +00:00
coopops 5ef499a75f feat(desktop/_dev/ui): TokenPalette + chevron full-width fix
1. **Палитра системы.** Новая секция «00» в шапке dev-страницы со всеми
   основными --p-* токенами:
   - Акцент (accent / hover / press)
   - Поверхности (canvas / canvas-2 / surface / 2 / 3)
   - Текст (ink / ink-on-accent)
   - Линии (line / 1 / 2) — текстовый input для rgba
   - Статусы (pos / neg / warn / info)
   - Программные тинты (blagorost / wallet / generator)

   Color-picker для hex-токенов, текстовый ввод для rgba. Live-override
   через `document.documentElement.style.setProperty()` — inline-style на
   :root перебивает CSS-правила, перекрашивание мгновенное. Подсветка
   переопределённых токенов + счётчик в шапке + кнопка «Сбросить» (через
   removeProperty). Watch на Quasar Dark — при смене темы пикеры
   обновляются на новые resolved-значения.

2. **Chevron в свёрнутом виде — full-width.** Из-за того что для плавной
   анимации primary остаётся в DOM (max-height:0 вместо display:none),
   у него сохранялся `flex: 1` и он съедал ширину карточки → chevron не
   мог растянуться через canon-правило `width: 100%`. В `.is-collapsed`
   добавили `flex: 0 0 0` + `width: 0` для primary — chevron теперь
   корректно занимает всю ширину.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:31:54 +00:00
coopops 79afee2b82 fix(desktop/RailUserCard): chevron rotation, плавная свёртка, balance-route
По фидбеку:

1. **Chevron не переворачивается.** Canon-правило
   `.rail__usercard__collapse svg { transform: rotate(180deg) }` рассчитано
   на `<svg>`, а Quasar `<q-icon>` рендерит `<i class="q-icon">`. Прокидываем
   transition + rotate через scoped `:deep(.q-icon)` — chevron теперь крутится
   при свёртке/развёртке.

2. **«Карточка улетает»** — резкий jump при `display:none` создаёт визуальный
   прыжок. Подменяем display:none на `max-height: 0` + `opacity: 0` с
   transition'ом. Логика canon-селектора `.is-collapsed` не нарушается,
   просто добавлена плавность. Если структурное поведение rail (top
   опускается, bottom-chevron остаётся на месте) всё равно неудобно —
   перевёрстаем во вторую итерацию.

3. **Клик по балансу → маршрут кошелька.** Новый prop `balanceRoute` —
   если задан, блок `.rail__balance` оборачивается в `<router-link :to>`.
   Параллельно эмитим `balance-click` — для аналитики или функционального
   handler'а. На витрине привязал к `/_dev/ui#wallet` чтобы проверить.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:28:31 +00:00
coopops 0a977dcd83 feat(desktop/domain): RailUserCard + фикс RouteMeta для dev-роута
1. `RouteMeta.icon` объявлен обязательным в `src/env.d.ts` — добавили
   `icon: 'fa-solid fa-flask'` для dev-роута, иначе vue-tsc валится.

2. `RailUserCard` (`shared/ui/domain/RailUserCard`) — доменный
   общеплатформенный компонент мини-кошелька в нижней части drawer'а.
   Это будущий E11 (Волна 2), но логично вытащить раньше, чтобы основной
   layout можно было заменить целиком уже сейчас.

   Реализован по canon строго: rail__usercard с usertop+balance+actions,
   collapse-chevron справа с поворотом arrow при свёртке (через
   `.is-collapsed` модификатор + canon CSS), отдельный `rail__signout`
   блок (выходит вне `.rail__usercard` — рендерится сразу после, в том
   же `aside.rail` через footer-slot).

   Контракт строго dumb по архитектуре: только props/emits/slots.
   - props: name, role, avatarSrc, balance, symbol, balanceLabel,
     lockedBalance, lockedLabel, primaryActionLabel, collapsed,
     showSignout, signoutLabel
   - emits: primary-action, update:collapsed (v-model), signout
   - slots: usertop-extra, actions

   Coнnected-обёртки (с инжектом store/auth) — на уровне страницы,
   не здесь.

3. Витрина `/_dev/ui`:
   - Секция 12 (AppDrawer): заменили временный огрызок rail-demo__user
     на полноценный `<RailUserCard show-signout>` через footer-slot.
   - Секция 16 (Каркас приложения): аналогично — теперь у rail полноценный
     footer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:19:09 +00:00
coopops 74539a6c46 feat(desktop/layout): E4 — AppDrawer + AppHeader + PageHead + PageTabs
E4 «Layout-каркас» Волны 1: 4 структурные обёртки в `src/shared/ui/layout/`.
По архитектуре — только props/emits/slots, без store/router/api, без знания
о данных. Cтраницы передают menu-items, breadcrumbs, табы и активные ключи
сверху.

Состав:
- `AppDrawer` — canon `.rail`: solid surface, активный пункт soft-accent
  пилюлей + 2-px рейл слева. Поддерживает плоский список + секции
  (RailSection с eyebrow-меткой), opt-in ⌘K-кнопку, slot `footer` для
  user-card / logout.
- `AppHeader` — canon `.topbar`: бургер · crumb · `topbar__actions` ·
  `topbar__right`. Поддерживает простой title и multi-level breadcrumb.
  Глобальные действия через слоты `notifications`/`theme`/`wallet`/`profile`
  или общий `right`.
- `PageHead` — canon `.page-head`: eyebrow + title + subtitle + slot
  `actions`. Используется когда страница БЕЗ подстраниц.
- `PageTabs` — canon `.tabbar` + `.tab`: вкладки подстраниц с tab__count,
  опциональный hairline-разделитель и `tabbar__actions` справа.

Также в составе коммита:
- Убрали AuthCard из base — он композит (cap + body + head + footer),
  переедет в E6 Auth flow рядом с SignUp/ResetKey/Invite.
- BaseForm: gap полей 16→10px (тесные формы как в каноне).
- Витрина `/_dev/ui`: 4 новые секции (12-15) на каждый layout-компонент +
  секция 16 «Каркас приложения» — AppDrawer + AppHeader + PageTabs в живой
  связке без legacy QLayout/QDrawer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 06:17:01 +00:00
coopops dc93704990 fix(desktop/mono-platform): убрать HTML-теги <style> из canon CSS
Скрипт извлечения из bundled HTML оставил `  <style>...` в начале и
`</style>` в конце обоих файлов. CSS-парсер ловил `<` как ошибочный
селектор и пропускал блок до ближайшей `}` — съедая весь первый блок
правил.

В tokens.css первым блоком был `:root, [data-theme="light"]` со светлой
палитрой — она целиком игнорировалась. `[data-theme="dark"]` ниже
парсился нормально, поэтому тёмная тема работала, а светлая — нет:
кнопки primary/ghost/danger оставались с transparent background и
сливались с canvas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 05:54:25 +00:00
coopops 02a4a2aa14 feat(desktop): MONO Platform v2 — базовые компоненты дизайн-системы
Заносим эталонную дизайн-систему в проект — только базовые элементы для
дальнейшей компоновки. Source — MONO Design System.html.

Архитектура (по `_bmad-output/planning-artifacts/architecture.md`):
- `src/css/mono-platform/{tokens.css,components.css}` — canon токены и стили
  компонентов 1:1 из эталона; импортируются первыми в quasar.config css[].
- `src/shared/ui/base/<Имя>/{Vue,types,index}` — 14 базовых обёрток (Vue 3 +
  TS, native HTML над canon BEM, no Quasar-deps кроме Dark API в ThemeToggle).
  FSD-граница: только props/emits/slots, никаких store/router/api.
- `src/boot/ui.ts` — глобальная регистрация 14 базовых компонентов.
- `src/boot/theme.ts` — sync `html[data-theme]` ↔ Quasar Dark.
- Inter + JetBrains Mono в index.html.

14 базовых:
BaseButton, BaseInput, BaseSelect, BaseCard, BaseTable, BaseChip, BaseBadge,
BaseDialog, BaseBanner, BaseForm, EmptyState, Avatar, ThemeToggle, AuthCard.

Витрина `/_dev/ui` (dev-only) — каждая обёртка отдельной секцией над чистым
canon-канвасом, без layout-обёрток. Маршрут смонтирован на корневом уровне,
ВНЕ DynamicLayoutWrapper, чтобы legacy default.vue не перебивал стили.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 05:30:28 +00:00
1398 changed files with 70529 additions and 33520 deletions
+188 -200
View File
@@ -1,49 +1,60 @@
name: Release
# Единый релизный workflow по push'у тэга `v*`.
# Релизный workflow по push'у в testnet/main (FF-промоушн-модель, см.
# scripts/RELEASE.md).
#
# Порядок (через jobs.needs):
# 1) release — контракты + контейнеры + webhook деплоя (атомарно)
# 2) publish-packages — npm publish через lerna (если не -alpha)
# 3) publish-docs — mkdocs + standards-site → gh-pages (если не -alpha + ветка main)
# 4) trigger-coopenomics — repository_dispatch в coopenomics/coopenomics (тот же гейт, что docs)
# 2) publish-packages — npm publish через lerna (только ветка main)
# 3) trigger-coopenomics — workflow_dispatch сборки сайта C9S/coopenomics на
# Gitea (только ветка main); он сам тянет mono и
# пересобирает доки.
#
# publish-docs (gh-pages на github.com) удалён 2026-05-25 — после переезда на
# Gitea Pages-публикация невалидна; доки деплоятся через C9S/coopenomics.
#
# Зачем последовательно: пакеты/доки/внешний триггер не должны уезжать,
# если релиз контрактов или контейнеров провалился. Раньше четыре workflow'а
# стартовали параллельно по одному тэгу — отсюда гонка из инцидента 2026-05-13
# с ledger2 walletop sweep'ом (см. историю release.yaml до объединения).
# если релиз контрактов или контейнеров провалился.
#
# Гейт `-alpha` унифицирован: пакеты + доки + триггер docs пропускают только
# не-alpha тэги. -beta/-rc/-test в проекте не используются (см. CLAUDE.md),
# поэтому проще иметь один паттерн `!contains(github.ref, '-alpha')`.
# IS_PROD в release-job остаётся на `(alpha|beta|rc|test)` — он определяет
# деплой-webhook (testnet vs production) и тэг `:latest` у образов;
# намеренно строже, чтобы случайный -rc-тэг не уехал на прод.
# Триггер — push в testnet/main с изменением lerna.json. Версию бампает
# `lerna version` ОДИН раз на dev (scripts/cut-release.sh); тот же коммит едет
# вверх по fast-forward (scripts/promote.sh). lerna.json меняется только на
# релизном бампе, поэтому обычные feature-пуши деплой не триггерят. dev в
# триггере НЕТ: cut на dev только бампает версию, деплой начинается с testnet
# (staging) и завершается main (production).
#
# Резолв ветки. Тэг семантики окружения не несёт (например `v2026.5.13-alpha-2`
# может лежать на testnet или dev — определяет именно ветка, на которую сделан
# merge перед тэгом). Поэтому ищем первое совпадение SHA с remote-ветками
# main/testnet/dev. branch экспортируется как outputs.branch для downstream jobs.
# Окружение определяет ВЕТКА (main → production, testnet → staging), а не
# суффикс версии: единая версия едет во все среды, поэтому source of truth —
# ветка, на которую сделан промоушн. Версия читается из закоммиченного
# lerna.json. Гейты npm-publish/доки — `branch == main`.
on:
push:
tags:
- 'v*'
branches: [testnet, main]
paths:
- 'lerna.json'
# Ручной деплой: выбрать окружение и (опционально) версию/коммит. Нужен для
# отката на старую версию, редеплоя без бампа и точечного hotfix в один контур —
# всё, что FF-промоушн (scripts/promote.sh, только вперёд) сделать не может.
workflow_dispatch:
inputs:
environment:
description: 'Куда деплоить'
type: choice
options:
- testnet
- main
required: true
ref:
description: 'Релизный тег/коммит для сборки (пусто = верхушка выбранного окружения)'
required: false
default: ''
permissions:
contents: write
jobs:
# Гейт TS-типов перед сборкой образов. Reusable workflow `typecheck.yaml`
# запускает vue-tsc на desktop и tsc --noEmit на controller. Нужно потому,
# что `lerna run build` в корневом Dockerfile НЕ ловит TS-ошибки:
# quasar build идёт через esbuild с выключенным vueTsc, а у controller'а
# build-скрипта вообще нет (ts-node в рантайме).
typecheck:
uses: ./.github/workflows/typecheck.yaml
release:
needs: typecheck
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.resolve.outputs.tag_name }}
@@ -54,23 +65,32 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
# push → собираем пушнутый коммит; ручной запуск → выбранный ref
# (пусто = верхушка ветки окружения, на которой запущен workflow).
ref: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.ref) || github.sha }}
- name: Resolve branch, build mode and tags
- name: Resolve branch, build mode and version
id: resolve
run: |
TAG_NAME="${{ github.ref_name }}"
SHA="${{ github.sha }}"
BRANCH=""
for candidate in main testnet dev; do
if git branch -r --contains "$SHA" 2>/dev/null | grep -qE "^[[:space:]]*origin/${candidate}$"; then
BRANCH="$candidate"
break
fi
done
if [ -z "$BRANCH" ]; then
echo "::error::Тэг $TAG_NAME ($SHA) не лежит ни на одной из dev/testnet/main"
# Окружение: при ручном workflow_dispatch — из inputs.environment;
# при push — это ветка push'а (testnet/main). Версия/сборка берутся
# из реально вычекнутого дерева, не из ветки-триггера.
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
BRANCH="${{ github.event.inputs.environment }}"
else
BRANCH="${{ github.ref_name }}"
fi
SHA="$(git rev-parse HEAD)"
# Версия — из закоммиченного lerna.json. Её бампит lerna version ОДИН
# раз на dev (scripts/cut-release.sh), и тот же коммит едет по FF в
# testnet/main. Тэг как триггер больше не используется.
RAW_VERSION="$(sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' lerna.json | head -1)"
if [ -z "$RAW_VERSION" ]; then
echo "::error::Не удалось прочитать version из lerna.json"
exit 1
fi
TAG_NAME="v${RAW_VERSION}"
case "$BRANCH" in
main)
@@ -88,17 +108,22 @@ jobs:
CONTRACTS_TAG=dev
CONTRACTS_EXTRA=
;;
*)
echo "::error::Неподдерживаемая ветка $BRANCH (ожидались testnet/main/dev)"
exit 1
;;
esac
# IS_PROD определяет webhook деплоя (PRODUCTION vs TESTNET) и тэг :latest.
# Сознательно строже гейта publish-* — случайный -rc/-beta тэг не должен
# уехать на прод-вебхук, даже если в npm/docs его опубликовали.
if [[ "$TAG_NAME" =~ -(alpha|beta|rc|test) ]]; then
IS_PROD=false
WEBHOOK_URL='${{ vars.TESTNET_WEBHOOK_URL }}'
else
# Окружение деплоя определяет ВЕТКА: main → production, иначе → testnet.
# IS_PROD управляет деплой-webhook (PRODUCTION vs TESTNET) и тэгом :latest
# у образов. Раньше критерием был суффикс -alpha у тэга; теперь единая
# версия едет во все среды, поэтому источник истины — ветка промоушна.
if [ "$BRANCH" = "main" ]; then
IS_PROD=true
WEBHOOK_URL='${{ vars.PRODUCTION_WEBHOOK_URL }}'
else
IS_PROD=false
WEBHOOK_URL='${{ vars.TESTNET_WEBHOOK_URL }}'
fi
{
@@ -127,14 +152,41 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
# === Этап 1: контракты ===
# CDT ставим из .deb (C9S/cdt v4.2.0) прямо в окружение job'а и
# компилируем напрямую — БЕЗ вложенного docker. Почему не build-all.sh:
# тот монтирует $(pwd):/project в sibling-контейнер, а под Gitea
# act_runner сам job исполняется в контейнере → хостовый демон не видит
# этот путь, /project пуст, cmake падает "no CMakeLists.txt".
# build-all.sh остаётся для локальной сборки (оборачивает тот же
# build_contracts_cdt.sh в docker).
# CMakeLists хардкодит toolchain /cdt/build/...; .deb кладёт CDT в
# /usr/opt/cdt/4.2.0, а сам CDTWasmToolchain.cmake указывает на /usr
# абсолютно — поэтому симлинк /cdt/build → /usr/opt/cdt/4.2.0 сводит пути
# без правки CMakeLists.
- name: Pull CDT toolchain image
run: docker pull dicoop/blockchain_v5.1.1:dev
- name: Install CDT 4.2.0 toolchain (.deb)
run: |
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
$SUDO apt-get update
# build-essential — для host-компилятора (project() в CMakeLists);
# libz3-4/libtinfo6/libxml2/zlib1g — рантайм бинарей CDT (clang-9,
# ld.lld и т.д.). Пакет cdt объявляет только libcurl4-gnutls-dev,
# поэтому остальные .so ставим явно — в образе dicoop/blockchain они
# были из сборки исходников, из .deb не тянутся.
$SUDO apt-get install -y --no-install-recommends \
curl ca-certificates cmake make build-essential \
libz3-4 libtinfo6 libxml2 zlib1g
curl -fsSL -o /tmp/cdt.deb \
https://git.coopenomics.world/C9S/cdt/releases/download/v4.2.0/cdt_4.2.0-1_amd64.deb
$SUDO apt-get install -y /tmp/cdt.deb
$SUDO mkdir -p /cdt
$SUDO ln -sfn /usr/opt/cdt/4.2.0 /cdt/build
cdt-cpp --version || true
- name: Compile contracts
working-directory: components/contracts
run: |
./build-all.sh "$BUILD_MODE"
./build_contracts_cdt.sh "$BUILD_MODE"
echo "--- build/contracts ---"
ls -la build/contracts/
@@ -215,6 +267,17 @@ jobs:
- name: Build and push contracts image
working-directory: components/contracts
run: |
# Ретрай push'а: резолвинг registry-1.docker.io на runner'е изредка
# моргает (DNS-таймаут к 127.0.0.53) — 3 попытки с паузой.
dpush() {
local ref="$1" n=1
until docker push "$ref"; do
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
n=$((n+1)); sleep 10
done
}
IMAGE="dicoop/contracts"
SHORT_SHA="${SHA::7}"
docker build \
@@ -223,14 +286,14 @@ jobs:
--label "build.mode=$BUILD_MODE" \
-t "$IMAGE:$CONTRACTS_TAG" \
./docker/.context
docker push "$IMAGE:$CONTRACTS_TAG"
dpush "$IMAGE:$CONTRACTS_TAG"
docker tag "$IMAGE:$CONTRACTS_TAG" "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
docker push "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
dpush "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
if [ -n "$CONTRACTS_EXTRA" ]; then
docker tag "$IMAGE:$CONTRACTS_TAG" "$IMAGE:$CONTRACTS_EXTRA"
docker push "$IMAGE:$CONTRACTS_EXTRA"
dpush "$IMAGE:$CONTRACTS_EXTRA"
fi
- name: Verify pushed contracts image
@@ -243,15 +306,31 @@ jobs:
- name: Build and push base image
run: |
dpush() {
local ref="$1" n=1
until docker push "$ref"; do
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
n=$((n+1)); sleep 10
done
}
docker build --target runtime -t "dicoop/mono-base:$TAG_NAME" .
docker push "dicoop/mono-base:$TAG_NAME"
dpush "dicoop/mono-base:$TAG_NAME"
if [ "$IS_PROD" = "true" ]; then
docker tag "dicoop/mono-base:$TAG_NAME" dicoop/mono-base:latest
docker push dicoop/mono-base:latest
dpush dicoop/mono-base:latest
fi
- name: Build and push service images
run: |
dpush() {
local ref="$1" n=1
until docker push "$ref"; do
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
n=$((n+1)); sleep 10
done
}
build_service() {
local SVC="$1" PKG="$2" CMD="$3"
local DOCKERFILE="Dockerfile.$SVC"
@@ -260,10 +339,10 @@ jobs:
echo "CMD [\"pnpm\", \"-F\", \"$PKG\", \"run\", \"$CMD\"]"
} > "$DOCKERFILE"
docker build -t "dicoop/$SVC:$TAG_NAME" -f "$DOCKERFILE" .
docker push "dicoop/$SVC:$TAG_NAME"
dpush "dicoop/$SVC:$TAG_NAME"
if [ "$IS_PROD" = "true" ]; then
docker tag "dicoop/$SVC:$TAG_NAME" "dicoop/$SVC:latest"
docker push "dicoop/$SVC:latest"
dpush "dicoop/$SVC:latest"
fi
}
@@ -285,14 +364,16 @@ jobs:
# ============================================================================
# publish-packages — npm publish (бывший publish-packages.yaml).
# Гейт: не-alpha. Не зависит от ветки.
# Гейт: ветка main. Версия для publish — из закоммиченного package.json
# (lerna publish from-package); её бампит cut-release.sh на dev, тот же коммит
# доезжает до main по FF.
# Жёстко прибиваем версию pnpm, которой сгенерён lockfile (lockfileVersion 9.0
# = pnpm 9/10). Иначе action-setup может подтянуть другую версию и переписать
# pnpm-lock.yaml → Lerna EUNCOMMIT.
# ============================================================================
publish-packages:
needs: release
if: ${{ !contains(github.ref, '-alpha') }}
if: ${{ needs.release.outputs.branch == 'main' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -309,154 +390,61 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm lerna run build
# schema.gql не коммитится и не создаётся ничем в этом job'е, а
# @coopenomics/sdk's prepublishOnly (pnpm run docs → generate-index-comments.ts)
# читает components/controller/schema.gql и падает ENOENT без него — валя весь
# lerna publish (атомарный) молча с 30 мая. Скрипт изолированный (GraphQLSchemaBuilderModule,
# без TypeORM/Mongo/Redis) — не требует БД, но типизация controller'а резолвится через
# dist соседних workspace-пакетов (cooptypes/factory/notifications/inter) — ОБЯЗАТЕЛЬНО
# после `lerna run build`, иначе ts-node падает на их устаревших dist-типах.
- name: Generate controller schema.gql (required by sdk prepublishOnly)
working-directory: components/controller
run: pnpm run generate-schema
- run: pnpm lerna publish from-package --yes
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# ============================================================================
# publish-docs — mkdocs + standards-site → gh-pages (бывший publish-docs.yaml).
# Гейт: не-alpha + ветка main.
# ВНИМАНИЕ: gh-pages step пушит на github.com/coopenomics/mono — это GitHub
# Pages. При переезде на Gitea Actions нужно будет либо подложить PAT для
# внешнего push'а в GitHub, либо переключить публикацию на Gitea Pages.
# ============================================================================
publish-docs:
needs: release
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Версия pnpm берётся из `packageManager` корневого package.json,
# синхронно с publish-packages job'ом и Dockerfile'ами.
- name: Set up pnpm
uses: pnpm/action-setup@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Python requirements
run: |
python -m venv venv
source venv/bin/activate
pip install mkdocs-material mkdocs-macros-plugin mkdocs-section-index pymdown-extensions
working-directory: ./components/docs
- name: Install Node.js dependencies
run: pnpm install
working-directory: ./components/docs
- name: Patch spectaql-config.yml for CI
run: |
sed -i.bak "0,/url:.*/s|url:.*|url: 'https://testnet.coopenomics.world/backend/v1/graphql'|" spectaql-config.yml
working-directory: ./components/controller
- name: Show patched spectaql-config.yml
run: cat spectaql-config.yml
working-directory: ./components/controller
- name: Build cooptypes
run: pnpm run build
working-directory: ./components/cooptypes
- name: Generate controller docs
run: pnpm run docs
working-directory: ./components/controller
- name: Copy controller docs
run: |
mkdir -p ./components/docs/docs/graphql
cp -r ./components/controller/docs/* ./components/docs/docs/graphql/
- name: Generate sdk docs
run: pnpm run docs
working-directory: ./components/sdk
- name: Copy sdk docs
run: |
mkdir -p ./components/docs/docs/sdk
cp -r ./components/sdk/docs/* ./components/docs/docs/sdk/
- name: Generate cooptypes docs
run: pnpm run docs
working-directory: ./components/cooptypes
- name: Copy cooptypes docs
run: |
mkdir -p ./components/docs/docs/cooptypes
cp -r ./components/cooptypes/docs/* ./components/docs/docs/cooptypes/
# ── standards-site → /standards/ на docs.цифровой-кооператив.рф ──
# Отдельный Vue-сайт с BPMN-графом кооперативных стандартов,
# публикуется как поддиректория на том же домене. Vite собирает
# с base='/standards/' (см. vite.config.ts), относительные пути
# внутри dist/ и hash-router работают корректно.
# dist кладём в docs/standards/ ДО mkdocs build — так же, как
# graphql/sdk/cooptypes; mkdocs сам включит его в итоговый site/.
- name: Build standards-site
run: pnpm run build
working-directory: ./components/contracts/standards-site
- name: Copy standards-site into docs/standards
run: |
mkdir -p ./components/docs/docs/standards
cp -r ./components/contracts/standards-site/dist/* ./components/docs/docs/standards/
- name: Build docs (mkdocs)
run: |
source venv/bin/activate
mkdocs build
working-directory: ./components/docs
- name: Remove specific large file before publishing
run: |
rm -f ./components/docs/site/sdk/typedoc.json
if [ -f "./components/docs/site/sdk/typedoc.json" ]; then
echo "ERROR: typedoc.json still exists!"
exit 1
else
echo "SUCCESS: typedoc.json removed successfully"
fi
- name: Publish to GitHub Pages
run: npx gh-pages --nojekyll -d site --repo https://x-access-token:${GITHUB_TOKEN}@github.com/coopenomics/mono.git
working-directory: ./components/docs
env:
GIT_AUTHOR_NAME: github-actions
GIT_AUTHOR_EMAIL: github-actions@github.com
GIT_COMMITTER_NAME: github-actions
GIT_COMMITTER_EMAIL: github-actions@github.com
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger docs deployment webhook
if: ${{ success() }}
run: |
curl -X POST "${{ vars.DOCS_DEPLOY_WEBHOOK_URL }}" \
-H 'Content-Type: application/json' \
-d '{"ref":"${{ github.ref }}","sha":"${{ github.sha }}","branch":"${{ github.ref_name }}"}'
# ============================================================================
# trigger-coopenomics-docs — repository_dispatch в coopenomics/coopenomics
# (бывший build-contracts-docs.yaml). Гейт идентичен publish-docs.
# trigger-coopenomics-docs — пересборка сайта C9S/coopenomics.
# Гейт: ветка main.
#
# coopenomics переехал на Gitea (C9S/coopenomics); его publish-docs.yaml
# клонирует mono и пересобирает сайт. Gitea НЕ имеет API для
# repository_dispatch — только workflow_dispatch, поэтому дёргаем целевой
# workflow через Gitea API с PAT (secret DOCS_DISPATCH_TOKEN — префикс GITEA_
# у секретов зарезервирован Gitea, нельзя; права write на C9S/coopenomics).
# Целевой workflow слушает workflow_dispatch с входами
# mono_sha/mono_ref.
# ============================================================================
trigger-coopenomics-docs:
needs: release
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
if: ${{ needs.release.outputs.branch == 'main' }}
runs-on: ubuntu-latest
steps:
- name: Trigger Coopenomics deployment
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.COOPENOMICS_PAT }}
repository: coopenomics/coopenomics
event-type: deploy_from_mono
client-payload: '{"repository": "${{ github.repository }}", "sha": "${{ github.sha }}", "ref": "${{ github.ref }}", "actor": "${{ github.actor }}"}'
- name: Trigger coopenomics website rebuild (Gitea workflow_dispatch)
run: |
curl -fsSL -X POST \
-H "Authorization: token ${{ secrets.DOCS_DISPATCH_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"ref":"master","inputs":{"mono_sha":"${{ github.sha }}","mono_ref":"${{ github.ref }}"}}' \
"${{ github.server_url }}/api/v1/repos/C9S/coopenomics/actions/workflows/publish-docs.yaml/dispatches"
# ============================================================================
# trigger-mono-docs — деплой ВТОРОЙ документации (сайт доков mono) через
# webhook DOCS_DEPLOY_WEBHOOK_URL. Отдельная от coopenomics публикация —
# обе доки уезжают синхронно по релизу. Извлечён из бывшего publish-docs:
# gh-pages на github.com выпилен как мёртвый, остался реальный deploy-webhook
# (приёмник деплоит доки на своей стороне). Гейт: ветка main.
# ============================================================================
trigger-mono-docs:
needs: release
if: ${{ needs.release.outputs.branch == 'main' }}
runs-on: ubuntu-latest
steps:
- name: Trigger docs deployment webhook
run: |
curl -fsSL -X POST "${{ vars.DOCS_DEPLOY_WEBHOOK_URL }}" \
-H 'Content-Type: application/json' \
-d '{"ref":"${{ github.ref }}","sha":"${{ github.sha }}","branch":"${{ github.ref_name }}"}'
+12 -4
View File
@@ -13,17 +13,25 @@ name: Typecheck
#
# Триггеры:
# - pull_request на dev — гейт перед мерджем в основную ветку;
# - workflow_call — переиспользуется из release.yaml как `needs:` у release-job,
# чтобы битый тэг не уехал в DockerHub.
# - workflow_call — оставлен для переиспользования, но release.yaml его
# больше НЕ вызывает (гейт из релиза убран как избыточный — типы
# проверяются на PR в dev до того, как код доедет до тэга).
#
# Push в dev/testnet/main НЕ триггерит — намеренно (PR-гейт достаточен,
# а тэги покрывает workflow_call из release.yaml).
# Push в dev/testnet/main НЕ триггерит — намеренно (PR-гейт на dev достаточен).
on:
pull_request:
branches: [dev]
workflow_call:
# Новый push в PR-ветку отменяет ещё бегущий typecheck по той же ветке —
# иначе 30-минутные прогоны копятся очередью. Группа по head_ref (исходная
# ветка PR), для workflow_call fallback на ref. Отмена только своих же
# прежних прогонов, разные PR друг друга не трогают.
concurrency:
group: typecheck-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
desktop:
runs-on: ubuntu-latest
+3
View File
@@ -1,4 +1,5 @@
node_modules/
node_modules
lerna-debug.log
components/controller/graph.png
blockchain-data/
@@ -22,3 +23,5 @@ components/contracts/cpp/ledger2/scripts/out/
.env.testnet
.pnpm-store
schema.gql
.claude
+31
View File
@@ -38,6 +38,8 @@ ln -s /home/admin/mono-ai-4/components/controller/.env $WT/components/controller
**Базовая ветка mono-ai-1 — `dev`.** Прямые коммиты в `dev` для мелких фиксов разрешены и не требуют feature-ветки/PR (отменено 2026-05-22 пользователем). Для крупных задач — feature-ветка (`feat/...` или `fix/...`) от dev и PR в dev; merge делает пользователь на GitHub. Push в `main` / `mvp` — по-прежнему только через PR.
**Релиз/деплой — FF-промоушн `dev → testnet → main`** (см. `scripts/RELEASE.md`). Версию бампает `lerna` ОДИН раз на dev (`scripts/cut-release.sh`), тот же коммит едет вверх по fast-forward (`scripts/promote.sh testnet|main`). `testnet`/`main` не несут своих коммитов — только FF-указатели, поэтому merge-конфликтов нет. **В `testnet`/`main` прямых коммитов и повторных бампов версии быть не должно** — это ломает FF. Деплой триггерит push в testnet/main с изменением `lerna.json`; окружение определяет ветка (main → prod). Старые `publish-alpha.sh`/`publish-prod.sh` (merge `-X theirs` + per-branch бамп + back-merge) удалены — они и плодили 20-package.json конфликты.
**Не stash'ить `-u` при unstaged WIP пользователя.** Это создаёт окно для потери при drop/конфликте. Если нужно временно убрать unstaged — `git stash push -- <конкретные-paths>` либо коммит-в-feature, и только свои файлы. Кейс 2026-05-18: после `git stash -u`+`drop` при конфликте потерял WIP пользователя (infra.ts/config.ts/quasar.config.cjs и др.).
### Story-by-story для проекта «Стол заказов» (mono-ai-4 на marketplace2)
@@ -184,6 +186,35 @@ await sendPOST('/v1/graphql', { query: QUERY, variables });
Канон — `features/Branch/CreateBranch/{api,model}/index.ts`: тип в model `export type IXxxInput = Mutations.X.Y.IInput['data']`; функция в api `function (data: IXxxInput) { ... variables: { data } }`. Не делать `as` cast'ов.
## ⚠️ ДИЗАЙН-КАНОН desktop — ОБЯЗАТЕЛЕН К ИСПОЛЬЗОВАНИЮ
**ПРИ ЛЮБОЙ ВЁРСТКЕ В `components/desktop/` НЕ ВЫДУМЫВАТЬ СТИЛИ.** Всегда сначала свериться с каноном. Не строй гипотез о цветах/радиусах/типографике/паттернах из памяти — иди и читай.
**Полная спецификация канона — в skill `/mono-desktop-canon`** (правила обёрток, цвета/токены, иконки, структура страницы, stop-signals). Здесь — короткая выжимка; при расхождении побеждает skill.
**Источник истины — в самом репозитории** (НЕ внешние HTML/прототипы):
| Что | Путь |
|---|---|
| Токены палитры/типографики/spacing/radii (`--p-*`) | `components/desktop/src/css/mono-platform/tokens.css` |
| Прототип-классы компонентов + typography-утилиты `.t-*` | `components/desktop/src/css/mono-platform/components.css` |
| Оверрайды Quasar под канон | `components/desktop/src/css/mono-platform/quasar-canon.css` |
| Минимальная SCSS-палитра Quasar (`$primary`/`$dark`) | `components/desktop/src/css/quasar.variables.scss` |
| Обёртки / доменные блоки / layout | `components/desktop/src/shared/ui/{base,domain,layout}/` |
| **Живой эталон** (`/_dev/ui` в dev-сборке) | `components/desktop/src/pages/_dev/ui/index.vue` |
**При сомнении — открыть `tokens.css` и `_dev/ui/index.vue`, смотреть как сделано там.** Внешний `shared/MONO Design System.html` и `auth-prototype/` каноном НЕ являются — это устаревшие прототипы.
**Запреты:**
- Экран собирается из готовых компонентов: `shared/ui/base` (вместо сырых `q-input`/`q-btn`/`q-card`/`q-table`/`q-chip`/`q-dialog`/`q-select`), `shared/ui/domain` (WalletCard, DataRow, DocumentRow, IdentityPanel…), `shared/ui/layout` (PageHead, PageTabs, AppHeader/AppDrawer). Голый Quasar — только где обёртки нет (`q-icon`, `q-toggle`, `q-list`, `q-menu`, `q-tooltip`, `q-tabs`, `q-separator`, `q-spinner`, `q-inner-loading`…). Props обёрток не угадывать — читать `*.types.ts` рядом.
- Цвет — только токены `var(--p-*)` (поверхности `--p-surface*`, текст `--p-ink*`, линии `--p-line*`, акцент `--p-primary`, статусы `--p-pos/neg/warn/info`) либо utility-классы/color-props. Никаких сырых hex/rgb. Темы light/dark переключаются через `[data-theme]` на `<html>` — токены следуют сами.
- Spacing/радиусы/типографика — токены `--p-1..--p-10` (4px…72px), `--p-r-sm/md/lg/xl`, `--p-fs-*`/`--p-lh-*` либо классы `.t-*`. Без хардкод-px.
- Иконки — `q-icon(name='…')` именами Material Icons. **FontAwesome (`fa-*`) запрещён** — заменять на Material-эквивалент попутно.
- Запрещённые/удаляемые токены: `--mp-*` (старый `marketplace-tokens.scss`), `--tr-*`, `--ds-*`. Легитимны только `--p-*`, `--q-*` (Quasar автоген), `--prog-*`.
- Никаких локальных переопределений `.q-btn`/`.q-card`/`.q-notification` в feature-файлах — Quasar-overrides централизованы в `quasar-canon.css`.
**Кейс 2026-05-28:** при сомнении в каноне пошёл искать его во внешнем HTML/auth-prototype вместо репозитория — перевёрстал не туда, переделывал. SoT — репо (`tokens.css` + `_dev/ui`) и skill `/mono-desktop-canon`, не внешние файлы.
## Frontend desktop — English имена
В `components/desktop/` и любом Vue/TS frontend коде **все имена идентификаторов — английские**:
+8
View File
@@ -45,6 +45,14 @@ RUN corepack enable && npm install -g lerna --no-fund --no-audit
# выкидывает node_modules/dist/.git, поэтому `COPY .` лёгкий.
COPY . .
# electron (devDep desktop → @vue/devtools) и puppeteer нигде в build/runtime
# не запускаются (unbuild/tsc/vite их не трогают) — только их JS-обёртка нужна
# для типов. Их postinstall тянет тяжёлые бинарники (Electron zip с GitHub
# Releases, Chromium) по сети — периодически валит билд TLS-таймаутом.
# Отключаем сами загрузки, пакеты всё равно ставятся.
ENV ELECTRON_SKIP_BINARY_DOWNLOAD=1 \
PUPPETEER_SKIP_DOWNLOAD=true
# Используем существующий lockfile. Если он не совпадает с workspace —
# падать сразу, не дрейфовать незаметно.
RUN pnpm install --frozen-lockfile
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/blago-cli",
"version": "2026.5.23-4",
"version": "2026.7.21-1",
"description": "CLI синхронизации артефактов Благорост с бэкендом через @coopenomics/sdk",
"type": "module",
"private": true,
+2 -2
View File
@@ -362,9 +362,9 @@ export async function runCli(argv: string[]): Promise<void> {
)
.action(async (paths: string[]) => {
const root = requireRoot()
const { stagedPaths, skippedUnchanged, skippedIgnored, skippedPullOnlyArtifacts } = await runAdd(root, paths)
const { stagedPaths, skippedUnchanged, skippedIgnored, skippedPullOnlyArtifacts, skippedNonEntity } = await runAdd(root, paths)
success(
`Staging обновлён: в списке ${stagedPaths.length} путь(ей). Пропущено без изменений относительно индекса: ${skippedUnchanged}; по .blagoignore: ${skippedIgnored}; артефакты только pull (messages/ и meetings/): ${skippedPullOnlyArtifacts}.`,
`Staging обновлён: в списке ${stagedPaths.length} путь(ей). Пропущено без изменений относительно индекса: ${skippedUnchanged}; по .blagoignore: ${skippedIgnored}; артефакты только pull (messages/ и meetings/): ${skippedPullOnlyArtifacts}; не blago-сущности (нет type=project/issue/story во frontmatter): ${skippedNonEntity}.`,
)
})
+16
View File
@@ -38,6 +38,22 @@ export function parseBlagoMarkdown(raw: string): ParsedBlagoFile {
}
}
/** Не бросает: возвращает тип сущности blago, если файл — корректный project/issue/story; иначе undefined. */
export function peekBlagoEntityType(raw: string): EntityFrontmatterType | undefined {
let file: ReturnType<typeof matter>
try {
file = matter(raw)
}
catch {
return undefined
}
const t = file.data?.type
if (t === 'project' || t === 'issue' || t === 'story') {
return t
}
return undefined
}
export function serializeBlagoMarkdown(data: Record<string, unknown>, body: string): string {
const trimmed = body.endsWith('\n') ? body : `${body}\n`
// matter.stringify(string, data) сначала парсит string как frontmatter — body, начинающийся
+40 -11
View File
@@ -3,6 +3,7 @@
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { peekBlagoEntityType } from '../format/index.js'
import { sha256Hex } from '../lib/hash.js'
import { expandBlagoUserTargetsToRelativePaths } from './capital-target-expand.js'
import { isBlagoSyncExcludedDirName, isIgnoredRelativePath, loadBlagoIgnoreRules } from './ignore.js'
@@ -28,19 +29,39 @@ async function collectMarkdownFiles(absDir: string): Promise<string[]> {
return out
}
/** true — файл стоит поставить в staging: нет в индексе или SHA256 на диске ≠ content_etag_local. */
async function isDirtyVsIndex(root: string, rel: string, index: Awaited<ReturnType<typeof loadIndex>>): Promise<boolean> {
const entry = findByRelativePath(index, rel)
if (!entry) {
return true
}
type FileCheck =
| { kind: 'unreadable' }
| { kind: 'non-entity' }
| { kind: 'clean' }
| { kind: 'dirty' }
/**
* Решение по одному файлу для staging:
* — нечитаемый → dirty (push сам сообщит вменяемую ошибку),
* — без корректного frontmatter type → non-entity (README/notes/CLAUDE/AGENTS и пр. не отправляем),
* — есть в индексе и sha256 совпал → clean,
* — иначе dirty.
*/
async function classifyFileForAdd(
root: string,
rel: string,
index: Awaited<ReturnType<typeof loadIndex>>,
): Promise<FileCheck> {
let raw: string
try {
const raw = await fs.readFile(path.join(root, rel), 'utf8')
return sha256Hex(raw) !== entry.content_etag_local
raw = await fs.readFile(path.join(root, rel), 'utf8')
}
catch {
return true
return { kind: 'unreadable' }
}
if (peekBlagoEntityType(raw) === undefined) {
return { kind: 'non-entity' }
}
const entry = findByRelativePath(index, rel)
if (!entry) {
return { kind: 'dirty' }
}
return sha256Hex(raw) === entry.content_etag_local ? { kind: 'clean' } : { kind: 'dirty' }
}
export interface RunAddResult {
@@ -50,6 +71,8 @@ export interface RunAddResult {
skippedIgnored: number
/** messages/ и meetings/ — только синхронизация с сервера, в push не идут */
skippedPullOnlyArtifacts: number
/** Файлы без корректного blago-frontmatter (README, notes, CLAUDE/AGENTS и пр.) — в staging не попадают */
skippedNonEntity: number
}
export async function runAdd(root: string, targets: string[]): Promise<RunAddResult> {
@@ -65,6 +88,7 @@ export async function runAdd(root: string, targets: string[]): Promise<RunAddRes
let skippedUnchanged = 0
let skippedIgnored = 0
let skippedPullOnlyArtifacts = 0
let skippedNonEntity = 0
for (const t of expanded) {
const abs = path.resolve(root, t)
@@ -86,7 +110,12 @@ export async function runAdd(root: string, targets: string[]): Promise<RunAddRes
skippedPullOnlyArtifacts += 1
continue
}
if (!(await isDirtyVsIndex(root, rel, index))) {
const check = await classifyFileForAdd(root, rel, index)
if (check.kind === 'non-entity') {
skippedNonEntity += 1
continue
}
if (check.kind === 'clean') {
skippedUnchanged += 1
continue
}
@@ -96,5 +125,5 @@ export async function runAdd(root: string, targets: string[]): Promise<RunAddRes
const stagedPaths = [...set].sort()
await saveStaging(root, { paths: stagedPaths })
return { stagedPaths, skippedUnchanged, skippedIgnored, skippedPullOnlyArtifacts }
return { stagedPaths, skippedUnchanged, skippedIgnored, skippedPullOnlyArtifacts, skippedNonEntity }
}
@@ -9,12 +9,15 @@ export interface CommunicationCursorsFile {
messageLastTsByRoom: Record<string, number>
/** project_hash → ISO instant: транскрипции с endedAt ≤ этого момента уже выгружены */
transcriptionLastEndedExclusiveByProject: Record<string, string>
/** matrixRoomId → ISO instant: транскрипции непроектной комнаты с endedAt ≤ этого момента уже выгружены */
transcriptionLastEndedExclusiveByRoom: Record<string, string>
}
function empty(): CommunicationCursorsFile {
return {
messageLastTsByRoom: {},
transcriptionLastEndedExclusiveByProject: {},
transcriptionLastEndedExclusiveByRoom: {},
}
}
@@ -32,6 +35,11 @@ export async function loadCommunicationCursors(root: string): Promise<Communicat
&& typeof parsed.transcriptionLastEndedExclusiveByProject === 'object'
? { ...parsed.transcriptionLastEndedExclusiveByProject }
: {},
transcriptionLastEndedExclusiveByRoom:
parsed.transcriptionLastEndedExclusiveByRoom !== undefined
&& typeof parsed.transcriptionLastEndedExclusiveByRoom === 'object'
? { ...parsed.transcriptionLastEndedExclusiveByRoom }
: {},
}
}
catch {
@@ -43,7 +43,6 @@ export function projectCommunicationDayToMarkdown(
}
export interface TranscriptionHeaderModel {
matrixRoomId: string
roomId: string
startedAt: Date | string
endedAt: Date | string | null | undefined
@@ -71,7 +70,6 @@ export function renderCallTranscriptionMarkdown(
const header = [
`# Транскрипция звонка`,
``,
`- Matrix room: \`${transcription.matrixRoomId}\``,
`- LiveKit room: \`${transcription.roomId}\``,
`- Начало: ${start.toISOString()}`,
`- Окончание: ${end ? end.toISOString() : '—'}`,
@@ -25,7 +25,7 @@ import {
transcriptionMeetingFileStemUtc,
type CommunicationDayLine,
} from './communication-markdown.js'
import { workspaceBasePath, type ProjectPathModel } from './layout.js'
import { generateSlug, workspaceBasePath, type ProjectPathModel } from './layout.js'
import { syncEntityFile } from './sync-entity-file.js'
interface ProjectRowLite {
@@ -183,6 +183,19 @@ async function listRooms(ctx: AuthenticatedContext, projectHash: string) {
return q[Queries.ChatCoop.ListProjectCommunicationRooms.name] ?? []
}
/** Стабильная папка непроектной комнаты в `rooms/`. Системные — фиксированные, комнаты секретаря — slug + хвост id (уникальность). */
function nonProjectRoomFolder(kind: string, matrixRoomId: string, displayLabel: string): string {
if (kind === 'MEMBERS') {
return 'komnata-paishchikov'
}
if (kind === 'COUNCIL') {
return 'komnata-soveta'
}
const slug = generateSlug(displayLabel) || 'komnata'
const shortId = createHash('sha256').update(matrixRoomId, 'utf8').digest('hex').slice(0, 6)
return `${slug}-${shortId}`
}
export async function pullProjectCommunicationArtifacts(
ctx: AuthenticatedContext,
index: IndexFile,
@@ -198,6 +211,7 @@ export async function pullProjectCommunicationArtifacts(
cursors = {
messageLastTsByRoom: {},
transcriptionLastEndedExclusiveByProject: {},
transcriptionLastEndedExclusiveByRoom: {},
}
}
@@ -356,7 +370,6 @@ export async function pullProjectCommunicationArtifacts(
= dateFromUnknown(tr.endedAt) ?? (tr.endedAt as Date | string | null | undefined)
const md = renderCallTranscriptionMarkdown(
{
matrixRoomId: String(tr.matrixRoomId),
roomId: String(tr.roomId),
startedAt,
endedAt: endedAtTr,
@@ -422,3 +435,207 @@ export async function pullProjectCommunicationArtifacts(
warn(`Не удалось сохранить курсоры переписки: ${formatThrownValue(e)}`)
}
}
/**
* Pull переписки и транскрипций из комнат ВНЕ проектов Capital (пайщики, совет, комнаты секретаря).
* Раскладка — отдельная верхняя папка `rooms/<folder>/{messages,meetings}/`, чтобы не смешивать с
* проектными `meetings/`. Логика идентична проектной, но bucket = одна комната, курсор транскрипций — по matrixRoomId.
*/
export async function pullNonProjectCommunicationArtifacts(
ctx: AuthenticatedContext,
index: IndexFile,
): Promise<void> {
let rooms: { matrixRoomId: string, displayLabel: string, kind: string }[]
try {
const q = await ctx.client.Query(Queries.ChatCoop.ListNonProjectCommunicationRooms.query, {})
rooms = (q[Queries.ChatCoop.ListNonProjectCommunicationRooms.name] ?? []) as typeof rooms
}
catch (e) {
warn(`Список непроектных комнат (chatcoopListNonProjectCommunicationRooms): ${formatThrownValue(e)}`)
return
}
if (rooms.length === 0) {
return
}
let cursors: CommunicationCursorsFile
try {
cursors = await loadCommunicationCursors(ctx.root)
}
catch (e) {
warn(`Курсоры переписки (комнаты): не удалось прочитать, начинаем с пустых: ${formatThrownValue(e)}`)
cursors = {
messageLastTsByRoom: {},
transcriptionLastEndedExclusiveByProject: {},
transcriptionLastEndedExclusiveByRoom: {},
}
}
for (const room of rooms) {
const folder = nonProjectRoomFolder(room.kind, room.matrixRoomId, room.displayLabel)
const basePath = `rooms/${folder}`
const roomTitle = room.displayLabel || room.matrixRoomId
// Сообщения комнаты — по календарным суткам UTC новее курсора.
try {
const last = cursors.messageLastTsByRoom[room.matrixRoomId]
const afterTs = last ?? 0
const datesQ = await ctx.client.Query(Queries.ChatCoop.ListUtcDatesWithNewRoomMessages.query, {
variables: { data: { matrixRoomId: room.matrixRoomId, afterOriginServerTsExclusive: afterTs } },
})
const dates = (datesQ[Queries.ChatCoop.ListUtcDatesWithNewRoomMessages.name] ?? []).sort()
for (const utcDate of dates) {
const mq = await ctx.client.Query(Queries.ChatCoop.GetRoomMessagesForUtcDate.query, {
variables: { data: { matrixRoomId: room.matrixRoomId, utcDate } },
})
const linesRaw = mq[Queries.ChatCoop.GetRoomMessagesForUtcDate.name] ?? []
const lines: CommunicationDayLine[] = linesRaw.map(m => ({
originServerTs: m.originServerTs,
authorLabel: m.authorLabel,
coopUsername: m.coopUsername,
kind: String(m.kind),
bodyText: m.bodyText,
}))
if (lines.length === 0) {
continue
}
const content = projectCommunicationDayToMarkdown(roomTitle, room.matrixRoomId, utcDate, [
{ displayLabel: room.displayLabel, matrixRoomId: room.matrixRoomId, lines },
])
const rel = `${basePath}/messages/${utcDate}.md`
const entityHash = messageDayEntityHash(room.matrixRoomId, utcDate)
await syncEntityFile({
root: ctx.root,
index,
entityType: 'room_message_day',
entityHash,
relativePath: rel,
content,
remoteUpdatedAt: `${utcDate}T23:59:59.999Z`,
label: `переписка ${utcDate} (${room.matrixRoomId})`,
})
}
const maxQ = await ctx.client.Query(Queries.ChatCoop.GetMaxOriginServerTsForRoom.query, {
variables: { data: { matrixRoomId: room.matrixRoomId } },
})
const maxTs = maxQ[Queries.ChatCoop.GetMaxOriginServerTsForRoom.name] as number | null | undefined
if (maxTs !== undefined && maxTs !== null && Number.isFinite(maxTs)) {
cursors.messageLastTsByRoom[room.matrixRoomId] = maxTs
}
}
catch (e) {
warn(`Переписка Matrix, комната ${room.matrixRoomId} (${roomTitle}): ${formatThrownValue(e)}`)
}
// Транскрипции звонков комнаты + sibling memo.
try {
const tExIso = cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId]
const lowerBoundExclusive = tExIso === undefined ? new Date(0) : new Date(tExIso)
interface TranscriptionCandidate {
id: string
endedAt: Date
memo: string
updatedAt: Date | undefined
}
const byId = new Map<string, TranscriptionCandidate>()
const tq = await ctx.client.Query(Queries.ChatCoop.GetTranscriptions.query, {
variables: { data: { matrixRoomId: room.matrixRoomId, limit: CHATCOOP_TRANSCRIPTIONS_QUERY_LIMIT, offset: 0 } },
})
const list = tq[Queries.ChatCoop.GetTranscriptions.name] ?? []
for (const t of list) {
const end = dateFromUnknown(t.endedAt)
if (t.status !== Zeus.TranscriptionStatus.COMPLETED || !end) {
continue
}
const prev = byId.get(t.id)
if (!prev || end > prev.endedAt) {
byId.set(t.id, {
id: t.id,
endedAt: end,
memo: typeof t.memo === 'string' ? t.memo : '',
updatedAt: dateFromUnknown(t.updatedAt),
})
}
}
const allCompleted = [...byId.values()].sort((a, b) => a.endedAt.getTime() - b.endedAt.getTime())
const newMeetings = allCompleted.filter(c => c.endedAt.getTime() > lowerBoundExclusive.getTime())
let maxEnded: Date | null = null
for (const c of newMeetings) {
const packQ = await ctx.client.Query(Queries.ChatCoop.GetTranscription.query, {
variables: { data: { id: c.id } },
})
const pack = packQ[Queries.ChatCoop.GetTranscription.name]
if (!pack?.transcription || pack.transcription.status !== Zeus.TranscriptionStatus.COMPLETED) {
continue
}
const tr = pack.transcription
const startedAt: Date | string = dateFromUnknown(tr.startedAt) ?? (tr.startedAt as Date | string)
const endedAtTr: Date | string | null | undefined
= dateFromUnknown(tr.endedAt) ?? (tr.endedAt as Date | string | null | undefined)
const md = renderCallTranscriptionMarkdown(
{
roomId: String(tr.roomId),
startedAt,
endedAt: endedAtTr,
},
pack.segments.map(s => ({
speakerName: s.speakerName,
text: s.text,
startOffset: s.startOffset,
endOffset: s.endOffset,
})),
)
const stem = transcriptionMeetingFileStemUtc(c.endedAt)
const rel = `${basePath}/meetings/${stem}.md`
const entityHash = c.id.toLowerCase()
await syncEntityFile({
root: ctx.root,
index,
entityType: 'call_transcription',
entityHash,
relativePath: rel,
content: md,
remoteUpdatedAt: toUpdatedIso(c.endedAt),
label: `транскрипция ${c.id}`,
})
if (!maxEnded || c.endedAt > maxEnded) {
maxEnded = c.endedAt
}
}
for (const c of allCompleted) {
const stem = transcriptionMeetingFileStemUtc(c.endedAt)
const memoRel = `${basePath}/meetings/${stem}.memo.md`
const memoRemoteUpdatedAt = toUpdatedIso(c.updatedAt ?? c.endedAt)
await syncTranscriptionMemoFile({
root: ctx.root,
index,
transcriptionId: c.id,
relativePath: memoRel,
serverMemo: c.memo,
remoteUpdatedAtIso: memoRemoteUpdatedAt,
})
}
if (maxEnded) {
cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId] = maxEnded.toISOString()
}
else if (tExIso === undefined) {
cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId] = new Date().toISOString()
}
}
catch (e) {
warn(`Транскрипции звонков, комната ${room.matrixRoomId}: ${formatThrownValue(e)}`)
}
}
try {
await saveCommunicationCursors(ctx.root, cursors)
}
catch (e) {
warn(`Не удалось сохранить курсоры переписки (комнаты): ${formatThrownValue(e)}`)
}
}
+3 -1
View File
@@ -28,7 +28,7 @@ import {
storyFileRelativePath,
workspaceBasePath,
} from './layout.js'
import { pullProjectCommunicationArtifacts } from './pull-communication.js'
import { pullNonProjectCommunicationArtifacts, pullProjectCommunicationArtifacts } from './pull-communication.js'
import { scaffoldBmadWorkspacesAfterPull } from './scaffold-bmad-workspace.js'
import { syncEntityFile } from './sync-entity-file.js'
import { writeWorkspaceIndexMarkdown } from './workspace-index.js'
@@ -329,6 +329,8 @@ export async function runPull(ctx: AuthenticatedContext, options: RunPullOptions
await pullProjectCommunicationArtifacts(ctx, index, allProjects, projectByHash)
await pullNonProjectCommunicationArtifacts(ctx, index)
await scaffoldBmadWorkspacesAfterPull(ctx, allProjects, projectByHash)
await saveIndex(ctx.root, index)
+43 -3
View File
@@ -6,7 +6,7 @@ import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { Mutations, Queries } from '@coopenomics/sdk'
import { parseBlagoMarkdown } from '../format/index.js'
import { parseBlagoMarkdown, peekBlagoEntityType } from '../format/index.js'
import { sha256Hex } from '../lib/hash.js'
import { effectiveParentHash } from '../lib/parent-hash.js'
import { warn } from '../ui/output.js'
@@ -58,6 +58,30 @@ export async function runPush(ctx: AuthenticatedContext): Promise<void> {
}
staging = await loadStaging(ctx.root)
}
// Не-сущности (README/notes/CLAUDE/AGENTS и пр., в .md без type=project|issue|story) тоже не должны валить весь push.
// Чистим их автоматически — иначе одна забытая заметка блокирует отправку реальных правок.
const stagingAbs = (p: string): string => path.join(ctx.root, normalizeRelativePath(p))
const nonEntityInStaging: string[] = []
for (const p of staging.paths) {
try {
const raw = await fs.readFile(stagingAbs(p), 'utf8')
if (peekBlagoEntityType(raw) === undefined) {
nonEntityInStaging.push(normalizeRelativePath(p))
}
}
catch {
// нечитаемый файл — пусть основной цикл бросит понятную ошибку с путём
}
}
if (nonEntityInStaging.length > 0) {
const drop = new Set(nonEntityInStaging)
const kept = staging.paths.filter(p => !drop.has(normalizeRelativePath(p)))
await saveStaging(ctx.root, { paths: [...new Set(kept.map(p => normalizeRelativePath(p)))].sort() })
for (const p of nonEntityInStaging) {
warn(`Убрано из staging (нет blago-frontmatter type=project|issue|story): ${p}`)
}
staging = await loadStaging(ctx.root)
}
if (staging.paths.length === 0) {
throw new Error('Нечего отправлять. Добавьте файлы: blago add <путь | id проекта | projectId-issueId>')
}
@@ -75,8 +99,24 @@ export async function runPush(ctx: AuthenticatedContext): Promise<void> {
}
const abs = path.join(ctx.root, n)
const raw = await fs.readFile(abs, 'utf8')
const parsed = parseBlagoMarkdown(raw)
const { type, hash } = validateParsedForPush(parsed)
let parsed: ReturnType<typeof parseBlagoMarkdown>
try {
parsed = parseBlagoMarkdown(raw)
}
catch (err) {
// Без префикса путём из staging вылезает «type: undefined» без указания файла.
const msg = err instanceof Error ? err.message : String(err)
throw new Error(`Файл «${n}»: ${msg}`)
}
let type: ReturnType<typeof validateParsedForPush>['type']
let hash: string
try {
({ type, hash } = validateParsedForPush(parsed))
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err)
throw new Error(`Файл «${n}»: ${msg}`)
}
const entry = findByHash(index, type, hash)
const pKind = pendingKindForEntityType(type)
const pending = pKind ? await findPendingForParsed(ctx.root, pKind, hash) : undefined
-2
View File
@@ -367,7 +367,6 @@ export async function runRestore(ctx: AuthenticatedContext, userPath: string): P
if (entry.entity_type === 'call_transcription') {
interface TranscriptionRestorePack {
transcription: {
matrixRoomId: string
roomId: string
startedAt: Date | string
endedAt: Date | string | null | undefined
@@ -394,7 +393,6 @@ export async function runRestore(ctx: AuthenticatedContext, userPath: string): P
}
const content = renderCallTranscriptionMarkdown(
{
matrixRoomId: tr.matrixRoomId,
roomId: tr.roomId,
startedAt: tr.startedAt,
endedAt: tr.endedAt,
+8 -1
View File
@@ -35,7 +35,14 @@ RUN npm install -g pnpm@10.33.0
COPY . .
RUN pnpm install --frozen-lockfile --filter "@coopenomics/boot..."
# --ignore-scripts + точечный rebuild: --filter ограничивает только то, какие
# проекты получают node_modules, но НЕ то, для каких пакетов общего lockfile
# store выполняет lifecycle-скрипты при --frozen-lockfile. Без --ignore-scripts
# сюда тянутся electron/puppeteer (из @coopenomics/desktop/notifications,
# boot их не использует) — тяжёлые сетевые загрузки, изредка валящие билд по
# TLS-таймауту. rebuild --filter уже честно скопирован на подграф boot.
RUN pnpm install --frozen-lockfile --filter "@coopenomics/boot..." --ignore-scripts \
&& pnpm rebuild --filter "@coopenomics/boot..."
# Билд графа: cooptypes/factory собирают свои dist/, boot собирает
# свой bundle с inlined workspace-deps.
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/boot",
"type": "module",
"version": "2026.5.23-4",
"version": "2026.7.21-1",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "CLI-утилита инициализации блокчейна и кооператива",
@@ -26,6 +26,7 @@
"boot:remote": "esno src/index.ts boot:remote",
"reboot:clean": "cd components/boot/scripts && bash clean_reboot.sh",
"reboot:extra": "cd components/boot/scripts && bash extra_reboot.sh",
"reboot:blago": "cd components/boot/scripts && bash blago_reboot.sh",
"create-coop": "esno src/index.ts create-coop",
"clear": "./scripts/clear.sh",
"start": "./scripts/restart.sh",
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
# Полный перезапуск dev-среды для теста Благороста одной командой:
# 1) extra_reboot.sh — чистая цепь + кооператив + совет 5 человек +
# dev-shortcut онбординга capital (extensions.capital: все *_done=true);
# 2) ожидание готовности controller (GraphQL отвечает);
# 3) seed-capital до фазы $SEED_UP_TO (default 04-contributor): программы
# УХД/Благорост + setconfig, проекты, регистрация ant как участника.
#
# Глубину сида можно менять: SEED_UP_TO=08-investments pnpm run reboot:blago
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
# shellcheck disable=SC1091
source "$ROOT_DIR/.env"
set +a
fi
bash "$SCRIPT_DIR/extra_reboot.sh"
# Per-instance адрес controller'а — из корневого .env (API_URL); coopback после
# force-recreate поднимается десятки секунд, seed-фазы без него падают.
GRAPHQL_URL="${API_URL:-http://127.0.0.1:2998/v1/graphql}"
echo "Ждём готовности controller ($GRAPHQL_URL)..."
until curl -s --max-time 3 -o /dev/null -X POST -H 'Content-Type: application/json' \
--data '{"query":"{__typename}"}' "$GRAPHQL_URL"; do
sleep 3
done
echo "Controller готов."
UP_TO="${SEED_UP_TO:-04-contributor}"
echo "Сидируем capital до фазы $UP_TO..."
cd "$SCRIPT_DIR/.."
CONTROLLER_GRAPHQL_URL="$GRAPHQL_URL" pnpm exec esno src/scripts/seed-capital/index.ts --up-to="$UP_TO"
echo "Готово: среда Благорост поднята (seed до $UP_TO)."
+22 -13
View File
@@ -1,6 +1,7 @@
#!/bin/bash
# Загружаем per-instance конфиг из корня репо.
# Загружаем per-instance конфиг из корня репо (для CHAIN_URL/MONGODB_URL/API_URL,
# которые читают TS-код boot и шелл-скрипты networks.sh/preactivate.sh).
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
@@ -9,26 +10,34 @@ if [ -f "$ROOT_DIR/.env" ]; then
set +a
fi
# Останавливаем и удаляем контейнеры вместе с volumes
echo "Останавливаем и удаляем контейнеры с volumes..."
docker compose down -v mongo postgres cooparser || true
# Останавливаем и удаляем контейнеры (сеть НЕ трогаем — к ней может быть
# подключён внешний контейнер вроде provider-backend; down её снёс бы с ошибкой).
echo "Останавливаем и удаляем контейнеры..."
docker compose rm -fsv mongo postgres monoredis cooparser coopback || true
# Останавливаем blockchain контейнер перед удалением данных
echo "Останавливаем blockchain контейнер..."
docker compose stop node || true
# Удаляем blockchain data
# Удаляем тома баз данных для чистого старта (named volumes rm -v не чистит)
echo "Удаляем тома баз данных..."
PROJECT="${COMPOSE_PROJECT_NAME:-$(basename "$ROOT_DIR")}"
docker volume rm "${PROJECT}_postgres_data" "${PROJECT}_mongo_data" 2>/dev/null || true
# Удаляем blockchain data через контейнер (alpine под root стирает данные
# независимо от владельца — без sudo на любой ноде; на проде nodeos пишет под root).
echo "Удаляем blockchain data..."
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
sudo rm -rf ../blockchain-data/
docker run --rm -v "$(cd .. && pwd)/blockchain-data:/d" alpine sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true'
# Пересоздаем и запускаем базы данных
# Пересоздаем и запускаем базы данных + Redis (monoredis).
# monoredis ОБЯЗАТЕЛЕН: без него coopback падает на старте с
# `getaddrinfo EAI_AGAIN monoredis` → MaxRetriesPerRequestError → nodemon crash.
echo "Пересоздаем и запускаем базы данных..."
docker compose up -d mongo postgres
docker compose up -d mongo postgres monoredis
# Ждем готовности MongoDB
# Ждем готовности MongoDB (standalone, ping вместо ожидания PRIMARY).
echo "Ждем готовности MongoDB..."
until docker compose exec -T mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
until docker compose exec -T mongo mongosh --quiet --eval "db.adminCommand({ping:1}).ok" > /dev/null 2>&1; do
echo "MongoDB еще не готов, ждем..."
sleep 2
done
@@ -42,7 +51,7 @@ until docker compose exec -T postgres pg_isready -U postgres -d voskhod > /dev/n
done
echo "PostgreSQL готов!"
# Запускаем boot процесс
# Запускаем boot процесс (clean: только программы Благорост/маркетплейс)
echo "Запускаем boot процесс..."
pnpm run boot:clean
@@ -51,6 +60,6 @@ echo "Запускаем parser..."
docker compose up -d cooparser
echo "Запускаем контроллер..."
docker compose restart coopback || true
docker compose up -d --force-recreate coopback || true
echo "Перезапуск завершен!"
+17 -14
View File
@@ -1,6 +1,7 @@
#!/bin/bash
# Загружаем per-instance конфиг из корня репо.
# Загружаем per-instance конфиг из корня репо (для CHAIN_URL/MONGODB_URL/API_URL,
# которые читают TS-код boot и шелл-скрипты networks.sh/preactivate.sh).
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
@@ -9,30 +10,32 @@ if [ -f "$ROOT_DIR/.env" ]; then
set +a
fi
# Останавливаем контроллер перед очисткой данных
echo "Останавливаем контроллер..."
docker compose down coopback || true
# Останавливаем и удаляем контейнеры вместе с volumes
echo "Останавливаем и удаляем контейнеры с volumes..."
docker compose down -v mongo postgres cooparser || true
docker compose down -v mongo postgres monoredis cooparser coopback || true
# Останавливаем blockchain контейнер перед удалением данных
echo "Останавливаем blockchain контейнер..."
docker compose stop node || true
# Удаляем blockchain data
# Удаляем blockchain data.
# Контейнерный wipe (alpine под root) стирает данные независимо от их владельца —
# без sudo на любой ноде (на Pi нет passwordless sudo; на проде nodeos пишет
# данные под root). Единый способ с reboot.sh / clean_reboot.sh.
echo "Удаляем blockchain data..."
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
sudo rm -rf ../blockchain-data/
docker run --rm -v "$(cd .. && pwd)/blockchain-data:/d" alpine sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true'
# Пересоздаем и запускаем базы данных
# Пересоздаем и запускаем базы данных + Redis (monoredis).
# monoredis ОБЯЗАТЕЛЕН: без него coopback падает на старте с
# `getaddrinfo EAI_AGAIN monoredis` → MaxRetriesPerRequestError → nodemon crash,
# и провайдер не может взять org-данные partner1 (PROVIDER_URL=coopback:2998).
echo "Пересоздаем и запускаем базы данных..."
docker compose up -d mongo postgres
docker compose up -d mongo postgres monoredis
# Ждем готовности MongoDB
# Ждем готовности MongoDB (standalone, ping вместо ожидания PRIMARY).
echo "Ждем готовности MongoDB..."
until docker compose exec -T mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
until docker compose exec -T mongo mongosh --quiet --eval "db.adminCommand({ping:1}).ok" > /dev/null 2>&1; do
echo "MongoDB еще не готов, ждем..."
sleep 2
done
@@ -46,7 +49,7 @@ until docker compose exec -T postgres pg_isready -U postgres -d voskhod > /dev/n
done
echo "PostgreSQL готов!"
# Запускаем boot процесс
# Запускаем boot процесс (расширенный: совет + пайщики; partner1 — при EXTRA_RENT=1)
echo "Запускаем boot процесс..."
pnpm run boot:extra
@@ -55,6 +58,6 @@ echo "Запускаем parser..."
docker compose up -d cooparser
echo "Запускаем контроллер..."
docker compose up -d coopback
docker compose up -d --force-recreate coopback || true
echo "Перезапуск завершен!"
+37 -22
View File
@@ -291,31 +291,46 @@ export default class Blockchain {
async activateFeature(feature: Feature) {
await this.update_pass_instance()
await this.api.transact(
{
actions: [
{
account: 'eosio',
name: 'activate',
authorization: [
{
actor: 'eosio',
permission: 'active',
try {
await this.api.transact(
{
actions: [
{
account: 'eosio',
name: 'activate',
authorization: [
{
actor: 'eosio',
permission: 'active',
},
],
data: {
feature_digest: feature.hash,
},
],
data: {
feature_digest: feature.hash,
},
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
console.log('Фича активирована: ', feature.name)
console.log('Фича активирована: ', feature.name)
}
catch (e: any) {
// На уже забутстрапленном (не обнулённом) чейне протокол-фича уже активна —
// nodeos отдаёт protocol_feature_exception (code 3250000). Это не повод
// ронять весь boot: фича на месте, нужный результат достигнут, продолжаем.
const errName = e?.json?.error?.name
const errCode = e?.json?.error?.code
const msg = e instanceof Error ? e.message : String(e)
if (errName === 'protocol_feature_exception' || errCode === 3250000 || /already activated/i.test(msg)) {
console.warn(`Фича уже активирована, пропускаю: ${feature.name}`)
return
}
throw e
}
}
async createToken(params: TokenContract.Interfaces.ICreate) {
+5
View File
@@ -128,4 +128,9 @@ export default [
path: path.join(userBase, 'apps'),
target: 'apps',
},
{
name: 'expense',
path: path.join(userBase, 'expense'),
target: 'expense',
},
]
+4
View File
@@ -154,6 +154,10 @@ export default {
name: 'apps',
code_permissions_to: ['apps'],
},
{
name: 'expense',
code_permissions_to: ['expense'],
},
// {
// name: provider_chairman,
// },
@@ -0,0 +1,11 @@
{
"protocol_feature_type": "builtin",
"dependencies": [],
"description_digest": "2ad6b7aabba8edbc87b46f3340df4d0db0e4b65474db3e1cd5db33fb685a506b",
"subjective_restrictions": {
"earliest_allowed_activation_time": "1970-01-01T00:00:00.000",
"preactivation_required": true,
"enabled": true
},
"builtin_feature_codename": "ASSERT_RECOVER_KEY_ACCOUNT"
}
@@ -0,0 +1,33 @@
{
"protocol_feature_type": "builtin",
"dependencies": [
"09e86cb0accf8d81c9e85d34bea4b925ae936626d00c984e4691186891f5bc16",
"1a99a59d87e06e09ec5b028a9cbb7749b4a5ad8819004365d02dc4379a8b7241",
"2652f5f96006294109b3dd0bbde63693f55324af452b799ee137a81a905eed25",
"299dcb6af692324b899b39f16d5a530a33062804e41f09dc97e9f156b4476707",
"35c2186cc36f7bb4aeaf4487b36e57039ccf45a9136aa856a5d569ecca55ef2b",
"4a90c00d55454dc5b059055ca213579c6ea856967712a56017487886a4d4cc0f",
"4e7bf348da00a945489b2a681749eb56f5de00b900014e137ddae39f48f69d67",
"4fca8bd82bbd181e714e283f83e1b45d95ca5af40fb89ad3977b653c448f78c2",
"5443fcf88330c586bc0e5f3dee10e7f63c76c00249c87fe4fbf7f38c082006b4",
"63320dd4a58212e4d32d1f58926b73ca33a247326c2a5e9fd39268d2384e011a",
"68dcaa34c0517d19666e6b33add67351d8c5f69e999ca1e37931bc410a297428",
"6bcb40a24e49c26d0a60513b6aeb8551d264e4717f306b81a37a5afb3b47cedc",
"8ba52fe7a3956c5cd3a656a3174b931d3bb2abb45578befc59f283ecd816a405",
"ad9e3d8f650687709fd68f4b90b41f7d825a365b02c23a636cef88ac2ac00c43",
"bcd2a26394b36614fd4894241d3c451ab0f6fd110958c3423073621a70826e99",
"c3a6138c5061cf291310887c0b5c71fcaffeab90d5deb50d3b9e687cead45071",
"d528b9f6e9693f45ed277af93474fd473ce7d831dae2180cca35d907bd10cb40",
"e0fb64b1085cc5538970158d05a009c24e276fb94e1a0bf6a528b48fbc4ff526",
"ef43112c6543b88db2283a2e077278c315ae2c84719a8b25f25cc88565fbea99",
"f0af56d2c5a48d60a4a5b5c903edfb7db3a736a94ed589d0b797df33ff9d3e1d",
"fce57d2331667353a0eac6b4209b67b843a7262a848af0a49a6e2fa9f6584eb4"
],
"description_digest": "01a8bebb4922c38293a8909668f7d00ba4a21a455c69719ad7dcc33ea1e631c0",
"subjective_restrictions": {
"earliest_allowed_activation_time": "1970-01-01T00:00:00.000",
"preactivation_required": true,
"enabled": true
},
"builtin_feature_codename": "SAVANNA"
}
+19 -9
View File
@@ -1,7 +1,6 @@
import config from '../configs'
import { initExtensionsInPostgres, initSystemStatus } from '../postgres-init'
import { installExtraData, installInitialData, startInfra } from './infra'
import { CooperativeClass, startCoop } from './cooperative'
import { startCoop } from './cooperative'
export async function boot() {
const blockchain = await startInfra()
@@ -14,18 +13,29 @@ export async function boot() {
}
export async function bootClean() {
const blockchain = await startInfra()
console.log('Создаём программы (Благорост и маркетплейс)')
const cooperative = new CooperativeClass(blockchain)
await cooperative.createPrograms(config.provider)
// Только инфраструктура: контракты, фичи, токен, системные параметры.
// Ни совета, ни программ — программы (createPrograms) требуют существующий
// совет и создаются в boot/bootExtra внутри installInitialData. В clean их
// не делаем (иначе soviet::createprog падает с «Совет не найден»).
await startInfra()
}
export async function bootExtra() {
const blockchain = await startInfra()
await installInitialData(blockchain, true) // Создать расширенный совет (устанавливает статус 'active' в MongoDB)
await installExtraData(blockchain) // Добавить дополнительных пайщиков
// installExtraData регистрирует partner1 как coop с auto-approve от провайдера —
// это и есть программная on-chain активация, которая триггерит аренду VM в
// провайдере (PENDING→RENT). boot:extra используется НЕ только для аренды
// сервера (например, просто пересев совета/чейна для других задач), поэтому
// провижининг partner1 включается ТОЛЬКО под флагом EXTRA_RENT=1.
if (process.env.EXTRA_RENT === '1') {
console.log('EXTRA_RENT=1 → installExtraData: провижининг partner1 (триггер аренды)')
await installExtraData(blockchain) // Регистрируем partner1 как coop (active)
}
else {
console.log('EXTRA_RENT не задан → пропускаем провижининг partner1 (аренда не запускается)')
}
console.log('Инициализируем статус системы в PostgreSQL')
await initSystemStatus() // Устанавливает статус 'active' в PostgreSQL
+1 -1
View File
@@ -76,7 +76,7 @@ export class CooperativeClass {
await this.blockchain.createProgram({
coopname,
username: coopname,
type: 'blagorost',
type: 'capital',
title: 'Целевая потребительская программа "Благорост"',
announce: '',
description: '',
+14 -4
View File
@@ -15,6 +15,9 @@ import { sleep } from '../utils'
import { generateRandomSHA256 } from '../utils/randomHash'
import { initUsersInPostgres, initVaultInPostgres } from '../postgres-init'
import { CooperativeClass } from './cooperative'
import { signProgramAgreement } from './sign-program-agreement'
import { fakeDocument } from '../tests/shared/fakeDocument'
import { walletDraftId, walletProgramId } from '../tests/capital/consts'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
@@ -674,10 +677,17 @@ export async function installExtraData(blockchain: Blockchain) {
status: 'active',
})
// Ресурсы для партнёра: powerup CPU/NET. transfer токенов пропускаем —
// eosio.token::transfer падает на проверке membership в wallet program,
// которая для partner1 не настроена. Для provider sync + Hostkey-flow
// токены не нужны: tx от имени partner1 пойдут от soviet/admin.
// Записываем partner1 в ЦПП Кошелька оператора (voskhod, program_id=1).
// БЕЗ этого провайдерский performInitialTransfers (150 AXON на partner1 ДО
// перехода в ACTIVE, см. provider CLAUDE.md §13) падает ассертом
// eosio.token::is_can_transfer «Получатель не является участником целевой
// потребительской программы кошелька» — и инстанс навсегда застревает в
// INSTALL. wallet::signagree (auth voskhod@active) делает partner1 членом
// ЦПП Кошелька. Та же механика, что для обычных пайщиков в participant.ts.
console.log('Подписываем wallet-соглашение за partner1 (членство в ЦПП Кошелька)')
await signProgramAgreement(blockchain, config.provider, account.username, walletProgramId, walletDraftId, fakeDocument)
// Ресурсы для партнёра: powerup CPU/NET.
await blockchain.powerup({
payer: 'eosio',
receiver: account.username,
@@ -1,6 +1,6 @@
/**
* Фаза 01 программы УХД (id=3, type='generator') и Благорост
* (id=4, type='blagorost') + capital::setconfig.
* (id=4, type='capital') + capital::setconfig.
*
* Идемпотентно: программы создаются только если их ещё нет в `programs`-таблице,
* setconfig только если `state` пуст.
@@ -88,7 +88,7 @@ export async function phase01(): Promise<void> {
fixed_membership_contribution: '0.0000 RUB',
membership_percent_fee: '0',
meta: '',
type: 'blagorost',
type: 'capital',
}
await blockchain.api.transact({
actions: [{
@@ -300,8 +300,8 @@ async function ensureAgreementsAndWallet(blockchain: Blockchain, fix: IFixture):
}
// Согласие с ЦПП «Благорост» — для инвестиций в программу/компонент.
// Блокировка идёт по своему пути; для безопасности дёргаем при первом запуске.
await signAgreement(blockchain, fix.username, 'blagorost').catch((e) => {
log(`signAgreement blagorost для ${fix.username} — пропуск (${(e as Error).message?.slice(0, 80)})`)
await signAgreement(blockchain, fix.username, 'capital').catch((e) => {
log(`signAgreement capital для ${fix.username} — пропуск (${(e as Error).message?.slice(0, 80)})`)
})
}
@@ -234,7 +234,7 @@ describe('тест контракта CAPITAL', () => {
fixed_membership_contribution: '0.0000 RUB',
membership_percent_fee: '0',
meta: '',
type: 'blagorost',
type: 'capital',
}
const result = await blockchain.api.transact(
+1 -1
View File
@@ -213,7 +213,7 @@ describe('тест контракта CAPITAL', () => {
fixed_membership_contribution: '0.0000 RUB',
membership_percent_fee: '0',
meta: '',
type: 'blagorost',
type: 'capital',
}
const result = await blockchain.api.transact(
+1 -1
View File
@@ -12,6 +12,6 @@ export const capitalDraftId = 1000
export const ratePerHour = '1000.0000 RUB'
export const sourceProgramName = 'generator'
export const capitalProgramName = 'blagorost'
export const capitalProgramName = 'capital'
// ledger2::accounts.id хранит счёт со смещением *1000 (80 → 80000).
export const circulationAccountId = 80_000
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/cleos",
"version": "2026.5.23-4",
"version": "2026.7.21-1",
"private": true,
"description": "Обёртка над кошельком cleos для EOSIO блокчейна",
"scripts": {
+1
View File
@@ -58,6 +58,7 @@ add_contract_build(registrator)
add_contract_build(gateway)
add_contract_build(marketplace)
add_contract_build(apps)
add_contract_build(expense)
add_contract_build(system)
# Опции для тестов
+5 -7
View File
@@ -1,15 +1,13 @@
#!/usr/bin/env bash
# Локальная сборка всех контрактов: оборачивает build_contracts_cdt.sh в
# CDT-образ. Сам cmake/make живёт в build_contracts_cdt.sh — он же
# используется в CI (там CDT ставится из .deb, без docker), поэтому
# флаги сборки в одном месте и не разъезжаются.
mode="${1:-prod}"
if [ "$mode" = "test" ]; then
is_testnet="ON"
else
is_testnet="OFF"
fi
docker run --rm --name cdt \
--volume "$(pwd):/project" \
-w /project \
dicoop/blockchain:latest \
/bin/bash -c "mkdir -p build && cd build && cmake -DBUILD_TARGET= -DTEST_TARGET= -DVERBOSE=ON -DBUILD_TESTS=OFF -DIS_TESTNET=$is_testnet .. && make"
/bin/bash -c "./build_contracts_cdt.sh $mode"
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Компиляция всех контрактов кооперативной экономики через CDT.
#
# ВАЖНО: скрипт запускается УЖЕ внутри окружения с установленным CDT —
# тулчейн ожидается по пути /cdt/build/lib/cmake/cdt/CDTWasmToolchain.cmake
# (он захардкожен в CMakeLists.txt). Здесь НЕТ docker — только cmake/make.
#
# Два способа подготовить это окружение:
# - локально: build-all.sh оборачивает скрипт в `docker run dicoop/blockchain:latest`
# (в образе CDT уже лежит в /cdt/build);
# - в CI: CDT ставится из .deb (C9S/cdt), а /cdt/build симлинкуется на
# /usr/opt/cdt/<ver> (см. .github/workflows/release.yaml).
#
# Один источник cmake-флагов для обоих путей — чтобы локальная и CI-сборка
# не разъезжались.
set -euo pipefail
mode="${1:-prod}"
if [ "$mode" = "test" ]; then
is_testnet="ON"
else
is_testnet="OFF"
fi
mkdir -p build
cd build
cmake -DBUILD_TARGET= -DTEST_TARGET= -DVERBOSE=ON -DBUILD_TESTS=OFF -DIS_TESTNET="$is_testnet" ..
make
@@ -0,0 +1,65 @@
/**
* @brief Инициировать программный расход через шасси expense.
*
* capital только инициатор. Резервирует средства в @c program_expense_pool,
* создаёт скелет записи progexpense (status=CREATED, как маркер pending), затем
* шлёт inline action `expense::createexp(...)` с callback handler `{capital, onpgexpdone}`.
* Шасси expense обслуживает весь дальнейший flow (авторизация совета, выдача аванса,
* отчёт, закрытие либо отклонение) и на терминальном переходе шлёт inline `onpgexpdone`.
*
* @param coopname Кооператив
* @param expense_hash Хеш расхода (= proposal_hash в шасси)
* @param creator Инициатор расхода
* @param items Позиции СЗ (см. ExpenseDomain::item); механика оплаты per-item
* @param description Описание для отчётности
* @param statement Документ-заявление (ProgramExpenseStatement)
*
* @ingroup public_actions
* @ingroup public_capital_actions
*
* @note Авторизация требуется от @p coopname.
*/
void capital::createpgexp(name coopname, checksum256 expense_hash, name creator,
std::vector<ExpenseDomain::item> items,
std::string description, document2 statement) {
require_auth(coopname);
verify_document_or_fail(statement, {creator});
eosio::check(!items.empty(), "Программный расход должен содержать хотя бы один item");
// Сумма расхода — сумма planned_amount всех items.
asset amount = items.front().planned_amount;
amount.amount = 0;
for (const auto &it : items) {
amount += it.planned_amount;
}
Wallet::validate_asset(amount);
eosio::check(amount.amount > 0, "Сумма программного расхода должна быть положительной");
auto exist = Capital::get_program_expense(coopname, expense_hash);
eosio::check(!exist.has_value(), "Программный расход с указанным хэшем уже существует");
// Резерв в пуле до отправки в шасси — две параллельные заявки не превысят пул.
Capital::State::reserve_program_expense(coopname, amount);
Capital::Expenses::create_program_expense(coopname, expense_hash, creator,
amount, description, statement);
// Inline action в шасси expense с callback handler на capital::onpgexpdone.
// data пустой — capital резолвит запись по expense_hash, дополнительный payload не нужен.
ExpenseDomain::callback_handler callback{
.contract = _capital,
.action = Names::Capital::ON_PROGRAM_EXPENSE_DONE,
.data = std::vector<char>{}
};
// Authority — _capital: у capital@eosio.code нет coopname@active, а
// expense::createexp принимает контракты-инициаторы по contracts_whitelist.
eosio::action(
eosio::permission_level{_capital, "active"_n},
_expense,
Names::External::CREATE_EXPENSE_PROPOSAL,
std::make_tuple(coopname, creator, expense_hash,
ledger2_wallets::PROGRAM_EXPENSE_POOL, items, callback, statement)
).send();
}
@@ -0,0 +1,65 @@
/**
* @brief Callback от шасси expense на финализацию program-расхода.
*
* Шасси expense шлёт его inline (authority _expense@active) когда закончило
* flow (closeexp или declexp). capital не делает ledger2-проводок
* они уже сделаны шасси на payexp/reportexp/returnexp/overspendexp. capital
* только обновляет @c program_expense_pool и удаляет запись из @c progexpenses.
*
* status == ExpenseDomain::ProposalStatus::CLOSED (4):
* total_actual <= reserved consume(total_actual) + release(остаток);
* total_actual > reserved (был overspendexp) consume(reserved) +
* прямое списание превышения из program_expense_pool.
* status == ExpenseDomain::ProposalStatus::DECLINED (5):
* release(reserved); decline возможен только до оплат (total_actual == 0).
*
* @param coopname Кооператив
* @param expense_hash Хеш расхода (ключ в progexpenses)
* @param status ExpenseDomain::ProposalStatus
* @param total_actual Фактически потраченная сумма
* @param data Произвольный payload от инициатора (не используется на MVP)
*
* @ingroup public_actions
* @ingroup public_capital_actions
*
* @note Авторизация требуется от @p _expense inline-callback шасси.
*/
void capital::onpgexpdone(name coopname, checksum256 expense_hash, uint8_t status,
eosio::asset total_actual, std::vector<char> data) {
require_auth(_expense);
auto pgexp = Capital::Expenses::get_program_expense_or_fail(coopname, expense_hash);
const auto reserved = pgexp.amount;
if (status == static_cast<uint8_t>(ExpenseDomain::ProposalStatus::CLOSED)) {
eosio::check(total_actual.symbol == reserved.symbol,
"Символ total_actual не совпадает с зарезервированной суммой");
eosio::check(total_actual.amount >= 0,
"total_actual не может быть отрицательным");
if (total_actual.amount <= reserved.amount) {
if (total_actual.amount > 0) {
Capital::State::consume_program_expense(coopname, total_actual);
}
const asset leftover = reserved - total_actual;
if (leftover.amount > 0) {
Capital::State::release_program_expense(coopname, leftover);
}
} else {
// Перерасход (overspendexp в шасси): резерв списываем полностью,
// превышение — напрямую из свободного пула программных расходов.
Capital::State::consume_program_expense(coopname, reserved);
const asset excess = total_actual - reserved;
Capital::State::spend_program_expense_pool(coopname, excess);
}
} else if (status == static_cast<uint8_t>(ExpenseDomain::ProposalStatus::DECLINED)) {
// Шасси отклоняет СЗ только до первой оплаты — резерв возвращается целиком.
eosio::check(total_actual.amount == 0,
"DECLINED с ненулевым total_actual — нарушение инварианта шасси");
Capital::State::release_program_expense(coopname, reserved);
} else {
eosio::check(false, "onpgexpdone принимает только статусы CLOSED либо DECLINED");
}
Capital::Expenses::delete_program_expense(coopname, pgexp.id);
}
@@ -0,0 +1,33 @@
/**
* @brief Пополняет пул программных расходов из общего инвестиционного пула программы.
*
* Программные расходы это целевые списания «Благорост», не аллоцированные ни в один проект
* (хозяйственные расходы программы, оплата инфраструктуры программы и т.п.). Прежде чем
* создавать запрос на программный расход, председатель пополняет дедикейтнутый пул
* @c program_expense_pool из общего @c global_available_invest_pool. Помимо счётчиков
* state наполняется КООПЕРАТИВНЫЙ ledger2-кошелёк @c PROGRAM_EXPENSE_POOL (w.cap.pgexp)
* именно из него шасси expense оплачивает СЗ; личные L3-кошельки пайщиков (w.cap.blago)
* при расходах не изменяются.
*
* @param coopname Кооператив (scope state)
* @param amount Сумма пополнения (символ строго совпадает с программой)
* @ingroup public_actions
* @ingroup public_capital_actions
*
* @note Авторизация требуется от аккаунта: @p coopname (председатель).
*/
void capital::topupprogexp(name coopname, eosio::asset amount) {
require_auth(coopname);
Capital::State::topup_program_expense_pool(coopname, amount);
// Кошелёк-резерв пула: ISSUE w.cap.pgexp без бухпроводки (деньги физически
// на 51 с момента взносов — выделяется только назначение под расходы).
// Синтетический process_hash — у пополнения нет естественного анкера.
const uint64_t now_us = eosio::current_time_point().time_since_epoch().count();
const std::string anchor = coopname.to_string() + ":pgtopup:" + std::to_string(now_us);
const checksum256 process_hash = eosio::sha256(anchor.c_str(), anchor.size());
Ledger2::apply(_capital, coopname, operations::capital::PROGRAM_EXPENSE_TOPUP, amount,
name{}, process_hash, "capital:topupprogexp");
}
@@ -72,8 +72,8 @@ void capital::importcontrib(eosio::name coopname, eosio::name username, checksum
auto idx = user_wallets.get_index<"byuserwallet"_n>();
auto preimp_it = idx.find(combine_ids(ledger2_wallets::PREIMP_FUND.value, username.value));
if (preimp_it != idx.end() && preimp_it->available.amount > 0) {
eosio::check(preimp_it->blocked.amount == 0,
"preimp.blocked > 0 не поддерживается при импорте — обратиться в поддержку");
// Поле blocked упразднено (2026-05-24): резерв/блокировка на кошельках
// больше не используется, проверка preimp.blocked == 0 излишня.
Ledger2::apply(
_capital, coopname, operations::capital::DROP_PREIMP,
preimp_it->available, username, contributor_hash,
@@ -95,6 +95,12 @@
#include "app/debt_managment/create_debt/declinedebt.cpp"
#include "app/debt_managment/settle_debt/settledebt.cpp"
// Управление программными расходами через шасси expense.
// capital — инициатор + получатель callback, шасси обслуживает весь flow.
#include "app/expense_managment/program_expenses/topupprogexp.cpp"
#include "app/expense_managment/program_expenses/createpgexp.cpp"
#include "app/expense_managment/program_expenses/onpgexpdone.cpp"
// Управление расходами
#include "app/expense_managment/create_expense/approveexpns.cpp"
#include "app/expense_managment/create_expense/capauthexpns.cpp"
@@ -48,6 +48,7 @@ constexpr uint128_t CRPS_PRECISION_FACTOR = 100000000000000ULL; ///< Масшт
#include <eosio/asset.hpp>
#include "../lib/index.hpp"
#include "domain/index.hpp"
#include "../expense/expense.hpp" // ExpenseDomain::item / callback_handler — для inline-action в шасси
using namespace eosio;
@@ -322,4 +323,38 @@ public:
[[eosio::action]]
void settledebt(name coopname, name username, eosio::asset amount, document2 statement);
// Программные расходы через шасси expense.
// capital — инициатор + получатель callback, шасси держит весь flow расхода.
[[eosio::action]]
void topupprogexp(name coopname, eosio::asset amount);
/**
* @brief Инициировать программный расход через шасси expense.
* Резервирует amount в program_expense_pool, создаёт запись progexpense,
* шлёт inline action expense::createexp с callback={capital, onpgexpdone}.
* Шасси обслуживает весь flow (auth/pay/report/close|decline) и шлёт callback.
*
* items[] позиции СЗ (см. ExpenseDomain::item); механика оплаты per-item
* (item.mechanics: ADVANCE аванс, DIRECT прямая оплата).
*/
[[eosio::action]]
void createpgexp(name coopname, checksum256 expense_hash, name creator,
std::vector<ExpenseDomain::item> items,
std::string description, document2 statement);
/**
* @brief Callback от шасси expense на финализацию program-расхода.
* Шасси шлёт его inline (authority _expense@active, require_auth(_expense))
* после терминального перехода flow (CLOSED либо DECLINED).
*
* status == CLOSED consume(total_actual) + release(остаток резерва);
* при total_actual > reserved (перерасход) превышение
* списывается напрямую из program_expense_pool.
* status == DECLINED release(reserved); decline возможен только до оплат.
*/
[[eosio::action]]
void onpgexpdone(name coopname, checksum256 expense_hash, uint8_t status,
eosio::asset total_actual, std::vector<char> data);
};
@@ -9,23 +9,26 @@ namespace Capital::Core {
* @brief Совокупный баланс программы Благорост на уровне кооператива.
*
* Источник ledger2 L2 (`wallets[BLAGOROST_FUND]`), агрегированный по всем
* пайщикам. Σ L3.{available, blocked}[w.cap.blago] == L2.{available, blocked}
* (Эпик 3 invariant), поэтому L2 авторитетная сумма для CRPS-знаменателя.
* Если кошелёк ещё не создан (никто не инвестировал) нулевой баланс.
* пайщикам. Σ L3.available[w.cap.blago] == L2.available (Эпик 3 invariant),
* поэтому L2 авторитетная сумма для CRPS-знаменателя. Если кошелёк ещё не
* создан (никто не инвестировал) нулевой баланс.
*
* Поле blocked упразднено (2026-05-24): субсчёт «заблокировано» больше не
* используется, баланс кошелька = available.
*/
eosio::asset get_capital_program_share_balance(eosio::name coopname) {
wallets2_index wallets(_ledger2, coopname.value);
auto it = wallets.find(ledger2_wallets::BLAGOROST_FUND.value);
if (it == wallets.end()) return eosio::asset(0, _root_govern_symbol);
return it->available + it->blocked;
return it->available;
}
/**
* @brief Доля пайщика в Благоросте (available + blocked) из ledger2 L3.
* @brief Доля пайщика в Благоросте (available) из ledger2 L3.
*
* Источник `userwallets[(w.cap.blago, username)]`. Запись существует
* только пока available + blocked > 0 (Эпик 3 §6: auto-delete на нуле),
* поэтому отсутствие = нулевой баланс.
* только пока available > 0 (Эпик 3 §6: auto-delete на нуле), поэтому
* отсутствие = нулевой баланс. Поле blocked упразднено (2026-05-24).
*/
eosio::asset get_capital_program_user_share_balance(eosio::name coopname, eosio::name username) {
userwallets_index user_wallets(_ledger2, coopname.value);
@@ -33,6 +36,6 @@ namespace Capital::Core {
auto key = combine_ids(ledger2_wallets::BLAGOROST_FUND.value, username.value);
auto it = idx.find(key);
if (it == idx.end()) return eosio::asset(0, _root_govern_symbol);
return it->available + it->blocked;
return it->available;
}
}
@@ -178,8 +178,96 @@ namespace Capital::Expenses {
Capital::expense_index expenses(_capital, coopname.value);
auto expense = expenses.find(expense_id);
eosio::check(expense != expenses.end(), "Расход не найден");
expenses.erase(expense);
}
} // namespace Capital::Expenses
namespace Capital {
/**
* @brief Таблица программных расходов целевые списания «Благорост»,
* не привязанные к конкретному проекту.
* @par Область памяти (scope): coopname
* @par Имя таблицы (table): progexpenses
*/
struct [[eosio::table, eosio::contract(CAPITAL)]] program_expense {
uint64_t id; ///< Внутренний ключ
name coopname; ///< Имя кооператива
name username; ///< Инициатор расхода (председатель / казначей)
name status = Expenses::Status::CREATED; ///< Статус (CREATED/APPROVED/AUTHORIZED/PAID/DECLINED — общий enum)
checksum256 expense_hash; ///< Хеш расхода — анкер процесса p.cap.expns
uint64_t fund_id; ///< Идентификатор фонда списания
eosio::asset amount; ///< Сумма расхода
std::string description; ///< Описание (для отчётности)
document2 expense_statement; ///< Служебная записка (ProgramExpenseStatement)
document2 approved_statement; ///< Записка, принятая председателем
document2 authorization; ///< Решение совета
time_point_sec spended_at = current_time_point();
uint64_t primary_key() const { return id; }
uint64_t by_username() const { return username.value; }
checksum256 by_expense_hash() const { return expense_hash; }
};
typedef eosio::multi_index<
"progexpenses"_n, program_expense,
indexed_by<"byusername"_n, const_mem_fun<program_expense, uint64_t, &program_expense::by_username>>,
indexed_by<"byhash"_n, const_mem_fun<program_expense, checksum256, &program_expense::by_expense_hash>>
> program_expense_index;
inline std::optional<program_expense> get_program_expense(eosio::name coopname, const checksum256 &hash) {
program_expense_index t(_capital, coopname.value);
auto idx = t.get_index<"byhash"_n>();
auto itr = idx.find(hash);
if (itr == idx.end()) return std::nullopt;
return program_expense(*itr);
}
} // namespace Capital
namespace Capital::Expenses {
inline program_expense get_program_expense_or_fail(eosio::name coopname, const checksum256 &expense_hash) {
auto v = Capital::get_program_expense(coopname, expense_hash);
eosio::check(v.has_value(), "Программный расход с указанным хэшем не найден");
return v.value();
}
inline void create_program_expense(
eosio::name coopname,
const checksum256 &expense_hash,
eosio::name username,
const eosio::asset &amount,
const std::string &description,
const document2 &statement
) {
Capital::program_expense_index t(_capital, coopname.value);
uint64_t id = get_global_id_in_scope(_capital, coopname, "progexpenses"_n);
t.emplace(coopname, [&](auto &e) {
e.id = id;
e.coopname = coopname;
e.username = username;
e.expense_hash = expense_hash;
e.fund_id = Ledger::accounts::GENERAL_EXPENSES;
e.status = Status::CREATED;
e.spended_at = current_time_point();
e.expense_statement = statement;
e.amount = amount;
e.description = description;
});
}
inline void delete_program_expense(eosio::name coopname, uint64_t id) {
Capital::program_expense_index t(_capital, coopname.value);
auto itr = t.find(id);
eosio::check(itr != t.end(), "Программный расход не найден");
t.erase(itr);
}
} // namespace Capital::Expenses
@@ -1,5 +1,7 @@
#pragma once
#include <eosio/binary_extension.hpp>
using namespace eosio;
using std::string;
@@ -40,7 +42,18 @@ namespace Capital {
double program_membership_cumulative_reward_per_share; ///< Накопительное вознаграждение на долю в членских взносах
config config; ///< Управляемая конфигурация контракта
// ВНИМАНИЕ: поля программных расходов добавлены ПОСЛЕ config и обёрнуты в
// binary_extension. Поля в таблицу EOSIO можно дописывать только в ХВОСТ
// struct и только через binary_extension — иначе ломается десериализация
// уже записанного global_state. Инцидент 2026-06-16: эти поля были вставлены
// перед config напрямую (без extension) и задеплоены на прод — таблица state
// перестала читаться (get table → "Invalid symbol", action'ы падали на unpack).
// Инвариант: оба поля материализуются вместе (всегда оба has_value или оба
// пустые), чтобы порядок хвостовых extension оставался консистентным.
eosio::binary_extension<asset> program_expense_pool; ///< Доступная сумма программы под целевые расходы (без аллокации в проекты)
eosio::binary_extension<asset> program_expense_reserved; ///< Сумма, зарезервированная под approved/authorized программные расходы
uint64_t primary_key() const { return coopname.value; } ///< Первичный ключ (1)
};
@@ -50,6 +63,16 @@ namespace Capital {
namespace State {
/**
* @brief Значение binary_extension<asset> или ноль с символом @p sym, если поле
* ещё не материализовано (старая запись, созданная до добавления
* program_expense_* через binary_extension).
*/
inline asset ext_or_zero(const eosio::binary_extension<asset>& v, const eosio::symbol& sym) {
return v.has_value() ? v.value() : asset(0, sym);
}
/**
* @brief Обновляет глобальное состояние новыми значениями.
*
@@ -78,7 +101,7 @@ inline global_state get_global_state(name coopname) {
/**
* @brief Проверяет что контракт инициализирован.
*
*
* @param coopname Имя кооператива.
*/
inline void is_initialized(name coopname) {
@@ -86,6 +109,116 @@ inline void is_initialized(name coopname) {
auto itr = global_state_inst.find(coopname.value);
eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован");
}
/**
* @brief Пополняет пул программных расходов на @p amount из общего пула инвестиций программы.
* Вызывается из action topupprogexp (председатель).
*/
inline void topup_program_expense_pool(eosio::name coopname, const asset &amount) {
global_state_table global_state_inst(_capital, _capital.value);
auto itr = global_state_inst.find(coopname.value);
eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован");
eosio::check(amount.is_valid() && amount.amount > 0, "Сумма пополнения должна быть положительной");
eosio::check(itr->global_available_invest_pool >= amount,
"Недостаточно свободных инвестиций программы для пополнения пула расходов");
global_state_inst.modify(itr, _capital, [&](auto &s) {
asset pool = ext_or_zero(s.program_expense_pool, amount.symbol);
asset reserved = ext_or_zero(s.program_expense_reserved, amount.symbol);
s.global_available_invest_pool -= amount;
pool += amount;
s.program_expense_pool = pool;
s.program_expense_reserved = reserved;
});
}
/**
* @brief Резервирует средства под approved/authorized программный расход:
* program_expense_pool -= amount, program_expense_reserved += amount.
*/
inline void reserve_program_expense(eosio::name coopname, const asset &amount) {
global_state_table global_state_inst(_capital, _capital.value);
auto itr = global_state_inst.find(coopname.value);
eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован");
eosio::check(amount.is_valid() && amount.amount > 0, "Сумма резерва должна быть положительной");
eosio::check(ext_or_zero(itr->program_expense_pool, amount.symbol) >= amount,
"Недостаточно средств в пуле программных расходов");
global_state_inst.modify(itr, _capital, [&](auto &s) {
asset pool = ext_or_zero(s.program_expense_pool, amount.symbol);
asset reserved = ext_or_zero(s.program_expense_reserved, amount.symbol);
pool -= amount;
reserved += amount;
s.program_expense_pool = pool;
s.program_expense_reserved = reserved;
});
}
/**
* @brief Освобождает зарезервированное при отклонении программного расхода:
* program_expense_reserved -= amount, program_expense_pool += amount.
*/
inline void release_program_expense(eosio::name coopname, const asset &amount) {
global_state_table global_state_inst(_capital, _capital.value);
auto itr = global_state_inst.find(coopname.value);
eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован");
eosio::check(amount.is_valid() && amount.amount > 0, "Сумма освобождения должна быть положительной");
eosio::check(ext_or_zero(itr->program_expense_reserved, amount.symbol) >= amount,
"Зарезервированных программных расходов меньше указанной суммы");
global_state_inst.modify(itr, _capital, [&](auto &s) {
asset pool = ext_or_zero(s.program_expense_pool, amount.symbol);
asset reserved = ext_or_zero(s.program_expense_reserved, amount.symbol);
reserved -= amount;
pool += amount;
s.program_expense_pool = pool;
s.program_expense_reserved = reserved;
});
}
/**
* @brief Окончательное списание программного расхода (callback onpgexpdone, CLOSED):
* program_expense_reserved -= amount. Деньги уже ушли через шасси expense,
* ledger2-проводки сделаны на payexp/reportexp/returnexp.
*/
inline void consume_program_expense(eosio::name coopname, const asset &amount) {
global_state_table global_state_inst(_capital, _capital.value);
auto itr = global_state_inst.find(coopname.value);
eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован");
eosio::check(amount.is_valid() && amount.amount > 0, "Сумма списания должна быть положительной");
eosio::check(ext_or_zero(itr->program_expense_reserved, amount.symbol) >= amount,
"Зарезервированных программных расходов меньше указанной суммы");
global_state_inst.modify(itr, _capital, [&](auto &s) {
asset pool = ext_or_zero(s.program_expense_pool, amount.symbol);
asset reserved = ext_or_zero(s.program_expense_reserved, amount.symbol);
reserved -= amount;
s.program_expense_pool = pool;
s.program_expense_reserved = reserved;
});
}
/**
* @brief Прямое списание из свободного пула программных расходов превышение
* факта над резервом при закрытии расхода с перерасходом (overspendexp
* в шасси выдал доплату из BLAGOROST_FUND сверх зарезервированного).
*/
inline void spend_program_expense_pool(eosio::name coopname, const asset &amount) {
global_state_table global_state_inst(_capital, _capital.value);
auto itr = global_state_inst.find(coopname.value);
eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован");
eosio::check(amount.is_valid() && amount.amount > 0, "Сумма списания должна быть положительной");
eosio::check(ext_or_zero(itr->program_expense_pool, amount.symbol) >= amount,
"Недостаточно средств в пуле программных расходов для покрытия перерасхода — пополните пул (topupprogexp)");
global_state_inst.modify(itr, _capital, [&](auto &s) {
asset pool = ext_or_zero(s.program_expense_pool, amount.symbol);
asset reserved = ext_or_zero(s.program_expense_reserved, amount.symbol);
pool -= amount;
s.program_expense_pool = pool;
s.program_expense_reserved = reserved;
});
}
}// namespace State
}// namespace Capital
@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.5)
project(expense)
find_package(cdt)
add_contract(expense expense expense.cpp)
if(IS_TESTNET)
target_compile_definitions(expense PUBLIC IS_TESTNET=1)
endif()
@@ -0,0 +1,425 @@
// expense.cpp — шасси расходов (первый потребитель: Благорост).
//
// Принцип: контракт-владелец расходных операций, агностичен к программе-источнику.
// Кошелёк-пул приходит в createexp параметром source_wallet; ledger2-коды всех
// операций жизненного цикла выводятся из него через EXPENSE_OPERATION_SETS
// (фабричная настройка — подключение нового пула не меняет этот контракт).
// Механика оплаты per-item (ADVANCE | DIRECT) выбирает код внутри набора;
// callback-handler сохраняется как переменная.
#include "expense.hpp"
namespace D = ExpenseDomain;
// ── helpers ──────────────────────────────────────────────────────────────────
namespace {
asset zero_like(const asset& a) { return asset{0, a.symbol}; }
asset sum_planned(const std::vector<D::item>& items) {
if (items.empty()) return asset{0, eosio::symbol{"RUB", 4}};
asset total = zero_like(items.front().planned_amount);
for (const auto& it : items) total += it.planned_amount;
return total;
}
// Набор ledger2-кодов операций шасси по кошельку-источнику СЗ. Один источник =
// один набор (EXPENSE_OPERATION_SETS в ledger2) — в коде expense нет
// захардкоженных operation_code, контракт настраивается на любой пул.
const ExpenseOperationSet& expense_ops_for(eosio::name source_wallet) {
const auto* set = find_expense_operation_set(source_wallet);
eosio::check(set != nullptr,
"Для кошелька-источника не настроен набор операций шасси расходов (EXPENSE_OPERATION_SETS)");
return *set;
}
D::proposals_index get_proposals(eosio::name self, eosio::name coopname) {
return D::proposals_index(self, coopname.value);
}
auto find_proposal(D::proposals_index& tbl, const eosio::checksum256& proposal_hash) {
auto idx = tbl.get_index<"byhash"_n>();
return idx.find(proposal_hash);
}
void send_callback_if_any(const D::callback_handler& cb,
eosio::name coopname,
const eosio::checksum256& proposal_hash,
uint8_t status,
const eosio::asset& amount,
eosio::name self) {
if (cb.contract.value == 0) return;
// Payload: (coopname, proposal_hash, status, total_actual, data).
// status = ExpenseDomain::ProposalStatus — инициатор различает CLOSED vs DECLINED.
// coopname нужен инициатору для скоупа таблиц (capital::progexpenses scope=coopname).
eosio::action(
eosio::permission_level{self, "active"_n},
cb.contract,
cb.action,
std::make_tuple(coopname, proposal_hash, status, amount, cb.data)
).send();
}
} // namespace
// ── actions ──────────────────────────────────────────────────────────────────
void expense::createexp(name coopname, name username,
checksum256 proposal_hash,
name source_wallet,
std::vector<D::item> items,
D::callback_handler callback,
document2 statement) {
// Прямой вызов backend'а — авторизация кооператива; inline от контракта-инициатора
// (capital::createpgexp и т.п.) — авторизация по whitelist, как в ledger2::apply.
if (!has_auth(coopname)) {
check_auth_and_get_payer_or_fail(contracts_whitelist);
}
verify_document_or_fail(statement, {username});
eosio::check(!items.empty(), "СЗ должен содержать хотя бы один item");
eosio::check(ledger2_is_known_wallet(source_wallet),
"source_wallet не найден в LEDGER2_WALLET_REGISTRY");
// Источник определяет ledger2-коды всех операций жизненного цикла —
// отказываем сразу при создании, а не в момент оплаты.
expense_ops_for(source_wallet);
auto tbl = get_proposals(get_self(), coopname);
eosio::check(find_proposal(tbl, proposal_hash) == tbl.get_index<"byhash"_n>().end(),
"СЗ с таким proposal_hash уже существует");
// Механика оплаты задаётся на каждом item отдельно (ADVANCE | DIRECT) —
// в одном СЗ допустимо смешение: ledger2-проводка payexp идёт по-позиционно
// с кодом, выведенным из механики конкретного item'а.
for (auto& it : items) {
eosio::check(it.planned_amount.is_valid() && it.planned_amount.amount > 0,
"planned_amount каждого item должен быть положительным");
eosio::check(it.mechanics <= static_cast<uint8_t>(D::Mechanics::DIRECT),
"Неизвестная mechanics item (0 = ADVANCE, 1 = DIRECT)");
eosio::check(it.recipient_type <= static_cast<uint8_t>(D::RecipientType::ORG),
"Неизвестный recipient_type item");
// Инвариант: пайщик (SELF/MEMBER) получает только аванс под отчёт на личные
// реквизиты (расходом станет после его отчёта чеком); организация — только
// прямую оплату по выставленным реквизитам, аванс ей не выдаётся.
if (it.recipient_type == static_cast<uint8_t>(D::RecipientType::ORG)) {
eosio::check(it.mechanics == static_cast<uint8_t>(D::Mechanics::DIRECT),
"Организации/ИП доступна только оплата по счёту (DIRECT)");
} else {
eosio::check(it.mechanics == static_cast<uint8_t>(D::Mechanics::ADVANCE),
"Пайщику средства выдаются только авансом под отчёт (ADVANCE)");
eosio::check(it.recipient != name(),
"recipient обязателен для получателя-пайщика");
eosio::check(is_account(it.recipient),
"Аккаунт получателя-пайщика не существует");
}
it.status = static_cast<uint8_t>(D::ItemStatus::APPROVED);
it.actual_amount = zero_like(it.planned_amount);
}
const auto total = sum_planned(items);
tbl.emplace(get_self(), [&](auto& row) {
row.id = tbl.available_primary_key();
row.coopname = coopname;
row.proposal_hash = proposal_hash;
row.username = username;
row.source_wallet = source_wallet;
row.status = static_cast<uint8_t>(D::ProposalStatus::CREATED);
row.items = items;
row.total_planned = total;
row.total_actual = zero_like(total);
row.callback = callback;
row.statement_doc = statement;
row.decision_doc = document2{};
row.created_at = eosio::current_time_point();
row.updated_at = row.created_at;
});
// СЗ утверждает совет (как возвраты паевых взносов): ставим вопрос в повестку.
// Утверждение председателем после голосования → callback authexp от _soviet;
// отклонение (отрицательный консенсус / просрочка) → declexp с причиной.
::Soviet::create_agenda(
_expense,
coopname,
username,
get_valid_soviet_action("createexp"_n),
proposal_hash,
_expense,
"authexp"_n,
"declexp"_n,
statement,
std::string("")
);
}
void expense::authexp(name coopname, checksum256 proposal_hash, document2 decision) {
require_auth(_soviet);
auto tbl = get_proposals(get_self(), coopname);
auto idx = tbl.get_index<"byhash"_n>();
auto it = idx.find(proposal_hash);
eosio::check(it != idx.end(), "СЗ не найден");
eosio::check(it->status == static_cast<uint8_t>(D::ProposalStatus::CREATED),
"Авторизовать можно только СЗ в статусе CREATED");
verify_document_or_fail(decision);
idx.modify(it, get_self(), [&](auto& row) {
row.decision_doc = decision;
row.status = static_cast<uint8_t>(D::ProposalStatus::AUTHORIZED);
row.updated_at = eosio::current_time_point();
});
}
void expense::declexp(name coopname, checksum256 proposal_hash, std::string reason) {
require_auth(_soviet);
auto tbl = get_proposals(get_self(), coopname);
auto idx = tbl.get_index<"byhash"_n>();
auto it = idx.find(proposal_hash);
eosio::check(it != idx.end(), "СЗ не найден");
// Отклонение возможно только пока ни один item не оплачен: после payexp деньги
// уже ушли через ledger2, и decline разъехался бы с учётом инициатора
// (callback DECLINED возвращает инициатору весь резерв). Частично исполненный
// СЗ завершается обычным путём: reportexp / returnexp → closeexp.
eosio::check(it->status == static_cast<uint8_t>(D::ProposalStatus::CREATED) ||
it->status == static_cast<uint8_t>(D::ProposalStatus::AUTHORIZED),
"Отклонить можно только СЗ без оплат (статусы CREATED / AUTHORIZED)");
idx.modify(it, get_self(), [&](auto& row) {
row.status = static_cast<uint8_t>(D::ProposalStatus::DECLINED);
row.updated_at = eosio::current_time_point();
});
// Callback инициатору (capital::onpgexpdone и т.п.); total_actual здесь всегда 0 —
// оплат до decline не было (см. проверку статусов выше).
send_callback_if_any(it->callback, coopname, proposal_hash,
static_cast<uint8_t>(D::ProposalStatus::DECLINED),
it->total_actual, get_self());
}
void expense::payexp(name coopname, checksum256 proposal_hash, checksum256 item_hash,
asset actual_amount) {
require_auth(coopname);
auto tbl = get_proposals(get_self(), coopname);
auto idx = tbl.get_index<"byhash"_n>();
auto it = idx.find(proposal_hash);
eosio::check(it != idx.end(), "СЗ не найден");
eosio::check(it->status == static_cast<uint8_t>(D::ProposalStatus::AUTHORIZED) ||
it->status == static_cast<uint8_t>(D::ProposalStatus::PARTIALLY_PAID),
"Оплата возможна только из статусов AUTHORIZED / PARTIALLY_PAID");
eosio::check(actual_amount.is_valid() && actual_amount.amount > 0,
"actual_amount должен быть положительным");
bool item_found = false;
uint8_t paid_mechanics = 0;
name paid_recipient{};
idx.modify(it, get_self(), [&](auto& row) {
bool all_reported = true;
for (auto& i : row.items) {
if (i.item_hash == item_hash) {
eosio::check(i.status == static_cast<uint8_t>(D::ItemStatus::APPROVED),
"Item уже оплачен или закрыт");
eosio::check(actual_amount.symbol == i.planned_amount.symbol,
"Символ actual_amount не совпадает с планом item");
eosio::check(actual_amount.amount <= i.planned_amount.amount,
"Сумма оплаты не может превышать план item; доплата сверх плана — через overspendexp");
i.actual_amount = actual_amount;
// DIRECT не имеет фазы подотчёта (нет ADVANCE_HOLD): закрывающие документы
// прикладывает сам кассир, item считается отчитанным сразу после оплаты.
i.status = static_cast<D::Mechanics>(i.mechanics) == D::Mechanics::DIRECT
? static_cast<uint8_t>(D::ItemStatus::REPORTED)
: static_cast<uint8_t>(D::ItemStatus::PAID);
paid_mechanics = i.mechanics;
paid_recipient = i.recipient;
item_found = true;
}
if (i.status != static_cast<uint8_t>(D::ItemStatus::REPORTED)) {
all_reported = false;
}
}
eosio::check(item_found, "item с заданным hash не найден в СЗ");
row.total_actual += actual_amount;
// DIRECT-only СЗ становится готов к закрытию сразу после последней оплаты.
row.status = all_reported
? static_cast<uint8_t>(D::ProposalStatus::REPORT_SUBMITTED)
: static_cast<uint8_t>(D::ProposalStatus::PARTIALLY_PAID);
row.updated_at = eosio::current_time_point();
});
// ledger2-проводка по механике оплаченного item'а — выдача аванса или прямая
// оплата; коды выводятся из кошелька-источника СЗ. СЗ может смешивать механики.
// username нужен ledger2 только для USER_SHARED-стороны (кошелёк-подотчёт) —
// это ПОЛУЧАТЕЛЬ аванса, не автор СЗ.
const auto& ops = expense_ops_for(it->source_wallet);
const auto code = static_cast<D::Mechanics>(paid_mechanics) == D::Mechanics::ADVANCE
? ops.advance
: ops.direct;
Ledger2::apply(get_self(), coopname, code, actual_amount,
paid_recipient, proposal_hash, "expense:payexp");
}
void expense::reportexp(name coopname, checksum256 proposal_hash, checksum256 item_hash) {
require_auth(coopname);
auto tbl = get_proposals(get_self(), coopname);
auto idx = tbl.get_index<"byhash"_n>();
auto it = idx.find(proposal_hash);
eosio::check(it != idx.end(), "СЗ не найден");
eosio::check(it->status == static_cast<uint8_t>(D::ProposalStatus::PARTIALLY_PAID),
"Отчёт возможен только по СЗ в статусе PARTIALLY_PAID");
asset item_amount = asset{0, it->total_actual.symbol};
bool item_found = false;
name item_recipient{};
idx.modify(it, get_self(), [&](auto& row) {
bool all_reported = true;
for (auto& i : row.items) {
if (i.item_hash == item_hash) {
eosio::check(i.status == static_cast<uint8_t>(D::ItemStatus::PAID),
"Отчёт возможен только для оплаченных items");
eosio::check(static_cast<D::Mechanics>(i.mechanics) == D::Mechanics::ADVANCE,
"reportexp применим только к ADVANCE-механике");
i.status = static_cast<uint8_t>(D::ItemStatus::REPORTED);
item_amount = i.actual_amount;
item_recipient = i.recipient;
item_found = true;
}
if (i.status != static_cast<uint8_t>(D::ItemStatus::REPORTED)) {
all_reported = false;
}
}
eosio::check(item_found, "item с заданным hash не найден в СЗ");
if (all_reported) {
row.status = static_cast<uint8_t>(D::ProposalStatus::REPORT_SUBMITTED);
}
row.updated_at = eosio::current_time_point();
});
// ledger2: BURN кошелька-подотчёта на остаточный actual item'а (выдано
// возвращено + доплачено), без бухпроводки — проводка уже сделана при выдаче.
// Подотчёт числится на получателе аванса — его username и закрывает burn.
// Полный возврат аванса (actual == 0) — burn не нужен, подотчёт уже пуст.
if (item_amount.amount > 0) {
Ledger2::apply(get_self(), coopname, expense_ops_for(it->source_wallet).report, item_amount,
item_recipient, proposal_hash, "expense:reportexp");
}
}
void expense::closeexp(name coopname, checksum256 proposal_hash) {
require_auth(coopname);
auto tbl = get_proposals(get_self(), coopname);
auto idx = tbl.get_index<"byhash"_n>();
auto it = idx.find(proposal_hash);
eosio::check(it != idx.end(), "СЗ не найден");
eosio::check(it->status == static_cast<uint8_t>(D::ProposalStatus::REPORT_SUBMITTED),
"Закрыть можно только СЗ в статусе REPORT_SUBMITTED");
idx.modify(it, get_self(), [&](auto& row) {
row.status = static_cast<uint8_t>(D::ProposalStatus::CLOSED);
row.updated_at = eosio::current_time_point();
});
// Callback на финализацию (capital::onpgexpdone и т.п.) — если был установлен при createexp.
send_callback_if_any(it->callback, coopname, proposal_hash,
static_cast<uint8_t>(D::ProposalStatus::CLOSED),
it->total_actual, get_self());
}
void expense::returnexp(name coopname, checksum256 proposal_hash, checksum256 item_hash,
asset return_amount) {
require_auth(coopname);
auto tbl = get_proposals(get_self(), coopname);
auto idx = tbl.get_index<"byhash"_n>();
auto it = idx.find(proposal_hash);
eosio::check(it != idx.end(), "СЗ не найден");
eosio::check(it->status == static_cast<uint8_t>(D::ProposalStatus::PARTIALLY_PAID),
"Возврат возможен только по СЗ в статусе PARTIALLY_PAID");
bool item_found = false;
name item_recipient{};
idx.modify(it, get_self(), [&](auto& row) {
for (auto& i : row.items) {
if (i.item_hash == item_hash) {
eosio::check(i.status == static_cast<uint8_t>(D::ItemStatus::PAID),
"Возврат возможен только из статуса PAID");
eosio::check(static_cast<D::Mechanics>(i.mechanics) == D::Mechanics::ADVANCE,
"returnexp применим только к ADVANCE-механике");
eosio::check(return_amount.is_valid() &&
return_amount.symbol == i.actual_amount.symbol,
"Символ return_amount не совпадает с item");
eosio::check(return_amount.amount > 0 && return_amount.amount <= i.actual_amount.amount,
"return_amount должен быть положительным и не превышать actual_amount");
// Settlement-запись: статус item НЕ меняется (остаётся PAID) — после
// возврата остатка получатель штатно отчитывается reportexp по
// фактически потраченной части (при полном возврате — actual == 0).
i.actual_amount -= return_amount;
item_recipient = i.recipient;
item_found = true;
break;
}
}
eosio::check(item_found, "item с заданным hash не найден в СЗ");
row.total_actual -= return_amount;
row.updated_at = eosio::current_time_point();
});
// ledger2: TRANSFER подотчёт → пул-источник (зеркало выдачи аванса).
// Подотчёт снимается с получателя аванса.
Ledger2::apply(get_self(), coopname, expense_ops_for(it->source_wallet).refund, return_amount,
item_recipient, proposal_hash, "expense:returnexp");
}
void expense::overspendexp(name coopname, checksum256 proposal_hash, checksum256 item_hash,
asset overspend_amount) {
require_auth(coopname);
eosio::check(overspend_amount.amount > 0, "overspend_amount должен быть положительным");
auto tbl = get_proposals(get_self(), coopname);
auto idx = tbl.get_index<"byhash"_n>();
auto it = idx.find(proposal_hash);
eosio::check(it != idx.end(), "СЗ не найден");
eosio::check(it->status == static_cast<uint8_t>(D::ProposalStatus::PARTIALLY_PAID),
"Перерасход возможен только по СЗ в статусе PARTIALLY_PAID");
bool item_found = false;
name item_recipient{};
idx.modify(it, get_self(), [&](auto& row) {
for (auto& i : row.items) {
if (i.item_hash == item_hash) {
eosio::check(i.status == static_cast<uint8_t>(D::ItemStatus::PAID),
"Перерасход регистрируется только из статуса PAID");
eosio::check(static_cast<D::Mechanics>(i.mechanics) == D::Mechanics::ADVANCE,
"overspendexp применим только к ADVANCE-механике");
eosio::check(overspend_amount.is_valid() &&
overspend_amount.symbol == i.actual_amount.symbol,
"Символ overspend_amount не совпадает с item");
// Settlement-запись: статус item НЕ меняется (остаётся PAID) — подотчёт
// (теперь на полную сумму выдано + доплата) закрывается штатным reportexp,
// который сделает burn ADVANCE_HOLD на итоговый actual.
i.actual_amount += overspend_amount;
item_recipient = i.recipient;
item_found = true;
break;
}
}
eosio::check(item_found, "item с заданным hash не найден в СЗ");
row.total_actual += overspend_amount;
row.updated_at = eosio::current_time_point();
});
// ledger2: доплата перерасхода — TRANSFER пул-источник → подотчёт получателя
// (зеркало выдачи аванса на сумму доплаты). Закрытие на полную сумму — в reportexp.
Ledger2::apply(get_self(), coopname, expense_ops_for(it->source_wallet).overspend, overspend_amount,
item_recipient, proposal_hash, "expense:overspend");
}
@@ -0,0 +1,233 @@
#pragma once
#include <eosio/eosio.hpp>
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include "../lib/index.hpp"
using namespace eosio;
using std::string;
/**
* @defgroup public_expense Контракт EXPENSE
* @brief Шасси расходов цифрового кооператива (MVP Благорост).
*
* Универсальный контракт: principal storage = СЗ-расход с массивом items;
* кошелёк-источник (пул) приходит параметром source_wallet, ledger2-коды всех
* операций жизненного цикла выводятся из него через EXPENSE_OPERATION_SETS
* (ledger2/operations.hpp) подключение нового пула не меняет контракт;
* механика оплаты per-item (ADVANCE | DIRECT) выбирает код внутри набора;
* callback на финализацию (контракт+action+data) сохраняется при createexp
* как переменная `expense` агностичен к программе-получателю.
*
* See: components/desktop/extensions/expenses/NAMING-C28-28.md (v3).
*/
namespace ExpenseDomain {
/**
* @brief Способ оплаты item-а в СЗ.
* ADVANCE пайщик-получатель оплачивает, затем приносит чек.
* DIRECT кассир/председатель платит организации напрямую.
*/
enum class Mechanics : uint8_t {
ADVANCE = 0,
DIRECT = 1,
};
/**
* @brief Получатель платежа в item-е.
* SELF сам создатель СЗ.
* MEMBER другой пайщик.
* ORG внешняя организация.
*/
enum class RecipientType : uint8_t {
SELF = 0,
MEMBER = 1,
ORG = 2,
};
enum class ProposalStatus : uint8_t {
CREATED = 0, ///< СЗ создан, ждёт подписи signact1 создателя.
AUTHORIZED = 1, ///< СЗ подписан signact2 (совет), готов к оплате.
PARTIALLY_PAID = 2, ///< Часть item-ов оплачена.
REPORT_SUBMITTED = 3, ///< Отчёт о расходе подан, ждёт авторизации совета.
CLOSED = 4, ///< СЗ-отчёт авторизован — расход закрыт.
DECLINED = 5, ///< СЗ отклонён до или после авторизации.
};
enum class ItemStatus : uint8_t {
APPROVED = 0, ///< Запланирован, ждёт оплаты.
PAID = 1, ///< Оплачен (ADVANCE — выдан пайщику; DIRECT — оплачен организации).
REPORTED = 2, ///< Закрыт отчётом (чек приложен). DIRECT-item получает его сразу при payexp.
RETURNED = 3, ///< Зарезервировано, не используется: returnexp уменьшает actual_amount, статус остаётся PAID до отчёта.
OVERSPENT = 4, ///< Зарезервировано, не используется: overspendexp увеличивает actual_amount, статус остаётся PAID до отчёта.
};
/**
* @brief Callback на финализацию устанавливается при createexp как переменная.
*
* Контракт expense не знает про capital/blagorost он просто вызывает inline
* action на (contract, action) и прокидывает data наружу. Пустой contract = нет callback.
*
* Пример: для расхода в РИД-проект Благороста UI/backend заполняет
* { "capital"_n, "onexpreport"_n, packed(wip_project_hash) }.
*/
struct callback_handler {
eosio::name contract;
eosio::name action;
std::vector<char> data;
EOSLIB_SERIALIZE(callback_handler, (contract)(action)(data))
};
/**
* @brief Item одна строка СЗ-расхода.
*/
struct item {
eosio::checksum256 item_hash;
uint8_t mechanics; ///< Mechanics
uint8_t recipient_type; ///< RecipientType
eosio::name recipient; ///< username (для SELF/MEMBER), либо name-идентификатор (для ORG)
std::string description;
eosio::asset planned_amount;
eosio::asset actual_amount; ///< 0 пока не оплачено / не отчиталось
uint8_t status; ///< ItemStatus
EOSLIB_SERIALIZE(item, (item_hash)(mechanics)(recipient_type)(recipient)(description)(planned_amount)(actual_amount)(status))
};
/**
* @brief Таблица СЗ-расходов. Scope = coopname.
*/
struct [[eosio::table, eosio::contract("expense")]] proposal {
uint64_t id;
eosio::name coopname; ///< кооператив (= scope таблицы; дублируем полем по канону)
eosio::checksum256 proposal_hash;
eosio::name username; ///< создатель
eosio::name source_wallet; ///< семантически — из какого ЦПП/фонда (w.cap.blago, w.sov.expns, …)
uint8_t status; ///< ProposalStatus
std::vector<item> items;
eosio::asset total_planned;
eosio::asset total_actual;
callback_handler callback; ///< опционально; contract.value == 0 = нет callback
document2 statement_doc; ///< type=2010, signact1 создателя
document2 decision_doc; ///< type=2011, signact2 совета
eosio::time_point_sec created_at;
eosio::time_point_sec updated_at;
uint64_t primary_key() const { return id; }
eosio::checksum256 by_hash() const { return proposal_hash; }
uint64_t by_username() const { return username.value; }
uint64_t by_status() const { return static_cast<uint64_t>(status); }
EOSLIB_SERIALIZE(proposal,
(id)(coopname)(proposal_hash)(username)(source_wallet)(status)
(items)(total_planned)(total_actual)(callback)(statement_doc)(decision_doc)
(created_at)(updated_at))
};
using proposals_index = eosio::multi_index<"proposals"_n, proposal,
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<proposal, eosio::checksum256, &proposal::by_hash>>,
eosio::indexed_by<"byusername"_n, eosio::const_mem_fun<proposal, uint64_t, &proposal::by_username>>,
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<proposal, uint64_t, &proposal::by_status>>
>;
} // namespace ExpenseDomain
/**
* @ingroup public_contracts
* @brief Шасси расходов 8 actions, MVP только Благорост.
*/
class [[eosio::contract("expense")]] expense : public contract {
public:
using contract::contract;
/**
* @brief Создать и подать СЗ-расход.
*
* Создание и подача одна транзакция (slug createexp). На входе: items + источник
* (source_wallet) + опц. callback на финализацию.
* Подписывает создатель (signact1 statement_doc, type=2010).
*
* Авторизация: coopname (прямой вызов backend'а) либо контракт-инициатор из
* contracts_whitelist (inline, например capital::createpgexp).
* Механика оплаты per-item (item.mechanics: ADVANCE | DIRECT); ledger2-код
* операции выводится из механики позиции в момент оплаты payexp.
*
* Ставит вопрос в повестку совета (soviet::createagenda, тип createexp):
* утверждение совета callback authexp, отклонение declexp.
*/
[[eosio::action]]
void createexp(name coopname, name username,
checksum256 proposal_hash,
name source_wallet,
std::vector<ExpenseDomain::item> items,
ExpenseDomain::callback_handler callback,
document2 statement);
/**
* @brief Авторизовать СЗ советом (signact2 decision_doc, type=2011).
* Callback решения совета вызывается только контрактом soviet после
* голосования и утверждения председателем. После этого расход доступен для оплаты.
*/
[[eosio::action]]
void authexp(name coopname, checksum256 proposal_hash, document2 decision);
/**
* @brief Отклонить СЗ. Callback решения совета (отрицательный консенсус
* голосов «против» либо просрочка повестки) вызывается только контрактом
* soviet. Возможно только до первой оплаты (CREATED / AUTHORIZED):
* после payexp средства уже ушли через ledger2, и СЗ завершается обычным
* путём reportexp / returnexp closeexp.
*/
[[eosio::action]]
void declexp(name coopname, checksum256 proposal_hash, std::string reason);
/**
* @brief Оплатить item выдача аванса (ADVANCE) или прямая оплата организации (DIRECT).
* Контракт зовёт Ledger2::apply с кодом из набора операций кошелька-источника
* (EXPENSE_OPERATION_SETS: source_wallet + механика item'а operation_code).
* actual_amount не может превышать план item (доплата через overspendexp).
* DIRECT-item не имеет фазы подотчёта и помечается REPORTED сразу; когда все
* items REPORTED proposal переходит в REPORT_SUBMITTED прямо из payexp.
*/
[[eosio::action]]
void payexp(name coopname, checksum256 proposal_hash, checksum256 item_hash,
asset actual_amount);
/**
* @brief Отчёт о расходе (ADVANCE) пайщик закрывает item чеком.
* Зовёт Ledger2::apply(o.exp.advrpt) на остаточный actual item'а (при полном
* возврате аванса burn пропускается). Когда все items reported статус
* proposal становится REPORT_SUBMITTED.
*/
[[eosio::action]]
void reportexp(name coopname, checksum256 proposal_hash, checksum256 item_hash);
/**
* @brief Закрытие расхода советом финальный signact2 СЗ-отчёта.
* Если у proposal заполнен callback отправляет inline action на (callback.contract, callback.action, callback.data).
*/
[[eosio::action]]
void closeexp(name coopname, checksum256 proposal_hash);
/**
* @brief Возврат неиспользованного аванса (ADVANCE-остаток).
* Settlement-запись: уменьшает actual_amount item'а, статус остаётся PAID
* фактически потраченная часть закрывается затем штатным reportexp.
* Зовёт Ledger2::apply(o.exp.advret).
*/
[[eosio::action]]
void returnexp(name coopname, checksum256 proposal_hash, checksum256 item_hash,
asset return_amount);
/**
* @brief Доплата при перерасходе (ADVANCE).
* Settlement-запись: увеличивает actual_amount item'а, статус остаётся PAID.
* Зовёт Ledger2::apply(o.exp.over) выдача доплаты; закрытие подотчёта на
* полную сумму (выдано + доплата) делает последующий reportexp.
*/
[[eosio::action]]
void overspendexp(name coopname, checksum256 proposal_hash, checksum256 item_hash,
asset overspend_amount);
};
@@ -67,12 +67,12 @@ void ledger2::revert(eosio::name coopname,
eosio::check(original_operation_id != 0, "revert: original_operation_id обязателен");
// -------- validate mirror_wallet_op --------
eosio::check(mirror_wallet_op <= 5, "revert: неизвестный mirror_wallet_op");
// Не позволяем откатывать через BLOCK/UNBLOCK — они асимметричны и нет
// адекватного зеркала в одной операции (BLOCK + UNBLOCK — обратные сами по себе).
eosio::check(mirror_wallet_op != static_cast<uint8_t>(WalletOp::BLOCK) &&
mirror_wallet_op != static_cast<uint8_t>(WalletOp::UNBLOCK),
"revert: BLOCK/UNBLOCK не подлежат откату через revert (они симметричны сами себе)");
// Зеркало revert — только TRANSFER (обмен wallet_from/wallet_to) либо BURN
// (зеркало ISSUE: изъятие с wallet_from). Бывшие BLOCK/UNBLOCK упразднены
// (2026-05-24); ISSUE/NONE как зеркало смысла не имеют.
eosio::check(mirror_wallet_op == static_cast<uint8_t>(WalletOp::TRANSFER) ||
mirror_wallet_op == static_cast<uint8_t>(WalletOp::BURN),
"revert: mirror_wallet_op должен быть TRANSFER или BURN");
// -------- validate mirror wallets/accounts --------
if (mirror_wallet_from.value != 0) {
@@ -1,5 +1,5 @@
/**
* @brief Атомарная операция по кошельку (issue/transfer/block/unblock/burn/burn_blocked).
* @brief Атомарная операция по кошельку (issue/transfer/burn).
*
* Внутренний action ledger2 вызывается только через inline из apply().
* Auth: только сам ledger2 (require_auth(get_self())).
@@ -53,13 +53,15 @@ void ledger2::walletop(eosio::name coopname,
eosio::check(amount.symbol == _root_govern_symbol,
"walletop: некорректный символ валюты");
eosio::check(memo.size() < 256, "walletop: memo > 255");
// op_code = 5 (NONE) намеренно не допускается: NONE-операции — это только
// бухпроводка без кошелькового движения, apply.cpp не диспатчит для них walletop.
// Прямой вызов с op_code=5 был бы no-op и сбил бы инвариант parity.
// Допустимы 0..4 (ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN) и 6 (BURN_BLOCKED).
eosio::check(op_code <= static_cast<uint8_t>(WalletOp::BURN_BLOCKED) &&
op_code != static_cast<uint8_t>(WalletOp::NONE),
"walletop: неизвестный op_code");
// Допустимы только ISSUE(0), TRANSFER(1), BURN(4). NONE(5) — это только
// бухпроводка без кошелькового движения, apply.cpp для неё walletop не диспатчит;
// прямой вызов был бы no-op и сбил бы parity. Бывшие BLOCK(2)/UNBLOCK(3)/
// BURN_BLOCKED(6) упразднены (2026-05-24): резерв возврата теперь TRANSFER на
// w.wal.wpend, их op_code больше не валиден.
eosio::check(op_code == static_cast<uint8_t>(WalletOp::ISSUE) ||
op_code == static_cast<uint8_t>(WalletOp::TRANSFER) ||
op_code == static_cast<uint8_t>(WalletOp::BURN),
"walletop: недопустимый op_code");
wallets2_index wallets(get_self(), coopname.value);
userwallets_index user_wallets(get_self(), coopname.value);
@@ -172,7 +174,7 @@ void ledger2::walletop(eosio::name coopname,
// миграций 048/049.
//
// walletop по построению применяет одно и то же `amount` к L2 и L3 (см.
// ниже case'ы ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN/BURN_BLOCKED), а sender-guard на
// ниже case'ы ISSUE/TRANSFER/BURN), а sender-guard на
// строке 46-47 запрещает обход. Поэтому инвариант сохраняется по
// конструкции; полную сверку выполняет бэкенд («стол бухгалтера»),
// вне транзакционного hot path.
@@ -227,60 +229,6 @@ void ledger2::walletop(eosio::name coopname,
cleanup_l3_if_empty(wallet_from);
break;
}
case WalletOp::BLOCK: {
eosio::check(wallet_from.value != 0, "walletop BLOCK: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop BLOCK: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->available >= amount,
std::string{"walletop BLOCK: недостаточно available на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) {
w.available -= amount;
w.blocked += amount;
});
if (is_user_shared_l3(wallet_from)) {
auto uw = find_l3(wallet_from);
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
uw->available >= amount,
std::string{"walletop BLOCK: недостаточно L3-available у пайщика "} +
username.to_string() + " на " + wallet_from.to_string());
auto uw_pri = user_wallets.find(uw->id);
user_wallets.modify(uw_pri, payer, [&](auto& r) {
r.available -= amount;
r.blocked += amount;
});
}
break;
}
case WalletOp::UNBLOCK: {
eosio::check(wallet_from.value != 0, "walletop UNBLOCK: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop UNBLOCK: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->blocked >= amount,
std::string{"walletop UNBLOCK: недостаточно blocked на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) {
w.blocked -= amount;
w.available += amount;
});
if (is_user_shared_l3(wallet_from)) {
auto uw = find_l3(wallet_from);
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
uw->blocked >= amount,
std::string{"walletop UNBLOCK: недостаточно L3-blocked у пайщика "} +
username.to_string() + " на " + wallet_from.to_string());
auto uw_pri = user_wallets.find(uw->id);
user_wallets.modify(uw_pri, payer, [&](auto& r) {
r.blocked -= amount;
r.available += amount;
});
}
break;
}
case WalletOp::BURN: {
eosio::check(wallet_from.value != 0, "walletop BURN: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop BURN: wallet_to должен быть пустым");
@@ -311,30 +259,6 @@ void ledger2::walletop(eosio::name coopname,
eosio::check(false, "walletop NONE: запрещённый op_code");
break;
}
case WalletOp::BURN_BLOCKED: {
eosio::check(wallet_from.value != 0, "walletop BURN_BLOCKED: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop BURN_BLOCKED: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->blocked >= amount,
std::string{"walletop BURN_BLOCKED: недостаточно blocked на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) { w.blocked -= amount; });
if (is_user_shared_l3(wallet_from)) {
auto uw = find_l3(wallet_from);
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
uw->blocked >= amount,
std::string{"walletop BURN_BLOCKED: недостаточно L3-blocked у пайщика "} +
username.to_string() + " на " + wallet_from.to_string());
auto uw_pri = user_wallets.find(uw->id);
user_wallets.modify(uw_pri, payer, [&](auto& r) { r.blocked -= amount; });
}
cleanup_l2_if_empty(wallet_from);
cleanup_l3_if_empty(wallet_from);
break;
}
}
// Post-mutation Σ L3 == L2: см. блок выше (строки 163-175). Снято
@@ -1,4 +1,4 @@
#include <vector>
#include <array>
/**
* @brief Универсальное миграционное действие контракта ledger2 точка
@@ -9,23 +9,33 @@
* после её прогона на проде тело очищается до пустого `require_auth(get_self())`
* (как в `capital::migrate`).
*
* Текущая задача (2026-05-21): чистка осиротевших L3-записей `w.cap.gen`
* на voskhod после смены WalletKind GENERATOR_FUND с USER_SHARED на
* COOPERATIVE (см. `lib/core/ledger2/wallets.hpp`).
* Текущая задача (2026-05-24): свёртка `blocked available` по ВСЕМ коопам.
*
* Контекст: на voskhod `convertsegm` падал «недостаточно L3-средств у
* пайщика» в проектах, где CRPS перераспределял доли между сегментами.
* w.cap.gen был USER_SHARED, а CRPS в approvecmmt не делал per-user
* компенсирующих TRANSFER между сегментами: `Σ COMMIT_RID == Σ ACCEPT_RID`
* соблюдался только на проекте, не на пайщике.
* Контекст: механика «заблокированного» баланса упразднена (см.
* `lib/core/ledger2/operations.hpp` удалены WalletOp BLOCK/UNBLOCK/BURN_BLOCKED;
* резерв возврата паевого теперь выражается переводом на кошелёк-резерв
* `w.wal.wpend`). Поле `blocked` остаётся в таблицах `wallets2`/`userwallets`
* как deprecated (физическое удаление поля = небезопасная смена layout таблицы
* на живых коопах, выносится в отдельный cleanup-деплой). Перед тем как поле
* перестанет поддерживаться кодом, накопленные `blocked`-остатки нужно вернуть
* в `available`, чтобы средства не «зависли» на упразднённом субсчёте.
*
* Архитектурный фикс: w.cap.gen COOPERATIVE-пул без L3. Чтобы UI/бэкенд
* не врали остатками по «личным» w.cap.gen, удаляем осиротевшие L3-записи
* прямым `userwallets.erase`. L2-баланс `wallets2[w.cap.gen]` уже верен
* (синхронен с Σ старых userwallets), его не трогаем.
* Действие: пройти всех кооперативов (cooperatives2 в scope registrator) и для
* каждого свернуть `blocked available` на уровнях L2 (`wallets2`) и L3
* (`userwallets`): `available += blocked; blocked = 0`. Сумма средств на кошельке
* не меняется только субсчёт.
*
* Идемпотентно: повторный вызов на чистой БД no-op (lower_bound пуст).
* Только voskhod: на остальных кооперативах w.cap.gen не использовался.
* Идемпотентно: после свёртки `blocked == 0`, повторный прогон no-op.
*
* Сигнатура без аргументов действие вызывается автоматически при деплое
* контракта (как и прочие задачи migrate); проходит по всем кооперативам сам.
*
* ПРЕДУСЛОВИЕ (операционное): на момент прогона не должно быть заявок на возврат
* «в полёте» (статусы pending/authorized в `wallet::withdraws`) их `blocked`
* относится к старой механике и при свёртке в `available` вернётся пайщику как
* свободные средства, а последующий `completewthd` (BURN с `w.wal.wpend`) не
* найдёт резерва. Незавершённые заявки нужно довести (complete/decline) ДО
* деплоя с этой миграцией.
*
* @ingroup public_ledger2_actions
*
@@ -34,24 +44,101 @@
void ledger2::migrate() {
require_auth(get_self());
const eosio::name target_coop = "voskhod"_n;
cooperatives2_index coops(_registrator, _registrator.value);
userwallets_index user_wallets(get_self(), target_coop.value);
auto idx = user_wallets.get_index<"bywallet"_n>();
for (auto c = coops.begin(); c != coops.end(); ++c) {
const eosio::name coopname = c->username;
// Собираем primary id записей до erase (модификация контейнера при
// итерации через secondary index — небезопасна).
std::vector<uint64_t> ids_to_erase;
for (auto it = idx.lower_bound(ledger2_wallets::GENERATOR_FUND.value);
it != idx.end() && it->wallet_name == ledger2_wallets::GENERATOR_FUND;
++it) {
ids_to_erase.push_back(it->id);
// --- L3: userwallets[coopname] ---
// Модифицируем только не-ключевые поля (available/blocked) — итерация по
// первичному индексу с modify безопасна (порядок строк не меняется).
userwallets_index user_wallets(get_self(), coopname.value);
for (auto it = user_wallets.begin(); it != user_wallets.end(); ++it) {
if (it->blocked.amount <= 0) continue;
user_wallets.modify(it, get_self(), [&](auto& r) {
r.available += r.blocked;
r.blocked = eosio::asset(0, r.blocked.symbol);
});
}
// --- L2: wallets2[coopname] ---
wallets2_index wallets(get_self(), coopname.value);
for (auto it = wallets.begin(); it != wallets.end(); ++it) {
if (it->blocked.amount <= 0) continue;
wallets.modify(it, get_self(), [&](auto& w) {
w.available += w.blocked;
w.blocked = eosio::asset(0, w.blocked.symbol);
});
}
}
for (uint64_t id : ids_to_erase) {
auto pri = user_wallets.find(id);
if (pri != user_wallets.end()) {
user_wallets.erase(pri);
// ──────────────────────────────────────────────────────────────────────────
// Разовая коррекция СЧЕТОВ под удаление фантомных пайщиков fgrtejiwnynn
// (инцидент 2026-06-04). Каждый из 6 дублей-аккаунтов при регистрации внёс
// минимальный паевой 300 RUB (o.reg.putmin: Dr 51 / Cr 80, ISSUE w.reg.minshr).
// Эти суммы — фантомные (нет реального пайщика), их нужно убрать из паевого
// фонда и с расчётного счёта. Снимаем L3-доли фантомов на w.reg.minshr,
// суммируем фактически снятое и ровно на эту сумму уменьшаем L2-кошелёк и
// бухсчета 80 (Cr) и 51 (Dr). Реальные пайщики (их доли на том же w.reg.minshr
// и счетах) не затрагиваются — трогаем только перечисленные username.
//
// Список синхронизирован с registrator::migrate и soviet::migrate.
// Идемпотентно: после первого прогона L3-строк фантомов нет → removed == 0 →
// L2/бухсчета не трогаются (вся коррекция в одной atomic-tx).
{
const eosio::name PHANTOM_COOP = "fgrtejiwnynn"_n;
const std::array<eosio::name, 6> PHANTOMS = {
"bbsezpgufvmm"_n, "errwcgjwverm"_n, "hcfsluqsfehw"_n,
"kgkzdadfpzki"_n, "nzuyijobapsv"_n, "tplwfwbujugq"_n,
};
const eosio::name MINSHR = ledger2_wallets::MIN_SHARE_FUND; // w.reg.minshr
eosio::asset removed(0, _root_govern_symbol);
// 1) Снимаем L3-доли фантомов на w.reg.minshr, суммируем снятое.
userwallets_index uw(get_self(), PHANTOM_COOP.value);
auto uw_byuser = uw.get_index<"byuser"_n>();
for (const auto& u : PHANTOMS) {
auto it = uw_byuser.lower_bound(u.value);
while (it != uw_byuser.end() && it->username == u) {
if (it->wallet_name == MINSHR) {
removed += it->available + it->blocked;
it = uw_byuser.erase(it);
} else {
++it;
}
}
}
if (removed.amount > 0) {
// 2) L2 w.reg.minshr: available -= removed; удалить кошелёк, если опустел.
wallets2_index w2(get_self(), PHANTOM_COOP.value);
auto wit = w2.find(MINSHR.value);
if (wit != w2.end()) {
w2.modify(wit, get_self(), [&](auto& w) { w.available -= removed; });
if (wit->is_empty()) w2.erase(wit);
}
// 3) Бухсчета (scope=coopname): Cr 80 (паевой фонд) -= removed,
// Dr 51 (расчётный счёт) -= removed; пересчёт сальдо по типу счёта.
// Инвариант Σ Dr == Σ Cr сохраняется (обе стороны уменьшены на removed).
accounts2_index acc(get_self(), PHANTOM_COOP.value);
auto a80 = acc.find(ledger2_accounts::SHARE_FUND);
if (a80 != acc.end()) {
acc.modify(a80, get_self(), [&](auto& a) {
a.credit_balance -= removed;
a.balance = account2::compute_balance(a.account_type, a.debit_balance, a.credit_balance);
});
}
auto a51 = acc.find(ledger2_accounts::BANK_ACCOUNT);
if (a51 != acc.end()) {
acc.modify(a51, get_self(), [&](auto& a) {
a.debit_balance -= removed;
a.balance = account2::compute_balance(a.account_type, a.debit_balance, a.credit_balance);
});
}
}
}
}
+11 -5
View File
@@ -80,12 +80,13 @@ static constexpr eosio::name _wallet_program = "wallet"_n; ///< Главный
static constexpr eosio::name _marketplace_program = "marketplace"_n; ///< Кошелёк программы "Маркетплейс"
static constexpr eosio::name _source_program = "generator"_n; ///< Кошелёк для генерации по договору УХД
static constexpr eosio::name _capital_program = "blagorost"_n; ///< Кошелёк программы "Благорост"
static constexpr eosio::name _capital_program = "capital"_n; ///< Кошелёк программы "Благорост"
static const std::set<eosio::name> soviet_actions = {
"joincoop"_n, //регистрация пайщика
"leavecoop"_n, //выход пайщика из кооператива (возврат паевого взноса)
//MEET
"creategm"_n,//предложение повестки планового общего собрание
"completegm"_n, //решение общего собрания пайщиков
@@ -102,7 +103,10 @@ static const std::set<eosio::name> soviet_actions = {
//WALLET
"createwthd"_n, //создать заявление на возврат паевого взноса
//EXPENSE
"createexp"_n, //служебная записка-смета о расходах (шасси расходов)
//LEDGER
"ledgerwthd"_n, //заявление на списание со счета через ledger
@@ -165,6 +169,7 @@ static constexpr uint64_t _capital_program_id = 4;
static constexpr eosio::name _ledger = "ledger"_n;
static constexpr eosio::name _ledger2 = "ledger2"_n;
static constexpr eosio::name _apps = "apps"_n;
static constexpr eosio::name _expense = "expense"_n;
static constexpr eosio::name _power_account = "eosio.power"_n;
static constexpr eosio::name _saving_account = "eosio.saving"_n;
@@ -193,7 +198,8 @@ static constexpr uint64_t _capital_program_id = 4;
"ledger"_n,
"ledger2"_n,
"capital"_n,
"apps"_n
"apps"_n,
"expense"_n
// Добавьте остальные стандартные или пользовательские контракты по необходимости
};
@@ -239,7 +245,7 @@ static constexpr uint64_t _capital_program_id = 4;
#ifdef IS_TESTNET
static constexpr uint64_t MIN_SOVIET_MEMBERS_COUNT = 1; /*!< минимальное количество членов совета (тест) */
#else
static constexpr uint64_t MIN_SOVIET_MEMBERS_COUNT = 5; /*!< минимальное количество членов совета (прод) */
static constexpr uint64_t MIN_SOVIET_MEMBERS_COUNT = 3; /*!< минимальное количество членов совета (прод) */
#endif
/**
@@ -23,6 +23,11 @@ enum class AccountType : uint8_t {
ACTIVE_PASSIVE = 2,
};
// Пересмотр 2026-06-06: добавлен счёт 76 «Расчёты с пайщиками» (А-П) —
// суспенс для регистрационного взноса между приёмом денег кассой
// (Dr 51 / Cr 76) и решением совета. На одобрении переносится на 80/86
// (Dr 76 / Cr 80, Dr 76 / Cr 86), на отказе возвращается (Dr 76 / Cr 51).
/**
* @brief План счетов ledger2 (MVP) со смещением *1000.
*
@@ -32,12 +37,13 @@ enum class AccountType : uint8_t {
* состояния «принятый коммит» (паевой взнос имуществом в переходе РИД
* в программу Благорост): commit Dr 08 / Cr 80, accept Dr 04 / Cr 08.
*
* Состав (6 счетов):
* Состав (7 счетов):
*
* - 04 Нематериальные активы (РИД, принятые в паевой фонд)
* - 08 Вложения во внеоборотные активы (промежуточное состояние)
* - 51 Расчётный счёт
* - 58 Финансовые вложения (выданные пайщикам беспроцентные займы)
* - 76 Расчёты с пайщиками (суспенс регистрационного взноса до решения совета)
* - 80 Паевой фонд (складочный капитал)
* - 86 Целевое финансирование (без субсчетов)
*
@@ -55,6 +61,9 @@ struct ledger2_accounts {
static constexpr uint64_t BANK_ACCOUNT = 51 * 1000; ///< 51 — Расчётный счёт (А)
static constexpr uint64_t FINANCIAL_INVESTMENTS = 58 * 1000; ///< 58 — Финансовые вложения (А)
// Активно-пассивные
static constexpr uint64_t PARTICIPANT_SETTLEMENTS = 76 * 1000; ///< 76 — Расчёты с пайщиками (А-П): суспенс регистрационного взноса до решения совета
// Пассивы
static constexpr uint64_t SHARE_FUND = 80 * 1000; ///< 80 — Паевой фонд (П)
static constexpr uint64_t TARGET_RECEIPTS = 86 * 1000; ///< 86 — Целевое финансирование (П)
@@ -79,11 +88,12 @@ struct Ledger2AccountMeta {
* `constexpr std::array` + `string_view` чтобы не было dynamic init
* при загрузке контракта и тип был полностью заморожен на этапе сборки.
*/
inline constexpr std::array<Ledger2AccountMeta, 6> LEDGER2_ACCOUNT_MAP = {{
inline constexpr std::array<Ledger2AccountMeta, 7> LEDGER2_ACCOUNT_MAP = {{
{ ledger2_accounts::INTANGIBLE_ASSETS, "Нематериальные активы", AccountType::ACTIVE },
{ ledger2_accounts::NON_CURRENT_INVESTMENTS, "Вложения во внеоборотные активы", AccountType::ACTIVE },
{ ledger2_accounts::BANK_ACCOUNT, "Расчётный счёт", AccountType::ACTIVE },
{ ledger2_accounts::FINANCIAL_INVESTMENTS, "Финансовые вложения", AccountType::ACTIVE },
{ ledger2_accounts::PARTICIPANT_SETTLEMENTS, "Расчёты с пайщиками", AccountType::ACTIVE_PASSIVE },
{ ledger2_accounts::SHARE_FUND, "Паевой фонд (складочный капитал)", AccountType::PASSIVE },
{ ledger2_accounts::TARGET_RECEIPTS, "Целевое финансирование", AccountType::PASSIVE },
}};
@@ -31,9 +31,9 @@
* - `WalletOp::WALLET_ONLY` удалён (ADR-003): «без бухпроводок» определяется
* парой `(debit_account_id == 0, credit_account_id == 0)` на уровне записи.
* Для `o.cap.invest` теперь TRANSFER без проводок (оба account_id == 0).
* - Добавлен `WalletOp::BURN` (ADR-003): `available -= amount` на `wallet_from`,
* без `wallet_to`. На текущем этапе в `OPERATION_REGISTRY` не используется
* зарезервирован под будущие операции штатного сжигания.
* - `WalletOp::BURN` (ADR-003): `available -= amount` на `wallet_from`,
* без `wallet_to`. Используется в `o.wal.wthcpl` (сжигание резерва возврата
* с `w.wal.wpend`), `o.cap.drppre` и как зеркало ISSUE в `revert`.
* - Единые программные кошельки: `BLAGOROST_FUND` (`w.cap.blago`) и
* `GENERATOR_FUND` (`w.cap.gen`) заменили ранее декомпозированные
* `bginv/bgprop/bgrid/bgmem` и `gncom/gnmem` (ADR-009).
@@ -54,16 +54,22 @@ namespace operations {
// registrator
namespace registrator {
inline constexpr eosio::name PAY_ENTRANCE = "o.reg.payent"_n; ///< Оплата вступительного взноса (Dr 51 / Cr 86, ISSUE ENTRANCE_FEES).
inline constexpr eosio::name PUT_MINSHARE = "o.reg.putmin"_n; ///< Внесение минимального паевого при регистрации (Dr 51 / Cr 80, ISSUE MIN_SHARE_FUND).
inline constexpr eosio::name PAY_ENTRANCE = "o.reg.payent"_n; ///< Оплата вступительного взноса (Dr 51 / Cr 86, ISSUE ENTRANCE_FEES). Одношаговый путь adduser (без совета).
inline constexpr eosio::name PUT_MINSHARE = "o.reg.putmin"_n; ///< Внесение минимального паевого при регистрации (Dr 51 / Cr 80, ISSUE MIN_SHARE_FUND). Одношаговый путь adduser (без совета).
// Двухфазный путь через совет (reguser → confirmpay → confirmreg/declinereg):
inline constexpr eosio::name RECEIVE_PAYMENT = "o.reg.inpay"_n; ///< Приём регистрационного взноса кассой в ожидание решения совета (Dr 51 / Cr 76, ISSUE REGISTRATION_PENDING).
inline constexpr eosio::name SETTLE_MINSHARE = "o.reg.setmin"_n; ///< Зачисление минимального паевого по решению совета (Dr 76 / Cr 80, TRANSFER REGISTRATION_PENDING → MIN_SHARE_FUND).
inline constexpr eosio::name SETTLE_ENTRANCE = "o.reg.setent"_n; ///< Зачисление вступительного по решению совета (Dr 76 / Cr 86, TRANSFER REGISTRATION_PENDING → ENTRANCE_FEES).
inline constexpr eosio::name REFUND = "o.reg.refund"_n; ///< Возврат регистрационного взноса при отказе совета (Dr 76 / Cr 51, BURN REGISTRATION_PENDING — деньги уходят из системы банковским переводом кандидату).
inline constexpr eosio::name MOVE_MINSHARE = "o.reg.mvmin"_n; ///< Перенос минимального паевого на главный паевой при выходе из кооператива (TRANSFER MIN_SHARE_FUND → SHARE_FUND_PAY, без Dr/Cr — оба кошелька на счёте 80). Готовит полный паевой к возврату.
}
// wallet
namespace wallet {
inline constexpr eosio::name COMPLETE_DEPOSIT = "o.wal.depcpl"_n; ///< Завершение внесения паевого взноса (Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY).
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, BURN_BLOCKED SHARE_FUND_PAY — деньги уходят из системы, без wallet_to).
inline constexpr eosio::name REQUEST_WITHDRAW = "o.wal.wthreq"_n; ///< Запрос на возврат паевого: BLOCK на SHARE_FUND_PAY (без Dr/Cr).
inline constexpr eosio::name DECLINE_WITHDRAW = "o.wal.wthdec"_n; ///< Отклонение запроса на возврат: UNBLOCK на SHARE_FUND_PAY (без Dr/Cr).
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, BURN с WITHDRAW_PENDING — деньги уходят из системы, без wallet_to).
inline constexpr eosio::name REQUEST_WITHDRAW = "o.wal.wthreq"_n; ///< Запрос на возврат паевого: TRANSFER SHARE_FUND_PAY → WITHDRAW_PENDING (резерв, без Dr/Cr).
inline constexpr eosio::name DECLINE_WITHDRAW = "o.wal.wthdec"_n; ///< Отклонение запроса на возврат: TRANSFER WITHDRAW_PENDING → SHARE_FUND_PAY (снятие резерва, без Dr/Cr).
}
// capital
@@ -80,6 +86,7 @@ namespace operations {
inline constexpr eosio::name WITHDRAW_FROM_CAPITAL = "o.cap.wthcap"_n; ///< Возврат паевого из ЦПП «Благорост» в кошелёк пайщика (TRANSFER BLAGOROST_FUND → SHARE_FUND_PAY, без Dr/Cr).
inline constexpr eosio::name CONVERT_TO_SHARE = "o.cap.cnvshr"_n; ///< Конвертация сегмента: РИД → главный кошелёк (TRANSFER GENERATOR_FUND → SHARE_FUND_PAY, без Dr/Cr — бухпроводка уже была сделана в ACCEPT_RID).
inline constexpr eosio::name CONVERT_TO_BLAGO = "o.cap.cnvbl"_n; ///< Конвертация сегмента: РИД → ЦПП «Благорост» (TRANSFER GENERATOR_FUND → BLAGOROST_FUND, без Dr/Cr — бухпроводка уже была сделана в ACCEPT_RID).
inline constexpr eosio::name PROGRAM_EXPENSE_TOPUP = "o.cap.pgtop"_n; ///< Пополнение пула программных расходов из инвестиций программы (ISSUE PROGRAM_EXPENSE_POOL, без Dr/Cr — деньги уже на 51, выделяется кооперативный резерв под расходы; паевые L3-кошельки пайщиков не трогаются).
}
// marketplace
@@ -93,6 +100,27 @@ namespace operations {
inline constexpr eosio::name CONVERT_AXN = "o.sov.axncnv"_n; ///< Трансляция паевого взноса в членский (Dr 80 / Cr 86, TRANSFER SHARE_FUND_PAY → DELEGATE_FEES).
}
// expense — шасси расходов (MVP: только Благорост; хозрасходы из членских — отдельный эпик).
//
// Принципы (см. components/desktop/extensions/expenses/NAMING-C28-28.md):
// - Контракт `expense` универсальный: operation_code передаётся в payload.
// - Источник оплат — КООПЕРАТИВНЫЙ пул расходов (PROGRAM_EXPENSE_POOL),
// пополняемый o.cap.pgtop; личные L3-кошельки пайщиков (w.cap.blago)
// при оплатах СЗ не изменяются.
// - При расходе из Благороста паевой фонд (80) НЕ трогается: меняется только форма
// актива 51 → 08 (Дт 08 / Кт 51 для обеих механик).
// - ADVANCE-отчёт (`o.exp.advrpt`) НЕ создаёт новой бухпроводки: проводка уже
// сделана на `o.exp.blgadv` при выдаче.
// - Callback на финализацию — переменная (`callback{contract, action, data}`),
// заполняется при `expense::createexp`; expense ничего не знает про capital.
namespace expense {
inline constexpr eosio::name BLAGO_ADVANCE = "o.exp.blgadv"_n; ///< Выдача подотчётных из пула расходов (TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD, Dr 08 / Cr 51).
inline constexpr eosio::name BLAGO_DIRECT = "o.exp.blgdir"_n; ///< Прямая оплата из пула расходов (BURN PROGRAM_EXPENSE_POOL, Dr 08 / Cr 51).
inline constexpr eosio::name ADVANCE_REPORT = "o.exp.advrpt"_n; ///< Закрытие подотчёта пайщика (BURN ADVANCE_HOLD, без бухпроводки — canal 08/51 уже сделан на blgadv).
inline constexpr eosio::name ADVANCE_RETURN = "o.exp.advret"_n; ///< Возврат неиспользованного подотчёта (TRANSFER ADVANCE_HOLD → PROGRAM_EXPENSE_POOL, Dr 51 / Cr 08).
inline constexpr eosio::name OVERSPEND = "o.exp.over"_n; ///< Доплата сверх подотчёта (TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD, Dr 08 / Cr 51); сразу за ней expense вызывает ADVANCE_REPORT.
}
// migration (только из migrate.cpp)
//
// В OPERATION_REGISTRY включены **только** те транзиты, которые проводятся
@@ -129,14 +157,23 @@ namespace operations {
* `(debit_account_id == 0, credit_account_id == 0)` на уровне записи реестра.
* Compile-time правило `(debit==0) (credit==0)` ловит смешанные пары.
*/
//
// Удаление BLOCK/UNBLOCK/BURN_BLOCKED (2026-05-24): механика «заблокированного»
// баланса упразднена. Резерв средств под заявку на возврат паевого теперь
// выражается переводом на отдельный кошелёк-резерв `w.wal.wpend` (TRANSFER),
// возврат резерва — обратным TRANSFER, завершение — BURN с резерва. Поле
// `blocked` в таблицах wallets2/userwallets оставлено deprecated (всегда 0
// после ledger2::migrate-свёртки) — физическое удаление поля = небезопасная
// смена layout таблицы на живых коопах, выносится в отдельный cleanup-деплой.
//
// Числовые значения ISSUE/TRANSFER/BURN/NONE СОХРАНЕНЫ (не перенумерованы),
// чтобы исторические op_code в blockchain_actions читались бэкендом без сдвига
// смысла (2 и 3 — бывшие BLOCK/UNBLOCK — больше не выдаются и невалидны на входе).
enum class WalletOp : uint8_t {
ISSUE = 0, ///< первичный вход средств на кошелёк wallet_to (wallet_from = empty)
TRANSFER = 1, ///< перемещение wallet_from → wallet_to (с Dr/Cr ИЛИ без — по парам account_id)
BLOCK = 2, ///< available-=amount, blocked+=amount на wallet_from
UNBLOCK = 3, ///< blocked-=amount, available+=amount на wallet_from
BURN = 4, ///< изъятие amount с wallet_from->available, без wallet_to. Покрывает оба кейса: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY; (b) зеркало ISSUE при `ledger2::revert` (различие — через operation_code: `o.adj.rev` для adjustment-mirror).
NONE = 5, ///< только бухпроводка без перемещения средств (wallet_from = empty, wallet_to = empty, debit ≠ 0, credit ≠ 0). Покрывает кейсы внутрибалансовых проводок типа Dr 04 / Cr 08 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
BURN_BLOCKED = 6, ///< изъятие amount из wallet_from->blocked, без wallet_to. Используется при завершении возврата паевого (`o.wal.wthcpl`): средства предварительно заблокированы через BLOCK (REQUEST_WITHDRAW), на завершении сжигаются — выходят из системы (получателя на цепи нет). На L3 синхронно: from_uw->blocked -= amount.
};
/**
@@ -145,7 +182,7 @@ enum class WalletOp : uint8_t {
* Семантика полей по `wallet_op`:
* - ISSUE: wallet_from = eosio::name{}, wallet_to = required.
* - TRANSFER: wallet_from = required, wallet_to = required ( from).
* - BLOCK / UNBLOCK / BURN / BURN_BLOCKED: wallet_from = required, wallet_to = eosio::name{}.
* - BURN: wallet_from = required, wallet_to = eosio::name{}.
*
* Семантика бух.проводки:
* - Без проводок: debit_account_id == 0 И credit_account_id == 0.
@@ -157,7 +194,7 @@ struct OperationRegistryEntry {
eosio::name process_type; ///< тип процесса с префиксом `p.<contract>.<noun>`
WalletOp wallet_op;
eosio::name wallet_from; ///< пустое имя для ISSUE
eosio::name wallet_to; ///< пустое имя для BLOCK/UNBLOCK/BURN/BURN_BLOCKED
eosio::name wallet_to; ///< пустое имя для BURN
uint64_t debit_account_id; ///< 0 если без бухпроводки (тогда credit_account_id тоже == 0)
uint64_t credit_account_id; ///< 0 если без бухпроводки (тогда debit_account_id тоже == 0)
const char* human_name;
@@ -179,17 +216,53 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Минимальный паевой взнос пайщика при регистрации" },
// 2a. Приём регистрационного взноса кассой (поток через совет): Dr 51 / Cr 76, ISSUE REGISTRATION_PENDING.
// Деньги получены, но взнос ещё не признан — висит на расчётах с пайщиком (76)
// до решения совета. Сумма = вступительный + минимальный паевой.
{ operations::registrator::RECEIVE_PAYMENT, processes::registrator::ACCEPT, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::REGISTRATION_PENDING,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::PARTICIPANT_SETTLEMENTS,
"Приём регистрационного взноса в ожидание решения совета" },
// 2b. Зачисление минимального паевого по решению совета: Dr 76 / Cr 80, TRANSFER REGISTRATION_PENDING → MIN_SHARE_FUND.
{ operations::registrator::SETTLE_MINSHARE, processes::registrator::ACCEPT, WalletOp::TRANSFER,
ledger2_wallets::REGISTRATION_PENDING, ledger2_wallets::MIN_SHARE_FUND,
ledger2_accounts::PARTICIPANT_SETTLEMENTS, ledger2_accounts::SHARE_FUND,
"Зачисление минимального паевого взноса по решению совета" },
// 2c. Зачисление вступительного по решению совета: Dr 76 / Cr 86, TRANSFER REGISTRATION_PENDING → ENTRANCE_FEES.
{ operations::registrator::SETTLE_ENTRANCE, processes::registrator::ACCEPT, WalletOp::TRANSFER,
ledger2_wallets::REGISTRATION_PENDING, ledger2_wallets::ENTRANCE_FEES,
ledger2_accounts::PARTICIPANT_SETTLEMENTS, ledger2_accounts::TARGET_RECEIPTS,
"Зачисление вступительного взноса по решению совета" },
// 2d. Возврат регистрационного взноса при отказе совета: Dr 76 / Cr 51, BURN REGISTRATION_PENDING.
// Отдельный процесс p.reg.refund: приём взноса прерывается, начинается возврат.
// Деньги уходят из системы (банковский перевод кандидату), получателя на цепи нет.
{ operations::registrator::REFUND, processes::registrator::REFUND, WalletOp::BURN,
ledger2_wallets::REGISTRATION_PENDING, eosio::name{},
ledger2_accounts::PARTICIPANT_SETTLEMENTS, ledger2_accounts::BANK_ACCOUNT,
"Возврат регистрационного взноса при отказе совета" },
// 2e. Перенос минимального паевого на главный при выходе из кооператива:
// TRANSFER MIN_SHARE_FUND → SHARE_FUND_PAY (без Dr/Cr — оба кошелька на счёте 80).
// Консолидирует минимальный паевой на главный, чтобы вернуть его вместе с
// основным паевым через wallet-withdraw (o.wal.wthcpl, Дт 80 / Кт 51).
{ operations::registrator::MOVE_MINSHARE, processes::wallet::WITHDRAW, WalletOp::TRANSFER,
ledger2_wallets::MIN_SHARE_FUND, ledger2_wallets::SHARE_FUND_PAY,
0, 0,
"Перенос минимального паевого на главный при выходе из кооператива" },
// 3. Внесение паевого взноса: Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY
{ operations::wallet::COMPLETE_DEPOSIT, processes::wallet::DEPOSIT, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::SHARE_FUND_PAY,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Внесение пайщиком паевого взноса" },
// 4. Возврат паевого взноса: Dr 80 / Cr 51, BURN_BLOCKED SHARE_FUND_PAY.
// Сжигание из заблокированной суммы пайщика (BLOCK был на REQUEST_WITHDRAW):
// 4. Возврат паевого взноса: Dr 80 / Cr 51, BURN WITHDRAW_PENDING.
// Сжигание из кошелька-резерва (TRANSFER в резерв был на REQUEST_WITHDRAW):
// деньги уходят из системы (банковский перевод пайщику), получателя на цепи нет.
// Бухгалтерия: паевой фонд уменьшается (Дт 80), расчётный счёт уменьшается (Кт 51).
{ operations::wallet::COMPLETE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BURN_BLOCKED,
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
{ operations::wallet::COMPLETE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BURN,
ledger2_wallets::WITHDRAW_PENDING, eosio::name{},
ledger2_accounts::SHARE_FUND, ledger2_accounts::BANK_ACCOUNT,
"Возврат паевого взноса пайщику" },
@@ -273,17 +346,19 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
ledger2_accounts::SHARE_FUND, ledger2_accounts::TARGET_RECEIPTS,
"Трансляция паевого взноса из ЦПП «Цифровой Кошелёк» в членский взнос за пользование инфраструктурой" },
// 15. Запрос на возврат паевого: BLOCK SHARE_FUND_PAY (без Dr/Cr — внутри одного бухсчёта 80).
{ operations::wallet::REQUEST_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BLOCK,
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
// 15. Запрос на возврат паевого: TRANSFER SHARE_FUND_PAY → WITHDRAW_PENDING
// (без Dr/Cr — оба кошелька на счёте 80; резерв средств на время рассмотрения).
{ operations::wallet::REQUEST_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::TRANSFER,
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::WITHDRAW_PENDING,
0, 0,
"Блокировка паевого под запрос на возврат" },
"Резервирование паевого под запрос на возврат" },
// 16. Отклонение запроса на возврат: UNBLOCK SHARE_FUND_PAY (без Dr/Cr — зеркало REQUEST_WITHDRAW).
{ operations::wallet::DECLINE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::UNBLOCK,
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
// 16. Отклонение запроса на возврат: TRANSFER WITHDRAW_PENDING → SHARE_FUND_PAY
// (без Dr/Cr — зеркало REQUEST_WITHDRAW; возврат резерва пайщику).
{ operations::wallet::DECLINE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::TRANSFER,
ledger2_wallets::WITHDRAW_PENDING, ledger2_wallets::SHARE_FUND_PAY,
0, 0,
"Разблокировка паевого после отклонения запроса на возврат" },
"Снятие резерва паевого после отклонения запроса на возврат" },
// 17. Возврат из ЦПП «Благорост» в Цифровой Кошелёк: TRANSFER BLAGOROST_FUND → SHARE_FUND_PAY (без Dr/Cr — оба счёта 80, зеркало INVEST).
{ operations::capital::WITHDRAW_FROM_CAPITAL, processes::capital::WTHCAP, WalletOp::TRANSFER,
@@ -309,6 +384,72 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
0, 0,
"Конвертация сегмента: РИД → ЦПП «Благорост»" },
// 19a. Пополнение пула программных расходов: ISSUE PROGRAM_EXPENSE_POOL, без Dr/Cr.
// Совет выделяет часть свободных инвестиций программы под целевые расходы:
// деньги физически на 51 с момента взносов, здесь появляется кооперативный
// резерв-кошелёк, из которого шасси expense оплачивает СЗ. Паевые L3-кошельки
// пайщиков (w.cap.blago, счёт 80) не изменяются — права требования сохраняются.
{ operations::capital::PROGRAM_EXPENSE_TOPUP, processes::capital::PGEXP, WalletOp::ISSUE,
eosio::name{}, ledger2_wallets::PROGRAM_EXPENSE_POOL,
0, 0,
"Пополнение пула программных расходов ЦПП «Благорост»" },
// ----- Шасси расходов (o.exp.*) — вызываются из контракта expense -----
//
// Базовое состояние Благороста ДО расхода:
// - Деньги физически на 51 с момента o.wal.depcpl (Dr 51 / Cr 80).
// - 80 (паевой) наполнен; кошелёк w.cap.blago.
// - 08 пустой (`o.cap.invest` — TRANSFER без проводок).
//
// Принцип: расход не уменьшает паевой фонд (80). Меняется только форма актива
// 51 → 08 (банк уходит, появляется WIP-проект).
// 20. Выдача подотчётных из пула программных расходов: Dr 08 / Cr 51,
// TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD.
// Источник — КООПЕРАТИВНЫЙ пул расходов (пополняется o.cap.pgtop); личные
// L3-кошельки пайщиков (w.cap.blago) при оплате СЗ не трогаются — их паевые
// взносы в программе не уменьшаются. Деньги физически уходят пайщику (Cr 51),
// стоимость капитализируется в WIP (Dr 08). ADVANCE_HOLD фиксирует
// ответственность получателя аванса (USER_SHARED) до отчёта.
{ operations::expense::BLAGO_ADVANCE, processes::expense::PROPOSAL, WalletOp::TRANSFER,
ledger2_wallets::PROGRAM_EXPENSE_POOL, ledger2_wallets::ADVANCE_HOLD,
ledger2_accounts::NON_CURRENT_INVESTMENTS, ledger2_accounts::BANK_ACCOUNT,
"Выдача подотчётных из пула расходов ЦПП «Благорост»" },
// 21. Прямая оплата из пула программных расходов (DIRECT): Dr 08 / Cr 51,
// BURN PROGRAM_EXPENSE_POOL. Оплата организации по счёту; деньги уходят с 51,
// стоимость капитализируется в 08. Кошелёк-резерв не задействован.
{ operations::expense::BLAGO_DIRECT, processes::expense::PROPOSAL, WalletOp::BURN,
ledger2_wallets::PROGRAM_EXPENSE_POOL, eosio::name{},
ledger2_accounts::NON_CURRENT_INVESTMENTS, ledger2_accounts::BANK_ACCOUNT,
"Прямая оплата из пула расходов ЦПП «Благорост»" },
// 22. Закрытие подотчёта пайщика по отчёту: BURN ADVANCE_HOLD, БЕЗ бухпроводки.
// Проводка Dr 08 / Cr 51 уже сделана на BLAGO_ADVANCE при выдаче. При отчёте только
// снимается кошелёк-резерв пайщика — никакого canal 08/51 второй раз.
{ operations::expense::ADVANCE_REPORT, processes::expense::PROPOSAL, WalletOp::BURN,
ledger2_wallets::ADVANCE_HOLD, eosio::name{},
0, 0,
"Закрытие подотчёта пайщика по отчёту" },
// 23. Возврат неиспользованного подотчёта: Dr 51 / Cr 08,
// TRANSFER ADVANCE_HOLD → PROGRAM_EXPENSE_POOL.
// Зеркало BLAGO_ADVANCE: деньги возвращаются на 51, WIP-стоимость уменьшается,
// остаток снова доступен пулу расходов.
{ operations::expense::ADVANCE_RETURN, processes::expense::PROPOSAL, WalletOp::TRANSFER,
ledger2_wallets::ADVANCE_HOLD, ledger2_wallets::PROGRAM_EXPENSE_POOL,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::NON_CURRENT_INVESTMENTS,
"Возврат неиспользованного подотчёта в пул расходов" },
// 24. Доплата сверх подотчёта (перерасход): Dr 08 / Cr 51,
// TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD.
// Зеркало BLAGO_ADVANCE на сумму перерасхода. Контракт expense сразу за OVERSPEND
// вызывает ADVANCE_REPORT — две последовательные записи в одной транзакции `expense::overspendexp`.
{ operations::expense::OVERSPEND, processes::expense::PROPOSAL, WalletOp::TRANSFER,
ledger2_wallets::PROGRAM_EXPENSE_POOL, ledger2_wallets::ADVANCE_HOLD,
ledger2_accounts::NON_CURRENT_INVESTMENTS, ledger2_accounts::BANK_ACCOUNT,
"Доплата сверх подотчёта (перерасход)" },
// ----- Миграционные (o.mig.*) — вызываются только из migrate.cpp -----
// 15. Миграция: минимальный паевой: Dr 51 / Cr 80, ISSUE MIN_SHARE_FUND
@@ -340,7 +481,7 @@ static constexpr size_t OPERATION_REGISTRY_SIZE = sizeof(OPERATION_REGISTRY) / s
// оба существуют в `LEDGER2_ACCOUNT_MAP`.
// 4. Для TRANSFER: `wallet_from` ≠ `wallet_to`, оба ≠ 0.
// 5. Для ISSUE: `wallet_from` == 0 и `wallet_to` ≠ 0.
// 6. Для BLOCK / UNBLOCK / BURN / BURN_BLOCKED: `wallet_from` ≠ 0, `wallet_to` == 0.
// 6. Для BURN: `wallet_from` ≠ 0, `wallet_to` == 0.
// 7. Все id кошельков из записей существуют в `LEDGER2_WALLET_REGISTRY`.
namespace ledger2_registry_detail {
constexpr bool operation_codes_unique() {
@@ -384,11 +525,11 @@ namespace ledger2_registry_detail {
return true;
}
// Правило 6: BURN / BURN_BLOCKED — wallet_from required, wallet_to == 0 (ADR-003).
// Правило 6: BURN — wallet_from required, wallet_to == 0 (ADR-003).
constexpr bool burn_pattern_correct() {
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
const auto& e = OPERATION_REGISTRY[i];
if (e.wallet_op != WalletOp::BURN && e.wallet_op != WalletOp::BURN_BLOCKED) continue;
if (e.wallet_op != WalletOp::BURN) continue;
if (e.wallet_from.value == 0) return false;
if (e.wallet_to.value != 0) return false;
}
@@ -440,7 +581,7 @@ static_assert(ledger2_registry_detail::dr_ne_cr_when_posting(),
static_assert(ledger2_registry_detail::transfer_wallet_from_ne_to(),
"OPERATION_REGISTRY: TRANSFER с wallet_from == wallet_to или одним из них == 0");
static_assert(ledger2_registry_detail::burn_pattern_correct(),
"OPERATION_REGISTRY: BURN / BURN_BLOCKED требует wallet_from ≠ 0 и wallet_to == 0");
"OPERATION_REGISTRY: BURN требует wallet_from ≠ 0 и wallet_to == 0");
static_assert(ledger2_registry_detail::none_pattern_correct(),
"OPERATION_REGISTRY: NONE требует wallet_from == 0, wallet_to == 0 и обе проводки заполненными");
static_assert(ledger2_registry_detail::accounts_exist_in_map(),
@@ -491,3 +632,105 @@ inline constexpr const OperationAdjustmentEntry* find_adjustment(eosio::name ope
}
return nullptr;
}
// =====================================================================
// Наборы операций шасси расходов — фабричная настройка контракта expense.
// =====================================================================
//
// Контракт `expense` агностичен к программе-источнику: кошелёк-пул приходит в
// `createexp` параметром `source_wallet`, а ledger2-коды всех пяти операций
// жизненного цикла (аванс / прямая оплата / отчёт / возврат / перерасход)
// выводятся из этого кошелька через таблицу ниже — в коде expense нет ни
// одного захардкоженного operation_code.
//
// Подключение шасси к новому пулу (например, кошельку членских взносов
// кооперативного участка) = добавить 5 операций в OPERATION_REGISTRY и одну
// строку здесь. Контракт expense при этом не меняется.
struct ExpenseOperationSet {
eosio::name source_wallet; ///< пул-источник средств (COOPERATIVE-кошелёк)
eosio::name advance; ///< выдача аванса под отчёт (TRANSFER pool → подотчёт)
eosio::name direct; ///< прямая оплата организации по счёту (BURN pool)
eosio::name report; ///< закрытие подотчёта по отчёту (BURN подотчёта)
eosio::name refund; ///< возврат неиспользованного аванса (TRANSFER подотчёт → pool)
eosio::name overspend; ///< доплата при перерасходе (TRANSFER pool → подотчёт)
};
static constexpr ExpenseOperationSet EXPENSE_OPERATION_SETS[] = {
// Пул программных расходов ЦПП «Благорост» — source_wallet заполняет
// capital::createpgexp при создании СЗ через inline expense::createexp.
{ ledger2_wallets::PROGRAM_EXPENSE_POOL,
operations::expense::BLAGO_ADVANCE,
operations::expense::BLAGO_DIRECT,
operations::expense::ADVANCE_REPORT,
operations::expense::ADVANCE_RETURN,
operations::expense::OVERSPEND },
};
static constexpr size_t EXPENSE_OPERATION_SETS_SIZE =
sizeof(EXPENSE_OPERATION_SETS) / sizeof(EXPENSE_OPERATION_SETS[0]);
// Compile-time валидация наборов: каждый код существует в OPERATION_REGISTRY,
// тип wallet-операции и привязка кошельков соответствуют роли кода в наборе.
namespace ledger2_expense_sets_detail {
constexpr const OperationRegistryEntry* find_op(eosio::name code) {
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
if (OPERATION_REGISTRY[i].code == code) return &OPERATION_REGISTRY[i];
}
return nullptr;
}
constexpr bool source_wallets_unique() {
for (size_t i = 0; i < EXPENSE_OPERATION_SETS_SIZE; ++i) {
for (size_t j = i + 1; j < EXPENSE_OPERATION_SETS_SIZE; ++j) {
if (EXPENSE_OPERATION_SETS[i].source_wallet == EXPENSE_OPERATION_SETS[j].source_wallet) {
return false;
}
}
}
return true;
}
constexpr bool sets_consistent() {
for (size_t i = 0; i < EXPENSE_OPERATION_SETS_SIZE; ++i) {
const auto& s = EXPENSE_OPERATION_SETS[i];
const auto* adv = find_op(s.advance);
const auto* dir = find_op(s.direct);
const auto* rep = find_op(s.report);
const auto* ref = find_op(s.refund);
const auto* ovr = find_op(s.overspend);
if (!adv || !dir || !rep || !ref || !ovr) return false;
// Аванс: пул → кошелёк-подотчёт.
if (adv->wallet_op != WalletOp::TRANSFER || adv->wallet_from != s.source_wallet) return false;
const eosio::name hold = adv->wallet_to;
// Прямая оплата: сжигание с пула (подотчёт не задействован).
if (dir->wallet_op != WalletOp::BURN || dir->wallet_from != s.source_wallet) return false;
// Отчёт: сжигание подотчёта.
if (rep->wallet_op != WalletOp::BURN || rep->wallet_from != hold) return false;
// Возврат: подотчёт → пул.
if (ref->wallet_op != WalletOp::TRANSFER ||
ref->wallet_from != hold || ref->wallet_to != s.source_wallet) return false;
// Перерасход: пул → подотчёт (зеркало аванса на сумму доплаты).
if (ovr->wallet_op != WalletOp::TRANSFER ||
ovr->wallet_from != s.source_wallet || ovr->wallet_to != hold) return false;
}
return true;
}
} // namespace ledger2_expense_sets_detail
static_assert(ledger2_expense_sets_detail::source_wallets_unique(),
"EXPENSE_OPERATION_SETS: source_wallet должен быть уникален");
static_assert(ledger2_expense_sets_detail::sets_consistent(),
"EXPENSE_OPERATION_SETS: набор операций не согласован с OPERATION_REGISTRY "
"(коды/типы wallet-операций/привязка кошельков)");
/**
* @brief Поиск набора операций шасси расходов по кошельку-источнику.
*/
inline const ExpenseOperationSet* find_expense_operation_set(eosio::name source_wallet) {
for (size_t i = 0; i < EXPENSE_OPERATION_SETS_SIZE; ++i) {
if (EXPENSE_OPERATION_SETS[i].source_wallet == source_wallet) {
return &EXPENSE_OPERATION_SETS[i];
}
}
return nullptr;
}
@@ -35,7 +35,8 @@ namespace processes {
// registrator
namespace registrator {
inline constexpr eosio::name ACCEPT = "p.reg.accept"_n; ///< Приём пайщика в кооператив (o.reg.payent + o.reg.putmin).
inline constexpr eosio::name ACCEPT = "p.reg.accept"_n; ///< Приём пайщика в кооператив (o.reg.payent + o.reg.putmin; для потока через совет — o.reg.inpay + o.reg.setmin + o.reg.setent).
inline constexpr eosio::name REFUND = "p.reg.refund"_n; ///< Возврат регистрационного взноса при отказе совета (o.reg.refund). Отдельный процесс: приём взноса прерывается, начинается возврат.
}
// wallet
@@ -53,6 +54,7 @@ namespace processes {
inline constexpr eosio::name PROPERTY = "p.cap.prop"_n; ///< Приём имущественного паевого взноса (одноактовый).
inline constexpr eosio::name PREIMP = "p.cap.preimp"_n; ///< Первичный учёт РИД-взноса до перехода на электронный учёт (одноактовый, anchor = preimp register hash).
inline constexpr eosio::name WTHCAP = "p.cap.wthcap"_n; ///< Возврат паевого из ЦПП «Благорост» в кошелёк пайщика (одноактовый).
inline constexpr eosio::name PGEXP = "p.cap.pgexp"_n; ///< Пул программных расходов: пополнение из инвестиций программы (o.cap.pgtop, одноактовый).
}
// marketplace
@@ -60,6 +62,11 @@ namespace processes {
inline constexpr eosio::name REQUEST = "p.mkt.reqst"_n; ///< Цикл запроса маркетплейса (o.mkt.supply + o.mkt.recv).
}
// expense — шасси расходов (СЗ → авторизация → платёж → отчёт → закрытие)
namespace expense {
inline constexpr eosio::name PROPOSAL = "p.exp.expns"_n; ///< Цикл расхода по СЗ: o.exp.blgadv|blgdir (платёж) + опц. o.exp.over (перерасход) + o.exp.advrpt (отчёт ADVANCE) + опц. o.exp.advret (возврат). Анкер процесса — proposal_hash.
}
// soviet
namespace soviet {
inline constexpr eosio::name AXN_CONVERT = "p.sov.axncnv"_n; ///< Конвертация паевого RUB → делегатский ЧВ (одноактовый).
@@ -27,7 +27,7 @@
* w.mkt.* Маркетплейс (выплаты поставщикам)
*
* Sentinel `eosio::name{}` (пустое имя, value=0) «кошелёк вне системы»
* для ISSUE (нет wallet_from) и для BURN/BLOCK/UNBLOCK (нет wallet_to).
* для ISSUE (нет wallet_from) и для BURN (нет wallet_to).
*
* При первом ISSUE/TRANSFER кошелёк создаётся автоматически по записи
* из WALLET_REGISTRY. При обнулении available+blocked запись удаляется.
@@ -46,11 +46,13 @@ struct ledger2_wallets {
// wallet — паевой фонд + возвраты + ЦК
static constexpr eosio::name SHARE_FUND_PAY = "w.wal.share"_n; ///< Паевой взнос пайщика (USER_SHARED)
static constexpr eosio::name CK_MEMBER = "w.wal.member"_n; ///< ЦК — членская часть пайщика (USER_SHARED)
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< DEPRECATED 2026-05-21: после переключения o.wal.wthcpl с TRANSFER на BURN_BLOCKED кошелёк больше не получает новых средств. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< DEPRECATED 2026-05-21: исторический sink возвратов. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
static constexpr eosio::name WITHDRAW_PENDING = "w.wal.wpend"_n; ///< Резерв паевого под заявку на возврат (COOPERATIVE-пул). o.wal.wthreq переводит сюда с w.wal.share, o.wal.wthdec возвращает обратно, o.wal.wthcpl сжигает отсюда. Заменил механику blocked/BLOCK/UNBLOCK 2026-05-24.
// registrator — минимальный паевой + вступительные
// registrator — минимальный паевой + вступительные + суспенс
static constexpr eosio::name MIN_SHARE_FUND = "w.reg.minshr"_n; ///< Минимальный паевой взнос пайщика (USER_SHARED, без сверки соглашений)
static constexpr eosio::name ENTRANCE_FEES = "w.reg.entry"_n; ///< Вступительные взносы (Cr 86, COOPERATIVE)
static constexpr eosio::name REGISTRATION_PENDING = "w.reg.pend"_n; ///< Регистрационный взнос в ожидании решения совета (суспенс счёта 76, USER_SHARED, без сверки соглашений — кандидат ещё не член). o.reg.inpay → сюда (Dr 51/Cr 76); o.reg.setmin/setent уносят на 80/86; o.reg.refund сжигает (Dr 76/Cr 51).
// soviet — членские (инфраструктура) + делегатские + хоз.расходы + использованные паевые
static constexpr eosio::name INFRA_FEES = "w.sov.infra"_n; ///< Членские взносы за инфраструктуру кооп. платформы (COOPERATIVE)
@@ -63,9 +65,16 @@ struct ledger2_wallets {
static constexpr eosio::name BLAGOROST_FUND = "w.cap.blago"_n; ///< Благорост — единый агрегированный кошелёк программы (USER_SHARED; ADR-009)
static constexpr eosio::name GENERATOR_FUND = "w.cap.gen"_n; ///< Генератор — единый агрегированный кошелёк программы (COOPERATIVE — кооперативный пул, без L3-разреза по пайщику; L3-разрез из ADR-009 отменён из-за несовместимости с CRPS-перераспределением, см. wallets.hpp:107)
static constexpr eosio::name PREIMP_FUND = "w.cap.preimp"_n; ///< Первичный учёт РИД-взносов до перехода на электронный учёт (USER_SHARED; o.cap.preimp / o.cap.drppre)
static constexpr eosio::name PROGRAM_EXPENSE_POOL = "w.cap.pgexp"_n; ///< Пул программных расходов ЦПП «Благорост» (COOPERATIVE) — кооперативный кошелёк, из которого шасси expense оплачивает СЗ; пополняется topupprogexp (o.cap.pgtop), паевые L3-кошельки пайщиков (w.cap.blago) при расходах не трогаются
// marketplace — выплаты
static constexpr eosio::name SUPPLIER_PAYMENTS = "w.mkt.payout"_n; ///< Выплаты поставщикам (sink RECEIVE_CONFIRM, COOPERATIVE)
// expense — шасси расходов (подотчёт пайщика, USER_SHARED)
// Зеркало паттерна w.wal.wpend: кошелёк-резерв на пайщике-получателе ADVANCE-механики.
// На момент выдачи аванса фиксирует ответственность пайщика; на отчёте — BURN без новой бухпроводки
// (canal Дт 08 / Кт 51 уже сделан на o.exp.blgadv).
static constexpr eosio::name ADVANCE_HOLD = "w.exp.adv"_n; ///< Подотчётные средства пайщика (USER_SHARED; резерв при ADVANCE-механике шасси расходов)
};
/**
@@ -94,15 +103,17 @@ struct Ledger2WalletMeta {
WalletKind kind;
};
inline constexpr std::array<Ledger2WalletMeta, 14> LEDGER2_WALLET_REGISTRY = {{
// USER_SHARED (5) — L3-разрез по пайщику
inline constexpr std::array<Ledger2WalletMeta, 18> LEDGER2_WALLET_REGISTRY = {{
// USER_SHARED (7) — L3-разрез по пайщику
{ ledger2_wallets::MIN_SHARE_FUND, "Минимальный паевой взнос", WalletKind::USER_SHARED },
{ ledger2_wallets::SHARE_FUND_PAY, "Паевой взнос пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::CK_MEMBER, "ЦК — членская часть пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::BLAGOROST_FUND, "ЦПП «Благорост» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::PREIMP_FUND, "Первичный учёт РИД-взносов до перехода на электронный учёт", WalletKind::USER_SHARED },
{ ledger2_wallets::ADVANCE_HOLD, "Подотчётные средства пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::REGISTRATION_PENDING, "Регистрационный взнос в ожидании решения совета", WalletKind::USER_SHARED },
// COOPERATIVE (9) — единый кооперативный баланс, без L3
// COOPERATIVE (10) — единый кооперативный баланс, без L3 (Generator + 9 «единых пулов»)
// GENERATOR_FUND переведён сюда из USER_SHARED (см. wallets.hpp:64) —
// CRPS-распределение между сегментами проекта не поддерживает per-user
// компенсирующие TRANSFER на approvecmmt, поэтому L3-проверка walletop
@@ -110,12 +121,14 @@ inline constexpr std::array<Ledger2WalletMeta, 14> LEDGER2_WALLET_REGISTRY = {{
{ ledger2_wallets::GENERATOR_FUND, "ЦПП «Генератор» — единый кошелёк программы", WalletKind::COOPERATIVE },
{ ledger2_wallets::ENTRANCE_FEES, "Вступительные взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам (deprecated, не используется в новых операциях)", WalletKind::COOPERATIVE },
{ ledger2_wallets::WITHDRAW_PENDING, "Резерв паевого под заявку на возврат", WalletKind::COOPERATIVE },
{ ledger2_wallets::INFRA_FEES, "Членские взносы за инфраструктуру кооп. платформы", WalletKind::COOPERATIVE },
{ ledger2_wallets::DELEGATE_FEES, "Делегатские членские взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::SOV_EXPENSES, "Хозяйственные расходы из числа целевого финансирования", WalletKind::COOPERATIVE },
{ ledger2_wallets::MIN_SHARE_USED, "Использованные минимальные паевые взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::LOAN_ISSUED, "Выданные пайщикам беспроцентные займы", WalletKind::COOPERATIVE },
{ ledger2_wallets::SUPPLIER_PAYMENTS, "Выплаты поставщикам", WalletKind::COOPERATIVE },
{ ledger2_wallets::PROGRAM_EXPENSE_POOL, "Пул программных расходов ЦПП «Благорост»", WalletKind::COOPERATIVE },
}};
static constexpr size_t LEDGER2_WALLET_REGISTRY_SIZE = LEDGER2_WALLET_REGISTRY.size();
@@ -228,13 +241,46 @@ struct Ledger2WalletProgramMapping {
uint64_t required_program_id; // 0 = исключение (без проверки)
};
inline constexpr std::array<Ledger2WalletProgramMapping, 6> LEDGER2_USER_SHARED_PROGRAM_MAPPING = {{
inline constexpr std::array<Ledger2WalletProgramMapping, 8> LEDGER2_USER_SHARED_PROGRAM_MAPPING = {{
{ ledger2_wallets::MIN_SHARE_FUND, 0 /* w.reg.minshr — без проверки */ },
{ ledger2_wallets::SHARE_FUND_PAY, 1 /* ЦК */ },
{ ledger2_wallets::CK_MEMBER, 1 /* ЦК */ },
{ ledger2_wallets::BLAGOROST_FUND, 4 /* Благорост */ },
{ ledger2_wallets::GENERATOR_FUND, 3 /* Генератор */ },
{ ledger2_wallets::PREIMP_FUND, 0 /* w.cap.preimp — РИД-учёт до перехода на электронный учёт, без проверки */ },
{ ledger2_wallets::ADVANCE_HOLD, 0 /* w.exp.adv — подотчёт пайщика по СЗ; программа-источник проверена контрактом expense, повторная gate не нужна */ },
{ ledger2_wallets::REGISTRATION_PENDING, 0 /* w.reg.pend — кандидат ещё не член, соглашения нет, без проверки */ },
}};
/**
* @brief Сет «боевых» (паевых) кошельков пайщика, возвращаемых при выходе из
* кооператива (заявление registry 200 одобрение совета `confirmexit`).
*
* Единый источник истины для суммы возврата. При одобрении выхода советом
* `registrator::confirmexit` обходит этот сет в цикле, аккумулирует доступный
* L3-баланс пайщика по каждому кошельку (>0), консолидирует на главный паевой
* (`w.wal.share`) и ставит всю сумму на возврат единым платежом. Тот же сет
* генерируется в cooptypes (`gen:from-cpp` `wallets.generated.ts`) и
* используется backend-preview, поэтому расчёт на фронте всегда совпадает с
* тем, что реально вернёт контракт.
*
* Состав только паевые/возвратные USER_SHARED-кошельки:
* w.reg.minshr минимальный паевой взнос;
* w.wal.share целевой паевой взнос (ЦК);
* w.cap.blago паевой взнос в ЦПП «Благорост».
*
* НЕ входят: w.wal.member (членский невозвратный), w.exp.adv (подотчёт под
* расход), w.cap.gen (Генератор COOPERATIVE, без L3-разреза по пайщику),
* w.cap.preimp (пред-импорт-учёт РИД).
*
* Каждому не-главному кошельку сета должна соответствовать операция переноса
* на `w.wal.share` в `Registrator::consolidate_share_to_main` (exit_helpers.hpp)
* иначе runtime упадёт с явным сообщением (защита от тихой потери средств).
*/
inline constexpr std::array<eosio::name, 3> LEDGER2_EXIT_REFUND_WALLETS = {{
ledger2_wallets::MIN_SHARE_FUND,
ledger2_wallets::SHARE_FUND_PAY,
ledger2_wallets::BLAGOROST_FUND,
}};
/**
+9 -1
View File
@@ -50,6 +50,10 @@ namespace Names {
// Коллбэки для расходов
constexpr eosio::name AUTHORIZE_EXPENSE = "capauthexpns"_n;
constexpr eosio::name DECLINE_EXPENSE = "capdeclexpns"_n;
// Программные расходы обслуживаются шасси expense; capital видит только
// инициирование (createpgexp) и этот callback завершения flow.
constexpr eosio::name ON_PROGRAM_EXPENSE_DONE = "onpgexpdone"_n;
// Коллбэки для долгов
constexpr eosio::name AUTHORIZE_DEBT = "debtauthcnfr"_n;
@@ -80,10 +84,13 @@ namespace Names {
// Внешние действия в других контрактах
constexpr eosio::name CREATE_OUTPAY = "createoutpay"_n;
constexpr eosio::name OPEN_PROGRAM_WALLET = "openprogwall"_n;
// Действия для отправки в soviet
constexpr eosio::name CREATE_APPROVAL = "createapprv"_n;
constexpr eosio::name CREATE_AGENDA = "createagenda"_n;
// Шасси expense
constexpr eosio::name CREATE_EXPENSE_PROPOSAL = "createexp"_n;
}
namespace SovietActions {
@@ -120,6 +127,7 @@ namespace Names {
constexpr eosio::name CREATE_INVESTMENT = "createinvest"_n; // акцепт инвестиции
constexpr eosio::name CREATE_PROGRAM_INVESTMENT = "createpinv"_n; // акцепт программной инвестиции
constexpr eosio::name CREATE_EXPENSE = "createexpnse"_n; // акцепт расхода
constexpr eosio::name CREATE_PROGRAM_EXPENSE = "createpgexp"_n; // акцепт программного расхода
constexpr eosio::name CREATE_WITHDRAW_1 = "createwthd1"_n; // акцепт возврата из задания
constexpr eosio::name CREATE_WITHDRAW_2 = "createwthd2"_n; // акцепт возврата из проекта
constexpr eosio::name CREATE_WITHDRAW_3 = "createwthd3"_n; // акцепт возврата из программы
@@ -19,6 +19,12 @@ using createapprv_interface = void(CREATEAPPRV_SIGNATURE);
#define DECLINE_CALLBACK_SIGNATURE name coopname, checksum256 hash, std::string reason
using decline_callback_interface = void(DECLINE_CALLBACK_SIGNATURE);
// Общая ветка отказа решения совета: отправка коллбэка отклонения инициатору
// (если решение пришло из внешнего процесса) + удаление решения из таблицы.
// Используется при просрочке (cancelexprd) и при достижении отрицательного
// консенсуса голосов «против» (voteagainst). Определение — cancelexprd.cpp.
void decline_and_erase_decision(eosio::name coopname, uint64_t decision_id, const std::string &reason);
#define AUTHORIZE_CALLBACK_SIGNATURE name coopname, checksum256 hash, document2 authorization
using authorize_callback_interface = void(AUTHORIZE_CALLBACK_SIGNATURE);
@@ -17,6 +17,7 @@
#include "table_registrator_verification.hpp"
#include "table_registrator_candidates.hpp"
#include "table_registrator_candidates_legacy.hpp"
#include "table_registrator_exits.hpp"
// coops (soviet)
#include "coops_access_helpers.hpp"
@@ -12,3 +12,18 @@ account get_account_or_fail(eosio::name username) {
return *account_row;
}
// Возврат аккаунта к состоянию «можно подать заявку заново»: снимаем карточку
// участника (type) и сторейджи, которые проставил reguser. Сам аккаунт (username,
// ключи) сохраняется — повторная подача идёт на том же аккаунте. Вызывается при
// терминальном закрытии кандидата (отказ совета + возврат взноса): после этого
// reguser снова проходит проверку type=="" и принимает новое заявление.
inline void reset_account_card(eosio::name username) {
accounts_index accounts(_registrator, _registrator.value);
auto account_row = accounts.find(username.value);
if (account_row == accounts.end()) return;
accounts.modify(account_row, _registrator, [&](auto &a) {
a.type = ""_n;
a.storages.clear();
});
}
@@ -0,0 +1,71 @@
#pragma once
#include <optional>
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include "../consts.hpp"
#include "document_core.hpp"
namespace Registrator {
using namespace eosio;
/**
* @ingroup public_tables
* @ingroup public_registrator_tables
* @par table: exits (registrator)
*
* @brief Заявление пайщика на выход из кооператива и сопровождающий его
* процесс возврата паевого взноса.
*
* Жизненный цикл (зеркало вступления reguserconfirmreg и возврата
* wallet::createwthdauthwthdcompletewthd):
* - exitcoop запись со статусом `pending`, повестка совета `leavecoop`;
* - confirmexit совет одобрил: статус `authorized`, консолидация
* минимального паевого на главный (o.reg.mvmin), резерв
* суммы возврата (o.wal.wthreq) и исходящий платёж в gateway;
* - completexit кассир подтвердил выплату: проводка Дт80/Кт51
* (o.wal.wthcpl), пайщик удаляется, аккаунт блокируется,
* запись стирается;
* - declinexit отказ совета или отклонение платежа: снятие резерва
* (o.wal.wthdec, если он был сделан) и стирание записи.
*
* `quantity` итоговая сумма возврата, вычисляется контрактом в момент
* одобрения советом (минимальный + целевой паевой пайщика по L3-балансам
* ledger2), а не доверяется клиенту.
*/
struct [[eosio::table, eosio::contract(REGISTRATOR)]] exit {
name username;
name coopname;
name status; ///< pending | authorized
time_point_sec created_at;
document2 statement; ///< заявление о выходе (registry 200)
document2 approved_statement; ///< решение совета о выходе (авторизация)
checksum256 exit_hash;
asset quantity; ///< итоговая сумма возврата (заполняется на confirmexit)
uint64_t primary_key() const { return username.value; }
checksum256 by_hash() const { return exit_hash; }
};
typedef multi_index<
"exits"_n, exit,
indexed_by<"byhash"_n, const_mem_fun<exit, checksum256, &exit::by_hash>>>
exits_index;
inline std::optional<exit> get_exit_by_hash(name coopname, const checksum256 &hash) {
exits_index primary_index(_registrator, coopname.value);
auto secondary_index = primary_index.get_index<"byhash"_n>();
auto itr = secondary_index.find(hash);
if (itr == secondary_index.end()) {
return std::nullopt;
}
return *itr;
}
} // namespace Registrator
@@ -8,10 +8,10 @@
# Источники правды в коде:
# • cpp/registrator/registrator.hpp — actions
# • cpp/registrator/src/user/{reguser,confirmpay,confirmreg,declinepay,declinereg}.cpp
# • cpp/lib/core/ledger2/operations.hpp — OPERATION_REGISTRY (o.reg.payent, o.reg.putmin)
# • cpp/lib/core/ledger2/operations.hpp — OPERATION_REGISTRY (o.reg.inpay, o.reg.setmin, o.reg.setent)
# • cpp/lib/core/ledger2/processes.hpp — processes::registrator::ACCEPT
# • cpp/lib/core/ledger2/wallets.hpp — w.reg.entry, w.reg.minshr
# • cpp/lib/core/ledger2/accounts.hpp — 51 / 80 / 86
# • cpp/lib/core/ledger2/wallets.hpp — w.reg.pend, w.reg.entry, w.reg.minshr
# • cpp/lib/core/ledger2/accounts.hpp — 51 / 76 / 80 / 86
# ─────────────────────────────────────────────────────────────────────────────
# ── Секция 1. Паспорт ───────────────────────────────────────────────────────
@@ -51,8 +51,9 @@ actions:
role: progress
purpose: >
Кассир подтверждает зачисление взносов: карточка переходит в `payed`,
повестка `joincoop` с колбэками отправлена в совет. Учётных проводок
ещё нет.
повестка `joincoop` с колбэками отправлена в совет. Деньги получены,
но взнос ещё не признан — он ставится на расчёты с пайщиком
(Дт 51 / Кт 76) и ждёт решения совета.
- name: registrator::declinepay
human: Отклонить оплату
@@ -60,25 +61,27 @@ actions:
role: reject
purpose: >
Кассир отклоняет платёж: запись кандидата удаляется, операции в книге
Ledger2 не создаются. Кандидат может подать заявление повторно.
Ledger2 не создаются (деньги ещё не были приняты). Кандидат может
подать заявление повторно.
- name: registrator::confirmreg
human: Утвердить советом
actor: soviet
role: closer
purpose: >
Совет утверждает приём: статус карточки → `active`, в книге Ledger2
одновременно проводятся `o.reg.payent` и `o.reg.putmin`, кандидат
добавлен в список участников.
Совет утверждает приём: статус карточки → `active`, взнос признаётся —
сумма с расчётов с пайщиком (счёт 76) переносится на паевой фонд (80)
и целевое финансирование (86) операциями `o.reg.setmin` и
`o.reg.setent`. Кандидат добавлен в список участников.
- name: registrator::declinereg
human: Отклонить советом
actor: soviet
role: reject
purpose: >
Совет отказывает в приёме: запись кандидата удаляется, операции
в книге Ledger2 не создаются. Возврат внесённых сумм оформляется
отдельно.
Совет отказывает в приёме: процесс приёма взноса прерывается и
начинается отдельный процесс возврата «Возврат регистрационного
взноса» (p.reg.refund) — полученная сумма возвращается кандидату.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
# Составная сущность: статус отслеживается в таблице `candidates` (created / payed),
@@ -99,23 +102,36 @@ states:
- name: payed
human: Взносы оплачены
description: >
Деньги получены, карточка в `payed`, в совет ушла повестка
`joincoop`. Проводок ещё нет — они сработают на решении совета.
Деньги получены, карточка в `payed`, в совет ушла повестка `joincoop`.
Полученная сумма стоит на расчётах с пайщиком (Дт 51 / Кт 76) и ждёт
решения совета: при одобрении — признаётся взносом, при отказе —
возвращается.
kind: normal
- name: active
human: Пайщик активен
description: >
`accounts.status` = `active`, карточка кандидата удалена, в книге
Ledger2 проведены `o.reg.payent` и `o.reg.putmin`. С этого момента
доступны все остальные кооперативные процессы.
Ledger2 взнос признан — сумма перенесена со счёта 76 на 80 и 86
операциями `o.reg.setmin` и `o.reg.setent`. С этого момента доступны
все остальные кооперативные процессы.
kind: final
- name: removed
human: Отклонено
description: >
Запись кандидата удалена, проводок в книге Ledger2 не было.
Если взносы успели поступить, возврат оформляется вне процесса.
Запись кандидата удалена на этапе оплаты (платёж не поступил/отклонён) —
деньги не принимались, проводок не было.
kind: virtual
virtual: true
- name: refunding
human: Возврат в процессе
description: >
Совет отказал кандидату, чьи деньги уже стоят на счёте 76. Процесс
приёма прерывается, и управление переходит в отдельный процесс
«Возврат регистрационного взноса» (p.reg.refund), где касса проводит
возврат и подтверждает его.
kind: virtual
virtual: true
@@ -133,6 +149,9 @@ transitions:
to: payed
action: registrator::confirmpay
actor: gateway_operator
ledger_code: p.reg.accept
operations:
- o.reg.inpay
guards:
- Кассир подтвердил зачисление суммы = initial + minimum.
@@ -149,17 +168,22 @@ transitions:
actor: soviet
ledger_code: p.reg.accept
operations:
- o.reg.payent
- o.reg.putmin
- o.reg.setmin
- o.reg.setent
guards:
- Совет принял положительное решение по повестке.
- from: payed
to: removed
to: refunding
action: registrator::declinereg
actor: soviet
operations: []
guards:
- Совет принял отрицательное решение по повестке.
- >
Проводок здесь нет: процесс приёма прерывается и управление переходит
в отдельный процесс «Возврат регистрационного взноса» (p.reg.refund),
где возврат проводится на шаге подтверждения кассой (o.reg.refund).
# ── Секция 4. Сценарий ──────────────────────────────────────────────────────
scenario:
@@ -190,33 +214,37 @@ scenario:
actor: gateway_operator
action: registrator::confirmpay
description: >
Деньги пришли — карточка движется к совету.
Деньги пришли — встают на расчёты с пайщиком, карточка движется к совету.
Кандидат оплачивает счёт (банковский перевод или криптоплатёж).
Кассир, получив подтверждение зачисления от платёжной системы,
вызывает `confirmpay`. Контракт переводит кандидата в статус
`payed` и создаёт повестку в совете с колбэками
`confirmreg` / `declinereg`. Учётных проводок ещё нет.
`payed`, создаёт повестку в совете с колбэками
`confirmreg` / `declinereg` и ставит полученную сумму на учёт
операцией `o.reg.inpay` (Дт 51 / Кт 76) — деньги получены, но
взнос ещё не признан и ждёт решения совета.
pre:
- Кандидат в статусе `created`.
- На расчётный счёт получена сумма initial + minimum.
post:
- Кандидат в статусе `payed`.
- В совете открыта повестка `joincoop`.
- В книге Ledger2 проведена o.reg.inpay (Дт 51 / Кт 76).
- step: 3
title: Утверждение советом
actor: soviet
action: registrator::confirmreg
description: >
Совет утверждает приём — два взноса встают на учёт, кандидат становится пайщиком.
Совет утверждает приём — взнос признаётся, кандидат становится пайщиком.
Совет рассматривает повестку `joincoop`. При положительном
решении контракт `confirmreg`: (1) меняет `accounts.status`
на `active`; (2) добавляет нового пайщика в список участников
совета (`soviet::addpartcpnt`); (3) применяет в книге Ledger2
две операции — `o.reg.payent` (вступительный взнос, Дт 51 / Кт 86)
и `o.reg.putmin` (минимальный паевой взнос, Дт 51 / Кт 80).
совета (`soviet::addpartcpnt`); (3) признаёт взнос — переносит
сумму с расчётов с пайщиком (76) на паевой фонд и целевое
финансирование двумя операциями: `o.reg.setmin` (минимальный
паевой, Дт 76 / Кт 80) и `o.reg.setent` (вступительный, Дт 76 / Кт 86).
Запись кандидата удаляется.
pre:
- Кандидат в статусе `payed`.
@@ -224,7 +252,7 @@ scenario:
post:
- accounts.status = `active` — пайщик стал активным членом кооператива.
- Пайщик добавлен в soviet::participants.
- В книге Ledger2 проведены o.reg.payent + o.reg.putmin.
- В книге Ledger2 проведены o.reg.setmin + o.reg.setent.
- Запись кандидата удалена из таблицы candidates.
alternatives:
@@ -236,8 +264,8 @@ scenario:
Платёж не прошёл или был отменён.
Кассир вызывает `declinepay`, кандидат удаляется, операции
в книге Ledger2 не создаются. Кандидат может повторить подачу
заявления.
в книге Ledger2 не создаются (деньги ещё не приняты). Кандидат
может повторить подачу заявления.
- branch: Отказ совета
at_step: 3
@@ -246,9 +274,11 @@ scenario:
description: >
Совет отказывает кандидату.
Запись кандидата удаляется, совершённый платёж подлежит возврату
озврат оформляется вне этого процесса). Операции в книге Ledger2
не создаются.
Процесс приёма взноса прерывается и начинается отдельный процесс
«Возврат регистрационного взноса» (p.reg.refund): кандидат переводится
в состояние возврата, кассе поручается вернуть деньги. Сама проводка
Дт 76 / Кт 51 и удаление кандидата происходят там — на шаге, когда
касса фактически проведёт возврат и подтвердит его.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
documents:
@@ -265,39 +295,72 @@ documents:
stored_in: "(authorization — в параметре действия, не хранится в candidates после удаления записи)"
# ── Секция 6. Операции ──────────────────────────────────────────────────────
# Процесс мульти-операционный: две записи OPERATION_REGISTRY с process_type = p.reg.accept.
# Обе срабатывают на закрывающем действии confirmreg.
# Процесс мульти-операционный: три записи OPERATION_REGISTRY с process_type = p.reg.accept.
# Двухфазный учёт: на приёме денег (confirmpay) сумма ставится на расчёты с
# пайщиком (76), на одобрении совета (confirmreg) — признаётся взносом (80/86).
#
# ПЕРЕХОДНЫЙ ПЕРИОД (до 30.07.2026): кандидаты, чей платёж был принят ДО релиза
# двухфазного учёта, не имеют суспенса на счёте 76. Для них одобрение совета
# проводится по старому одношаговому пути — прямой ISSUE Дт 51 / Кт 80
# (o.reg.putmin) и Дт 51 / Кт 86 (o.reg.payent), без переноса с 76. Признак —
# отсутствие баланса кандидата на w.reg.pend (см. registration_migration.hpp).
operations:
- ledger_code: o.reg.payent
human_name: Вступительный взнос пайщика
- ledger_code: o.reg.inpay
human_name: Приём регистрационного взноса в ожидание решения совета
wallet_op: ISSUE
wallet_from: ''
wallet_to: w.reg.entry
wallet_to: w.reg.pend
debit: 51
credit: 86
amount_ref: candidate.initial
triggered_by: registrator::confirmreg
credit: 76
amount_ref: candidate.initial + candidate.minimum
triggered_by: registrator::confirmpay
description: >
Постановка вступительного взноса на учёт. Сумма `candidate.initial`
зачисляется на кошелёк «Вступительные взносы» (w.reg.entry); двойная
запись Дт 51 / Кт 86 — целевые поступления в кооперативный фонд.
Постановка полученного регистрационного взноса на учёт. Сумма
«вступительный + минимальный паевой» зачисляется на кошелёк
«Регистрационный взнос в ожидании решения совета» (w.reg.pend);
двойная запись Дт 51 / Кт 76 — деньги на расчётном счёте, обязательство
перед пайщиком на счёте расчётов. Признание или возврат — позже.
- ledger_code: o.reg.putmin
human_name: Минимальный паевой взнос при регистрации
wallet_op: ISSUE
wallet_from: ''
- ledger_code: o.reg.setmin
human_name: Зачисление минимального паевого взноса по решению совета
wallet_op: TRANSFER
wallet_from: w.reg.pend
wallet_to: w.reg.minshr
debit: 51
debit: 76
credit: 80
amount_ref: candidate.minimum
triggered_by: registrator::confirmreg
description: >
Парная операция к `o.reg.payent`. Сумма `candidate.minimum`
появляется на кошельке «Минимальный паевой взнос» (w.reg.minshr);
двойная запись Дт 51 / Кт 80 — стартовый вклад в паевой фонд.
Признание минимального паевого взноса по решению совета. Сумма
`candidate.minimum` переносится с кошелька ожидания (w.reg.pend) на
«Минимальный паевой взнос» (w.reg.minshr); двойная запись Дт 76 / Кт 80 —
стартовый вклад в паевой фонд.
- ledger_code: o.reg.setent
human_name: Зачисление вступительного взноса по решению совета
wallet_op: TRANSFER
wallet_from: w.reg.pend
wallet_to: w.reg.entry
debit: 76
credit: 86
amount_ref: candidate.initial
triggered_by: registrator::confirmreg
description: >
Признание вступительного взноса по решению совета. Сумма
`candidate.initial` переносится с кошелька ожидания (w.reg.pend) на
«Вступительные взносы» (w.reg.entry); двойная запись Дт 76 / Кт 86 —
целевые поступления в кооперативный фонд.
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
related:
- process_type: p.reg.refund
id: public_registrator_refund_process
relation: triggers
note: >
При отрицательном решении совета процесс приёма прерывается и
запускается «Возврат регистрационного взноса» (p.reg.refund): сумма,
стоящая на расчётах с пайщиком (счёт 76), возвращается кандидату.
- process_type: p.wal.depo
id: public_wallet_deposit_process
relation: triggers
@@ -0,0 +1,213 @@
# ─────────────────────────────────────────────────────────────────────────────
# Стандарт «Возврат регистрационного взноса» — процесс возврата кандидату
# полученного регистрационного взноса при отказе совета в приёме.
#
# Это самостоятельный процесс: приём пайщика (p.reg.accept) прерывается на
# решении совета, и вместо признания взноса начинается его возврат. Возврат
# идёт в два шага: совет выносит отказ и поручает кассе вернуть деньги, а
# списание со счёта происходит только после того, как касса фактически проведёт
# исходящий перевод кандидату и подтвердит это. До подтверждения деньги стоят
# на расчётах с пайщиком (счёт 76).
#
# Источники правды в коде:
# • cpp/registrator/registrator.hpp — действия declinereg / refundpay
# • cpp/registrator/src/user/declinereg.cpp — отказ совета, создание исходящего возврата
# • cpp/registrator/src/user/refundpay.cpp — подтверждение возврата кассой, проводка и закрытие
# • cpp/registrator/src/user/declinerfnd.cpp — страж: отмена возврата невозможна
# • cpp/lib/core/ledger2/operations.hpp — OPERATION_REGISTRY (o.reg.refund)
# • cpp/lib/core/ledger2/processes.hpp — processes::registrator::REFUND
# • cpp/lib/core/ledger2/wallets.hpp — w.reg.pend
# • cpp/lib/core/ledger2/accounts.hpp — 51 / 76
# ─────────────────────────────────────────────────────────────────────────────
# ── Секция 1. Паспорт ───────────────────────────────────────────────────────
process_type: p.reg.refund
id: public_registrator_refund_process
title: Возврат регистрационного взноса
slug: refund
status: proposed
contract: registrator
summary: >
Когда совет отказывает кандидату в приёме, полученный регистрационный
взнос возвращается ему: совет поручает возврат, касса проводит перевод и
подтверждает его, после чего запись кандидата закрывается.
purpose: >
«Возврат регистрационного взноса» — терминальный процесс приёма пайщика
по отрицательному решению. Полученные кассой деньги стоят на расчётах
с пайщиком (счёт 76) в ожидании решения; при отказе совета они
возвращаются кандидату. Возврат денег не может быть отменён — права
кандидата на свои средства безусловны.
roles:
- contributor # несостоявшийся пайщик (получатель возврата)
- soviet # совет кооператива (выносит отказ и поручает возврат)
- chairman # председатель / кассир (проводит и подтверждает возврат)
# ── Секция 2. Действия контракта (блокчейн-уровень) ─────────────────────────
actions:
- name: registrator::declinereg
human: Отклонить советом
actor: soviet
role: opener
purpose: >
Совет отказывает кандидату в приёме. Процесс приёма прерывается:
контракт переводит кандидата в состояние возврата и поручает кассе
вернуть полученный взнос. Деньги остаются на расчётах с пайщиком
(счёт 76) — со счёта они ещё не уходят.
- name: registrator::refundpay
human: Подтвердить возврат
actor: chairman
role: closer
purpose: >
Касса фактически провела исходящий перевод денег кандидату и
подтверждает это. Контракт проводит возврат обратной проводкой
(Дт 76 / Кт 51) операцией `o.reg.refund` и закрывает запись кандидата.
После этого аккаунт освобождается для повторной подачи заявления на
том же аккаунте: несостоявшийся пайщик может подать заявку заново.
- name: registrator::declinerfnd
human: Отклонить возврат (недопустимо)
actor: chairman
role: guard
purpose: >
Отмена возврата регистрационного взноса невозможна — действие всегда
завершается ошибкой. Существует только как обязательная развилка
процессинга; в интерфейсе кассы кнопки отклонения возврата нет.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
entity: "registrator::candidate"
entity_human: "Кандидат"
entity_source: cpp/registrator/src/user/declinereg.cpp
states:
- name: payed
human: Взносы оплачены
description: >
Входное состояние процесса: деньги получены и стоят на расчётах
с пайщиком (w.reg.pend, счёт 76), повестка `joincoop` рассмотрена
советом с отрицательным решением.
kind: normal
- name: refunding
human: Возврат в процессе
description: >
Совет отказал и поручил кассе вернуть взнос. Создан исходящий платёж
возврата; деньги по-прежнему стоят на расчётах с пайщиком (w.reg.pend,
счёт 76) и ждут, пока касса фактически проведёт перевод и подтвердит его.
kind: normal
- name: refunded
human: Взнос возвращён
description: >
Касса подтвердила исходящий перевод. В книге Ledger2 проведена
обратная операция `o.reg.refund` (Дт 76 / Кт 51) — обязательство перед
кандидатом закрыто, деньги ушли со счёта. Запись кандидата удалена.
kind: final
transitions:
- from: payed
to: refunding
action: registrator::declinereg
actor: soviet
operations: []
guards:
- Совет принял отрицательное решение по повестке `joincoop`.
- Кандидат в статусе `payed` (деньги были получены и стоят на счёте 76).
- from: refunding
to: refunded
action: registrator::refundpay
actor: chairman
ledger_code: p.reg.refund
operations:
- o.reg.refund
guards:
- Касса фактически провела исходящий перевод денег кандидату.
- Кандидат в статусе `refunding`.
# ── Секция 4. Сценарий ──────────────────────────────────────────────────────
scenario:
steps:
- step: 1
title: Отказ совета и поручение на возврат
actor: soviet
action: registrator::declinereg
description: >
Совет рассматривает повестку `joincoop` и принимает отрицательное
решение. Процесс приёма прерывается: кандидат переводится в состояние
«возврат в процессе», и кассе поручается вернуть полученный
регистрационный взнос. Деньги остаются на расчётах с пайщиком
(w.reg.pend, счёт 76) — со счёта они ещё не уходят. Кандидат получает
уведомление об отказе.
pre:
- Кандидат в статусе `payed` — деньги получены и стоят на счёте 76.
- Совет принял отрицательное решение.
post:
- Кандидат в статусе `refunding`.
- Создан исходящий платёж возврата на сумму взноса.
- Деньги по-прежнему на счёте 76 (проводки ещё нет).
- step: 2
title: Касса возвращает деньги и подтверждает
actor: chairman
action: registrator::refundpay
description: >
Касса проводит фактический перевод денег кандидату и подтверждает,
что возврат выполнен. Контракт возвращает полученный регистрационный
взнос обратной проводкой `o.reg.refund` (Дт 76 / Кт 51): сумма
«вступительный + минимальный паевой» списывается с кошелька ожидания
(w.reg.pend), обязательство перед кандидатом закрывается, деньги
уходят со счёта. Запись кандидата удаляется. Отменить возврат нельзя.
pre:
- Кандидат в статусе `refunding`.
- Касса фактически провела исходящий перевод кандидату.
post:
- В книге Ledger2 проведена o.reg.refund (Дт 76 / Кт 51).
- Запись кандидата удалена из картотеки кандидатов.
- Кандидат получил свои деньги.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
documents:
- action: registrator::declinereg
title: Решение совета об отказе в приёме пайщика
registry_id: 501
signed_by: [ Совет ]
stored_in: "(решение совета — в повестке joincoop)"
# ── Секция 6. Операции ──────────────────────────────────────────────────────
# Возврат проводится одной операцией o.reg.refund — на шаге подтверждения кассой
# (registrator::refundpay), не на решении совета. Отмена возврата невозможна
# (действие-страж declinerfnd всегда завершается ошибкой).
#
# ПЕРЕХОДНЫЙ ПЕРИОД (до 30.07.2026): кандидаты, чей платёж был принят ДО релиза
# двухфазного учёта, суспенса на счёте 76 не имеют — для них on-chain проводки
# возврата нет (как было раньше), деньги возвращает бэкенд банковским переводом,
# а запись кандидата закрывается сразу при отказе совета. Операция o.reg.refund
# проводится только при наличии баланса на w.reg.pend.
operations:
- ledger_code: o.reg.refund
human_name: Возврат регистрационного взноса при отказе совета
wallet_op: BURN
wallet_from: w.reg.pend
wallet_to: ''
debit: 76
credit: 51
amount_ref: candidate.initial + candidate.minimum
triggered_by: registrator::refundpay
description: >
Возврат полученного регистрационного взноса. Сумма «вступительный +
минимальный паевой» списывается с кошелька «Регистрационный взнос
в ожидании решения совета» (w.reg.pend); двойная запись Дт 76 / Кт 51 —
закрытие обязательства перед кандидатом и уход денег с расчётного
счёта. Получателя на цепи нет — деньги фактически переводит касса.
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
related:
- process_type: p.reg.accept
id: public_registrator_accept_process
relation: interrupts
note: >
Возврат — отрицательная развилка процесса «Приём пайщика»
(p.reg.accept). Деньги, поставленные на расчёты с пайщиком операцией
`o.reg.inpay` на этапе оплаты, при отказе совета не признаются
взносом, а возвращаются этим процессом.
@@ -8,8 +8,16 @@
#include "src/user/confirmreg.cpp"
#include "src/user/declinereg.cpp"
#include "src/user/declinepay.cpp"
#include "src/user/refundpay.cpp"
#include "src/user/declinerfnd.cpp"
#include "src/user/reguser.cpp"
#include "src/exit/exit_helpers.hpp"
#include "src/exit/exitcoop.cpp"
#include "src/exit/confirmexit.cpp"
#include "src/exit/completexit.cpp"
#include "src/exit/declinexit.cpp"
#include "src/account/createbranch.cpp"
#include "src/account/newaccount.cpp"
#include "src/account/updateaccnt.cpp"
@@ -85,9 +85,17 @@ public:
[[eosio::action]] void changekey(eosio::name coopname, eosio::name changer, eosio::name username, eosio::public_key public_key);
[[eosio::action]] void confirmreg(eosio::name coopname, checksum256 registration_hash, document2 authorization);
// Выход пайщика из кооператива (возврат паевого взноса) — src/exit/*.cpp
[[eosio::action]] void exitcoop(eosio::name coopname, eosio::name username, checksum256 exit_hash, document2 statement);
[[eosio::action]] void confirmexit(eosio::name coopname, checksum256 exit_hash, document2 authorization);
[[eosio::action]] void completexit(eosio::name coopname, checksum256 exit_hash);
[[eosio::action]] void declinexit(eosio::name coopname, checksum256 exit_hash, std::string reason);
[[eosio::action]] void confirmpay(name coopname, checksum256 registration_hash);
[[eosio::action]] void declinepay(name coopname, checksum256 registration_hash, std::string reason);
[[eosio::action]] void declinereg(name coopname, checksum256 registration_hash, std::string reason);
[[eosio::action]] void refundpay(name coopname, checksum256 registration_hash);
[[eosio::action]] void declinerfnd(name coopname, checksum256 registration_hash, std::string reason);
[[eosio::action]] void createbranch(eosio::name coopname, eosio::name braname);
@@ -0,0 +1,35 @@
/**
* @brief Завершение возврата паевого взноса при выходе из кооператива.
* Кассир подтвердил исходящий платёж gateway вызывает этот коллбэк.
* Списываем зарезервированную сумму (o.wal.wthcpl: Дт80/Кт51, BURN с
* w.wal.wpend), удаляем пайщика из реестра совета и блокируем его аккаунт.
* Возврат назад невозможен повторное участие только через новую регистрацию.
* @param coopname Наименование кооператива
* @param exit_hash Хэш процесса выхода
* @ingroup public_actions
* @ingroup public_registrator_actions
* @note Авторизация требуется от аккаунта: @p gateway
*/
void registrator::completexit(eosio::name coopname, checksum256 exit_hash) {
require_auth(_gateway);
auto exist = Registrator::get_exit_by_hash(coopname, exit_hash);
eosio::check(exist.has_value(), "Объект выхода не найден");
Registrator::exits_index exits(_registrator, coopname.value);
auto e = exits.find(exist->username.value);
eosio::check(e->status == "authorized"_n, "Только одобренные заявления на выход могут быть завершены");
eosio::name username = e->username;
// Проводка возврата паевого: списываем резерв w.wal.wpend, Дт80/Кт51.
std::string memo = "Возврат паевого взноса при выходе из кооператива, username=" + username.to_string();
Ledger2::apply(_registrator, coopname, operations::wallet::COMPLETE_WITHDRAW,
e->quantity, username, exit_hash, memo);
// Финализируем выход: удаляем пайщика и блокируем аккаунт.
Registrator::finalize_member_exit(coopname, username);
exits.erase(e);
}
@@ -0,0 +1,76 @@
/**
* @brief Одобрение советом выхода пайщика из кооператива.
* Совет одобрил заявление о выходе. Контракт сам вычисляет сумму возврата:
* обходит сет паевых кошельков LEDGER2_EXIT_REFUND_WALLETS (w.reg.minshr +
* w.wal.share + w.cap.blago), собирает доступный L3-баланс каждого, консолидирует
* на главный паевой (w.wal.share), резервирует всю сумму (o.wal.wthreq) и создаёт
* исходящий платёж в gateway. Если возвращать нечего выход завершается сразу
* без платежа.
* @param coopname Наименование кооператива
* @param exit_hash Хэш процесса выхода
* @param authorization Документ-решение совета о выходе
* @ingroup public_actions
* @ingroup public_registrator_actions
* @note Авторизация требуется от аккаунта: @p soviet
*/
void registrator::confirmexit(eosio::name coopname, checksum256 exit_hash, document2 authorization) {
require_auth(_soviet);
auto exist = Registrator::get_exit_by_hash(coopname, exit_hash);
eosio::check(exist.has_value(), "Объект выхода не найден");
Registrator::exits_index exits(_registrator, coopname.value);
auto e = exits.find(exist->username.value);
eosio::check(e->status == "pending"_n, "Только ожидающие заявления на выход могут быть одобрены");
eosio::name username = e->username;
// оповещаем пайщика
require_recipient(username);
// Контракт сам считает сумму возврата по L3-балансам ledger2 — не доверяем
// клиенту. Обходим сет паевых («боевых») кошельков LEDGER2_EXIT_REFUND_WALLETS
// (источник истины — wallets.hpp; он же генерируется в cooptypes для
// backend-preview, поэтому расчёт на фронте совпадает с этим): аккумулируем
// доступный баланс каждого (>0) и тут же консолидируем его на главный паевой
// (w.wal.share), чтобы единым платежом вернуть весь паевой через o.wal.*.
eosio::asset total_return = eosio::asset(0, _root_govern_symbol);
for (const auto &wallet_name : LEDGER2_EXIT_REFUND_WALLETS) {
eosio::asset balance = Registrator::get_user_wallet_available(coopname, wallet_name, username);
if (balance.amount <= 0) continue;
total_return += balance;
Registrator::consolidate_share_to_main(coopname, username, wallet_name, balance, exit_hash);
}
exits.modify(e, _soviet, [&](auto &row) {
row.status = "authorized"_n;
row.approved_statement = authorization;
row.quantity = total_return;
});
if (total_return.amount > 0) {
// Резервируем сумму возврата: w.wal.share → w.wal.wpend (o.wal.wthreq).
std::string memo_req = "Резерв паевого взноса под выход из кооператива, username=" + username.to_string();
Ledger2::apply(_registrator, coopname, operations::wallet::REQUEST_WITHDRAW,
total_return, username, exit_hash, memo_req);
// Создаём исходящий платёж в gateway с коллбэками completexit/declinexit.
Action::send<createoutpay_interface>(
_gateway,
"createoutpay"_n,
_registrator,
coopname,
username,
exit_hash,
total_return,
_registrator,
"completexit"_n,
"declinexit"_n
);
} else {
// Возвращать нечего — финализируем выход без платежа.
Registrator::finalize_member_exit(coopname, username);
exits.erase(e);
}
}
@@ -0,0 +1,41 @@
/**
* @brief Отказ в выходе из кооператива.
* Вызывается либо советом при отклонении заявления (до резервирования средств),
* либо gateway при отклонении исходящего платежа (после резервирования). Если
* сумма возврата уже была зарезервирована снимаем резерв (o.wal.wthdec:
* w.wal.wpend w.wal.share). Пайщик остаётся действующим членом кооператива.
* @param coopname Наименование кооператива
* @param exit_hash Хэш процесса выхода
* @param reason Причина отказа
* @ingroup public_actions
* @ingroup public_registrator_actions
* @note Авторизация требуется от аккаунта: @p soviet или @p gateway
*/
void registrator::declinexit(eosio::name coopname, checksum256 exit_hash, std::string reason) {
check_auth_and_get_payer_or_fail({_soviet, _gateway});
auto exist = Registrator::get_exit_by_hash(coopname, exit_hash);
eosio::check(exist.has_value(), "Объект выхода не найден");
Registrator::exits_index exits(_registrator, coopname.value);
auto e = exits.find(exist->username.value);
eosio::name username = e->username;
// Если резерв уже был сделан (отказ платежа после одобрения советом) —
// снимаем его, возвращая средства пайщику на главный паевой кошелёк.
// Минимальный паевой, ранее консолидированный на главный (o.reg.mvmin),
// остаётся на w.wal.share: пайщик сохраняет полную сумму паевого, аналитика
// минимального/целевого разреза при несостоявшемся выходе не восстанавливается.
if (e->status == "authorized"_n && e->quantity.amount > 0) {
std::string memo = "Снятие резерва паевого при отмене выхода из кооператива: " + reason;
Ledger2::apply(_registrator, coopname, operations::wallet::DECLINE_WITHDRAW,
e->quantity, username, exit_hash, memo);
}
// оповещаем пайщика
require_recipient(username);
exits.erase(e);
}
@@ -0,0 +1,95 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/eosio.hpp>
#include "../../../lib/index.hpp"
/**
* @brief Вспомогательные функции процедуры выхода пайщика из кооператива.
*
* Вынесены в отдельный заголовок (а не в table_registrator_exits.hpp), т.к.
* опираются на ledger2-кошельки и userwallets, которые подключаются позже в
* domain/index.hpp. Подключается в registrator.cpp перед exit-экшенами, когда
* весь lib/index.hpp уже доступен.
*/
namespace Registrator {
using namespace eosio;
/**
* @brief Доступный L3-баланс пайщика на USER_SHARED-кошельке ledger2.
*
* Возвращает available (без blocked) пары `(wallet_name, username)` из таблицы
* userwallets контракта ledger2. Если записи нет нулевая сумма в базовой
* валюте управления.
*/
inline asset get_user_wallet_available(name coopname, name wallet_name, name username) {
userwallets_index userwallets(_ledger2, coopname.value);
auto idx = userwallets.get_index<"byuserwallet"_n>();
auto it = idx.find(combine_ids(wallet_name.value, username.value));
if (it == idx.end()) {
return asset(0, _root_govern_symbol);
}
return it->available;
}
/**
* @brief Консолидация доступного паевого кошелька пайщика на главный
* (`w.wal.share`) перед резервом возврата при выходе.
*
* Главный кошелёк (`w.wal.share`) уже целевой перенос не нужен. Для остальных
* кошельков сета `LEDGER2_EXIT_REFUND_WALLETS` применяется операция переноса на
* главный:
* w.reg.minshr o.reg.mvmin (MOVE_MINSHARE);
* w.cap.blago o.cap.wthcap (WITHDRAW_FROM_CAPITAL).
*
* Кошелёк сета без операции переноса (новый паевой кошелёк забыли смаппить)
* валит транзакцию с явным сообщением защита от тихой потери средств.
*/
inline void consolidate_share_to_main(name coopname, name username, name wallet_name, asset amount, checksum256 exit_hash) {
if (wallet_name == ledger2_wallets::SHARE_FUND_PAY) return; // уже на главном паевом
eosio::name op;
if (wallet_name == ledger2_wallets::MIN_SHARE_FUND) {
op = operations::registrator::MOVE_MINSHARE; // w.reg.minshr → w.wal.share
} else if (wallet_name == ledger2_wallets::BLAGOROST_FUND) {
op = operations::capital::WITHDRAW_FROM_CAPITAL; // w.cap.blago → w.wal.share
} else {
eosio::check(false,
std::string{"Нет операции консолидации паевого кошелька "} + wallet_name.to_string() +
" на главный при выходе — добавьте маппинг в consolidate_share_to_main");
}
std::string memo = "Консолидация паевого взноса при выходе, кошелёк=" +
wallet_name.to_string() + ", username=" + username.to_string();
Ledger2::apply(_registrator, coopname, op, amount, username, exit_hash, memo);
}
/**
* @brief Финализация выхода: удаление пайщика из реестра совета и блокировка
* аккаунта в registrator.
*
* Вызывается по завершении возврата паевого взноса (completexit) либо сразу,
* если возвращать нечего (нулевой паевой). После этого `get_participant_or_fail`
* для пайщика начинает падать он лишён права подавать заявления.
*/
inline void finalize_member_exit(name coopname, name username) {
// удаляем пайщика из реестра совета (уменьшит счётчик активных пайщиков)
action(
permission_level{_registrator, "active"_n},
_soviet,
"delpartcpnt"_n,
std::make_tuple(coopname, username)
).send();
// блокируем аккаунт в картотеке registrator
accounts_index accounts(_registrator, _registrator.value);
auto account = accounts.find(username.value);
eosio::check(account != accounts.end(), "Аккаунт не найден");
accounts.modify(account, _registrator, [&](auto &a) {
a.status = "blocked"_n;
});
}
} // namespace Registrator
@@ -0,0 +1,57 @@
/**
* @brief Подача заявления на выход из кооператива.
* Действующий пайщик подаёт подписанное заявление о выходе (registry 200).
* Создаётся объект выхода и повестка совета `leavecoop` на рассмотрение.
* Возврат паевого взноса произойдёт после одобрения советом (confirmexit).
* @param coopname Наименование кооператива
* @param username Имя пайщика, выходящего из кооператива
* @param exit_hash Хэш процесса выхода
* @param statement Подписанное заявление о выходе (registry 200)
* @ingroup public_actions
* @ingroup public_registrator_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
void registrator::exitcoop(eosio::name coopname, eosio::name username, checksum256 exit_hash, document2 statement) {
require_auth(coopname);
get_cooperative_or_fail(coopname);
// выйти может только действующий пайщик (не заблокированный)
get_participant_or_fail(coopname, username);
// повторная подача запрещена — у пайщика может быть только один процесс выхода
Registrator::exits_index exits(_registrator, coopname.value);
auto existing = exits.find(username.value);
eosio::check(existing == exits.end(), "Заявление на выход уже подано");
auto by_hash = Registrator::get_exit_by_hash(coopname, exit_hash);
eosio::check(!by_hash.has_value(), "Объект выхода уже существует с указанным хэшем");
// проверяем подпись заявления о выходе
verify_document_or_fail(statement);
exits.emplace(coopname, [&](auto &e) {
e.username = username;
e.coopname = coopname;
e.status = "pending"_n;
e.created_at = current_time_point();
e.statement = statement;
e.exit_hash = exit_hash;
e.quantity = asset(0, _root_govern_symbol);
});
// повестка совета на рассмотрение выхода с коллбэками confirmexit/declinexit
::Soviet::create_agenda(
_registrator,
coopname,
username,
get_valid_soviet_action("leavecoop"_n),
exit_hash,
_registrator, // callback_contract
"confirmexit"_n, // callback при одобрении
"declinexit"_n, // callback при отказе
statement,
std::string("")
);
}
@@ -1,3 +1,5 @@
#include <array>
/**
* @brief Миграция данных контракта.
* Обновляет счетчики активных пайщиков для всех кооперативов
@@ -40,4 +42,56 @@
coop.active_participants_count = active_count;
});
}
// ──────────────────────────────────────────────────────────────────────────
// Разовая очистка ФАНТОМНЫХ пайщиков кооператива fgrtejiwnynn (инцидент
// 2026-06-04). Причина: install.interactor прогнан 3× (2 провала + успех);
// каждый прогон минтил на цепи новые аккаунты совета (adduser) со случайным
// username, а откат в catch чистил только off-chain. На цепи осели 6 дублей-
// аккаунтов одних и тех же 3 учредителей (см. ~/gorozhane-dup-install-cleanup.md
// и ~/fixes.md). Удаляем записи registrator::accounts и пересчитываем счётчик
// активных пайщиков из soviet::participants (источник истины).
//
// Список username — единственный авторитет (дедуп off-chain по ФИО невозможен
// on-chain — личных данных в registrator::accounts нет). ОДИН И ТОТ ЖЕ список
// продублирован в soviet::migrate и ledger2::migrate — править синхронно.
// Идемпотентно: повторный прогон не находит записей → no-op.
{
const eosio::name PHANTOM_COOP = "fgrtejiwnynn"_n;
const std::array<eosio::name, 6> PHANTOMS = {
"bbsezpgufvmm"_n, "errwcgjwverm"_n, "hcfsluqsfehw"_n,
"kgkzdadfpzki"_n, "nzuyijobapsv"_n, "tplwfwbujugq"_n,
};
auto pcoop = coops2.find(PHANTOM_COOP.value);
if (pcoop != coops2.end() && pcoop->is_cooperative) {
accounts_index accounts(_registrator, _registrator.value);
for (const auto& u : PHANTOMS) {
auto acc = accounts.find(u.value);
if (acc == accounts.end()) continue; // уже удалён / отсутствует
// Дедуп-гард: трогаем только аккаунты, зарегистрированные ЭТИМ коопом.
if (acc->registrator != PHANTOM_COOP) continue;
accounts.erase(acc);
}
// Пересчитываем счётчик активных пайщиков из реестра soviet::participants
// (источник истины), ИСКЛЮЧАЯ фантомов явно — счётчик сходится к верному
// значению за ОДИН деплой независимо от порядка выполнения migrate
// (registrator::migrate vs soviet::migrate). Если бы считали всех has_vote,
// а soviet ещё не удалил фантомов — счётчик временно завысился бы.
participants_index pparts(_soviet, PHANTOM_COOP.value);
uint64_t active = 0;
for (auto p = pparts.begin(); p != pparts.end(); ++p) {
bool is_phantom = false;
for (const auto& ph : PHANTOMS) {
if (ph == p->username) { is_phantom = true; break; }
}
if (!is_phantom && p->has_vote) active++;
}
coops2.modify(pcoop, _registrator, [&](auto& coop) {
coop.active_participants_count = active;
});
}
}
}
@@ -23,7 +23,14 @@ void registrator::confirmpay(name coopname, checksum256 registration_hash){
candidates.modify(candidate, _registrator, [&](auto &c){
c.status = "payed"_n;
});
// Деньги получены кассой, но взнос ещё не признан — ставим на расчёты с
// пайщиком (Dr 51 / Cr 76, ISSUE w.reg.pend) до решения совета. Сумма —
// вступительный + минимальный паевой. Признание/возврат — в confirmreg/declinereg.
eosio::asset registration_quantity = candidate -> initial + candidate -> minimum;
std::string memo = "Приём регистрационного взноса в ожидание решения совета от username=" + candidate -> username.to_string();
Ledger2::apply(_registrator, coopname, operations::registrator::RECEIVE_PAYMENT, registration_quantity, candidate -> username, registration_hash, memo);
action(permission_level{ _registrator, "active"_n}, _soviet, "createagenda"_n,
std::make_tuple(
coopname,
@@ -1,3 +1,5 @@
#include "registration_migration.hpp"
/**
* @brief Подтверждение регистрации пользователя.
* Подтверждает регистрацию кандидата советом и добавляет его в кооператив
@@ -45,11 +47,28 @@ void registrator::confirmreg(eosio::name coopname, checksum256 registration_hash
)
).send();
std::string memo = "Минимальный паевой взнос при вступлении пайщика с username=" + candidate -> username.to_string();
Ledger2::apply(_registrator, coopname, operations::registrator::PUT_MINSHARE, candidate -> minimum, candidate -> username, registration_hash, memo);
// Совет одобрил — признаём взнос. Гарды amount>0: у кооператива может не быть
// вступительного (initial==0) — ledger2::apply падает на нулевой сумме.
//
// MIGRATION (снять условие после 30.07.2026, см. registration_migration.hpp):
// для кандидатов, принятых ДО релиза двухфазного учёта (нет баланса на
// w.reg.pend), признаём по старому пути — прямой ISSUE (Dr 51 / Cr 80|86);
// для новых — переносим суспенс с 76 на 80/86 (TRANSFER w.reg.pend → ...).
bool has_pending = Registrator::get_registration_pending_balance(coopname, candidate -> username).amount > 0;
memo = "Вступительный взнос при вступлении пайщика с username=" + candidate -> username.to_string();
Ledger2::apply(_registrator, coopname, operations::registrator::PAY_ENTRANCE, candidate -> initial, candidate -> username, registration_hash, memo);
if (candidate -> minimum.amount > 0) {
std::string memo = "Зачисление минимального паевого взноса по решению совета, username=" + candidate -> username.to_string();
Ledger2::apply(_registrator, coopname,
has_pending ? operations::registrator::SETTLE_MINSHARE : operations::registrator::PUT_MINSHARE,
candidate -> minimum, candidate -> username, registration_hash, memo);
}
if (candidate -> initial.amount > 0) {
std::string memo = "Зачисление вступительного взноса по решению совета, username=" + candidate -> username.to_string();
Ledger2::apply(_registrator, coopname,
has_pending ? operations::registrator::SETTLE_ENTRANCE : operations::registrator::PAY_ENTRANCE,
candidate -> initial, candidate -> username, registration_hash, memo);
}
// Увеличиваем счетчик активных пайщиков
cooperatives2_index cooperatives(_registrator, _registrator.value);
auto coop_itr = cooperatives.find(coopname.value);
@@ -14,7 +14,7 @@ void registrator::declinepay(name coopname, checksum256 registration_hash, std::
auto exist_candidate = Registrator::get_candidate_by_hash(coopname, registration_hash);
eosio::check(!exist_candidate.has_value(), "Кандидат не найден");
eosio::check(exist_candidate.has_value(), "Кандидат не найден");
Registrator::candidates_index candidates(_registrator, coopname.value);
@@ -1,3 +1,5 @@
#include "registration_migration.hpp"
/**
* @brief Отклонение регистрации пользователя.
* Отклоняет регистрацию кандидата советом
@@ -10,5 +12,53 @@
* @note Авторизация требуется от аккаунта: @p soviet
*/
void registrator::declinereg(name coopname, checksum256 registration_hash, std::string reason) {
//событие отклонения регистрации советом
}
require_auth(_soviet);
auto candidate = Registrator::get_candidate_by_hash(coopname, registration_hash);
eosio::check(candidate.has_value(), "Кандидат не найден");
// оповещаем кандидата об отказе совета
require_recipient(candidate -> username);
// Совет отказал — процесс приёма взноса прерывается, начинается процесс
// возврата (p.reg.refund). Деньги НЕ списываем здесь: они стоят на расчётах
// с пайщиком (счёт 76, кошелёк w.reg.pend) до тех пор, пока касса фактически
// не проведёт исходящий возврат. Обратную проводку Дт 76 / Кт 51 выполняет
// коллбэк refundpay по подтверждению кассы (gateway::outcomplete) — симметрично
// возврату паевого в контракте 'wallet' (authwthd → createoutpay → completewthd).
//
// MIGRATION (снять условие после 30.07.2026, см. registration_migration.hpp):
// кандидаты, принятые ДО релиза двухфазного учёта, суспенса на w.reg.pend
// не имеют — для них on-chain возврата нет (как было раньше), банковский
// возврат оформляет бэкенд, а запись кандидата закрываем сразу.
eosio::asset pending = Registrator::get_registration_pending_balance(coopname, candidate -> username);
Registrator::candidates_index candidates(_registrator, coopname.value);
auto it = candidates.find(candidate -> username.value);
if (pending.amount > 0) {
// Новый путь: переводим кандидата в возврат и создаём исходящий платёж в
// gateway с коллбэком после подтверждения кассой. Деньги остаются на счёте 76
// вплоть до фактического проведения возврата кассой (refundpay).
candidates.modify(it, _registrator, [&](auto &c){
c.status = "refunding"_n;
});
action(permission_level{ _registrator, "active"_n}, _gateway, "createoutpay"_n,
std::make_tuple(
coopname,
candidate -> username,
registration_hash,
pending,
_registrator,
"refundpay"_n,
"declinerfnd"_n
)
).send();
} else {
// Миграционный путь: возвращать на цепи нечего — закрываем кандидата и
// освобождаем аккаунт для повторной подачи (снимаем карточку участника).
if (it != candidates.end()) candidates.erase(it);
reset_account_card(candidate -> username);
}
}
@@ -0,0 +1,19 @@
/**
* @brief Отклонение возврата регистрационного взноса НЕДОПУСТИМО.
* Возврат полученного регистрационного взноса при отказе совета не может быть
* отменён: средства кандидата обязаны быть ему возвращены. Действие существует
* только как обязательный decline-коллбэк gateway (createoutpay требует имя
* decline-действия) и всегда завершается ошибкой. На фронте кнопки отклонения
* возврата нет; этот страж защищает от прямого вызова gateway::outdecline.
* @param coopname Наименование кооператива
* @param registration_hash Хэш регистрации (= outcome_hash исходящего платежа)
* @param reason Причина отклонения (игнорируется)
* @ingroup public_actions
* @ingroup public_registrator_actions
* @note Авторизация требуется от аккаунта: @p gateway
*/
void registrator::declinerfnd(name coopname, checksum256 registration_hash, std::string reason) {
require_auth(_gateway);
eosio::check(false, "Отмена возврата регистрационного взноса невозможна");
}
@@ -0,0 +1,42 @@
#include "registration_migration.hpp"
/**
* @brief Подтверждение возврата регистрационного взноса.
* Коллбэк gateway после фактического проведения кассой исходящего возврата
* (gateway::outcomplete). Проводит обратную проводку Дт 76 / Кт 51 (сжигание
* суспенса w.reg.pend) и закрывает запись кандидата. Деньги уходят со счёта 76
* ровно в этот момент не раньше, чем касса подтвердит исходящий перевод.
* @param coopname Наименование кооператива
* @param registration_hash Хэш регистрации (= outcome_hash исходящего платежа)
* @ingroup public_actions
* @ingroup public_registrator_actions
* @note Авторизация требуется от аккаунта: @p gateway
*/
void registrator::refundpay(name coopname, checksum256 registration_hash) {
require_auth(_gateway);
auto candidate = Registrator::get_candidate_by_hash(coopname, registration_hash);
eosio::check(candidate.has_value(), "Кандидат не найден");
eosio::check(candidate -> status == "refunding"_n, "Кандидат не находится в процессе возврата");
// оповещаем кандидата о завершении возврата
require_recipient(candidate -> username);
// Сжигаем фактический остаток на w.reg.pend — он равен полученной сумме
// (вступительный + минимальный паевой). Обратная проводка Дт 76 / Кт 51.
eosio::asset pending = Registrator::get_registration_pending_balance(coopname, candidate -> username);
eosio::check(pending.amount > 0, "Нет средств на возврат");
std::string memo = "Возврат регистрационного взноса при отказе совета, username=" + candidate -> username.to_string();
Ledger2::apply(_registrator, coopname, operations::registrator::REFUND, pending, candidate -> username, registration_hash, memo);
// Возврат завершён — удаляем кандидата из картотеки.
Registrator::candidates_index candidates(_registrator, coopname.value);
auto it = candidates.find(candidate -> username.value);
candidates.erase(it);
// Освобождаем аккаунт для повторной подачи на том же username: снимаем
// карточку участника, иначе reguser упадёт «повторное получение невозможно».
reset_account_card(candidate -> username);
}
@@ -0,0 +1,42 @@
#pragma once
// ─────────────────────────────────────────────────────────────────────────────
// ВРЕМЕННЫЙ МИГРАЦИОННЫЙ КОД — СНЯТЬ УСЛОВИЕ ПОСЛЕ 30.07.2026.
//
// На момент релиза двухфазного учёта регистрационного взноса (счёт 76,
// кошелёк w.reg.pend) в системе могут быть «зависшие» кандидаты: их платёж
// уже принят кассой (статус `payed`), но ПО СТАРОМУ пути — без операции
// o.reg.inpay, то есть БЕЗ баланса на w.reg.pend.
//
// Когда совет вынесет по ним решение уже на новом контракте:
// • confirmreg — нельзя делать TRANSFER с пустого w.reg.pend (упадёт
// «недостаточно средств»); для таких кандидатов признаём взнос по-старому
// прямым ISSUE (o.reg.putmin / o.reg.payent, Dr 51 / Cr 80|86).
// • declinereg — возврата через ledger2 нет (как было раньше): on-chain
// суспенса нет, банковский возврат оформляет бэкенд.
//
// Признак пути — наличие баланса кандидата на w.reg.pend (счёт 76):
// > 0 → принят по новому пути (o.reg.inpay) → признание/возврат через 76;
// == 0 → принят до релиза → старый путь.
//
// После 30.07.2026 (когда все зависшие на момент релиза кандидаты гарантированно
// получат решение совета) этот файл и ветвления в confirmreg/declinereg убрать,
// оставив только новый путь.
// ─────────────────────────────────────────────────────────────────────────────
namespace Registrator {
/**
* @brief Баланс кандидата на суспенс-кошельке регистрационного взноса
* (w.reg.pend, счёт 76) из ledger2 L3 (userwallets). Запись существует только
* пока available > 0 (auto-delete на нуле), поэтому отсутствие = нулевой баланс.
*/
inline eosio::asset get_registration_pending_balance(eosio::name coopname, eosio::name username) {
userwallets_index user_wallets(_ledger2, coopname.value);
auto idx = user_wallets.get_index<"byuserwallet"_n>();
auto it = idx.find(combine_ids(ledger2_wallets::REGISTRATION_PENDING.value, username.value));
if (it == idx.end()) return eosio::asset(0, _root_govern_symbol);
return it->available;
}
} // namespace Registrator
@@ -5,7 +5,7 @@
# получить от совета согласие на выполнение действия. Поток:
#
# контракт-инициатор → newpackage / create_agenda → soviet::decisions
# ↓ голосование (votefor / voteagainst / cancelvote)
# ↓ голосование (votefor / voteagainst)
# approved=true → authorize (председатель)
# ↓
# authorized=true → exec → callback в исходный контракт (confirmreg /
@@ -19,8 +19,8 @@
#
# Источники в коде:
# • cpp/soviet/src/doc/newpackage.cpp
# • cpp/soviet/src/vote/{votefor,voteagainst,cancelvote}.cpp
# • cpp/soviet/src/decision/{authorize,cancelexprd,exec}.cpp
# • cpp/soviet/src/vote/{votefor,voteagainst}.cpp
# • cpp/soviet/src/decision/{authorize,cancelexprd,declinedec,exec}.cpp
# ─────────────────────────────────────────────────────────────────────────────
process_type: sov.authpkg
@@ -74,17 +74,9 @@ actions:
actor: Член совета
role: reject
purpose: >
Член совета голосует «против». При достижении консенсуса по отказу
решение помечается как rejected и при exec в исходный контракт
уйдёт decline-callback.
- name: soviet::cancelvote
human: Отозвать голос
actor: Член совета
role: progress
purpose: >
Член совета отзывает ранее поданный голос до перехода решения
в финальный статус.
Член совета голосует «против». Голос фиксируется; решение само не
закрывается. Когда против высказалось больше половины состава совета,
председатель снимает решение явным действием (см. «Отклонить решение»).
- name: soviet::authorize
human: Утвердить протокол
@@ -107,11 +99,22 @@ actions:
- name: soviet::cancelexprd
human: Отменить просроченное
actor: Председатель
actor: Кооператив
role: reject
purpose: >
Если решение не было утверждено в срок, оно отменяется как
просроченное, в исходный контракт callback не уходит.
просроченное: в исходный процесс уходит уведомление об отказе,
решение закрывается. Снятие просроченных решений происходит
автоматически по истечении срока.
- name: soviet::declinedec
human: Отклонить решение
actor: Председатель
role: reject
purpose: >
Если против решения проголосовало больше половины состава совета,
председатель явно отклоняет его, не дожидаясь истечения срока: в
исходный процесс уходит уведомление об отказе, решение закрывается.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
entity: soviet::decision
@@ -150,9 +153,9 @@ states:
- name: rejected
human: Отклонено
description: >
Совет проголосовал «против» либо решение отменено как просроченное.
В исходный контракт уходит decline-callback (или callback
не вызывается, если просрочка).
Председатель отклонил решение по большинству «против» (declinedec) либо
решение отменено как просроченное (cancelexprd, автоматически по сроку).
В обоих случаях в исходный контракт уходит decline-callback.
kind: virtual
virtual: true
@@ -174,15 +177,15 @@ transitions:
- from: pending
to: rejected
action: soviet::voteagainst
actor: Член совета
action: soviet::declinedec
actor: Председатель
guards:
- Большинство голосов «против».
- from: pending
to: rejected
action: soviet::cancelexprd
actor: Председатель
actor: Кооператив
guards:
- Истёк срок принятия решения.
@@ -223,8 +226,8 @@ scenario:
actor: Член совета
action: soviet::votefor
description: >
Члены совета голосуют по решению (votefor / voteagainst /
cancelvote). При достижении большинства «за» — approved=true.
Члены совета голосуют по решению (votefor / voteagainst).
При достижении большинства «за» — approved=true.
pre:
- Решение в `pending`.
post:
@@ -259,18 +262,21 @@ scenario:
alternatives:
- branch: Отказ совета
at_step: 2
action: soviet::voteagainst
actor: Член совета
action: soviet::declinedec
actor: Председатель
description: >
Большинство «против» — при exec в исходный контракт уйдёт
decline-callback (например, registrator::declinereg).
Когда против высказалось больше половины состава совета, голос «против»
лишь фиксируется. Председатель явно отклоняет решение (declinedec), не
дожидаясь срока: в исходный контракт уходит decline-callback (например,
registrator::declinereg), запись решения удаляется.
- branch: Просрочка
at_step: 2
action: soviet::cancelexprd
actor: Председатель
actor: Кооператив
description: >
Решение не принято в срок, отменяется без вызова callback.
Решение не принято в срок — автоматически отменяется по истечении
срока: в исходный контракт уходит decline-callback, запись удаляется.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
# Документ повестки приходит из исходного процесса — конкретный registry_id
@@ -9,10 +9,10 @@
# • cpp/soviet/src/decision/freedecision.cpp
# • cpp/soviet/src/vote/votefor.cpp
# • cpp/soviet/src/vote/voteagainst.cpp
# • cpp/soviet/src/vote/cancelvote.cpp
# • cpp/soviet/src/decision/authorize.cpp
# • cpp/soviet/src/decision/exec.cpp
# • cpp/soviet/src/decision/cancelexprd.cpp
# • cpp/soviet/src/decision/declinedec.cpp (внутри cancelexprd.cpp)
# ─────────────────────────────────────────────────────────────────────────────
process_type: sov.decision
@@ -63,17 +63,9 @@ actions:
actor: Член совета
role: reject
purpose: >
Член совета голосует «против». При достижении консенсуса по отказу
решение переходит в статус rejected; протокол не оформляется,
исполнения не происходит.
- name: soviet::cancelvote
human: Отозвать голос
actor: Член совета
role: progress
purpose: >
Член совета отзывает ранее поданный голос (за или против) до того,
как решение перешло в финальный статус.
Член совета голосует «против». Голос фиксируется; решение само не
закрывается. Когда против высказалось больше половины состава совета,
председатель снимает решение явным действием (см. «Отклонить решение»).
- name: soviet::authorize
human: Утвердить протокол
@@ -96,11 +88,21 @@ actions:
- name: soviet::cancelexprd
human: Отменить просроченное
actor: Член совета
actor: Кооператив
role: reject
purpose: >
Если решение не было утверждено в срок, оно может быть отменено
как просроченное. Запись удаляется без исполнения.
Если решение не было утверждено в срок, оно отменяется как
просроченное. Запись удаляется без исполнения. Снятие просроченных
решений происходит автоматически по истечении срока.
- name: soviet::declinedec
human: Отклонить решение
actor: Председатель
role: reject
purpose: >
Если против решения проголосовало больше половины состава совета,
председатель явно отклоняет его, не дожидаясь истечения срока.
Запись удаляется без исполнения.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
entity: soviet::decision
@@ -165,15 +167,15 @@ transitions:
- from: created
to: rejected
action: soviet::voteagainst
actor: Член совета
action: soviet::declinedec
actor: Председатель
guards:
- Большинство голосов «против».
- from: created
to: rejected
action: soviet::cancelexprd
actor: Член совета
actor: Кооператив
guards:
- Истёк срок принятия решения.
@@ -214,7 +216,7 @@ scenario:
actor: Член совета
action: soviet::votefor
description: >
Члены совета поочерёдно голосуют (votefor / voteagainst / cancelvote).
Члены совета поочерёдно голосуют (votefor / voteagainst).
При достижении большинства «за» решение помечается как approved.
pre:
- Решение ещё не закрыто (created).
@@ -248,21 +250,22 @@ scenario:
- События newresolved/newdecision разосланы.
alternatives:
- branch: Отклонение голосованием
- branch: Отклонение советом
at_step: 2
action: soviet::voteagainst
actor: Член совета
action: soviet::declinedec
actor: Председатель
description: >
Большинство голосов «против» — решение помечается как rejected
и не утверждается председателем.
Когда против высказалось большинство состава совета, голос «против»
лишь фиксируется. Председатель явно отклоняет решение (declinedec) —
запись удаляется, протокол не оформляется.
- branch: Просрочка
at_step: 2
action: soviet::cancelexprd
actor: Член совета
actor: Кооператив
description: >
Если решение не было принято в срок, любой член совета может
вызвать cancelexprd — запись удаляется как просроченная.
Если решение не было принято в срок, оно автоматически отменяется
по истечении срока — запись удаляется как просроченная.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
documents:
@@ -49,6 +49,7 @@
#include "src/fund/fundwithdraw.cpp"
#include "src/participant/addparticipant.cpp"
#include "src/participant/delparticipant.cpp"
#include "src/participant/setminamt.cpp"
#include "src/participant/block.cpp"
#include "src/participant/cancelreg.cpp"
@@ -139,6 +139,9 @@ public:
//regaccount.cpp
[[eosio::action]] void addpartcpnt(eosio::name coopname, eosio::name username, eosio::name braname, eosio::name type, eosio::time_point_sec created_at, eosio::asset initial, eosio::asset minimum, bool spread_initial);
//delparticipant.cpp
[[eosio::action]] void delpartcpnt(eosio::name coopname, eosio::name username);
//setminamt.cpp
[[eosio::action]] void setminamt(eosio::name coopname, eosio::name username, eosio::asset minimum);
@@ -201,6 +204,7 @@ public:
//decisions
[[eosio::action]] void withdraw(eosio::name coopname, eosio::name username, uint64_t withdraw_id, document2 statement);
[[eosio::action]] void cancelexprd(eosio::name coopname, uint64_t decision_id);
[[eosio::action]] void declinedec(eosio::name coopname, uint64_t decision_id);
void withdraw_effect(eosio::name executer, eosio::name coopname, uint64_t decision_id, uint64_t batch_id);
@@ -9,10 +9,14 @@
* @ingroup public_actions
* @ingroup public_soviet_actions
* @note Авторизация требуется от аккаунта: @p chairman
* @note Авторизация требуется от аккаунта: @p coopname
*/
void soviet::authorize(eosio::name coopname, eosio::name chairman, uint64_t decision_id, document2 document) {
require_auth(chairman);
void soviet::authorize(eosio::name coopname, eosio::name chairman, uint64_t decision_id, document2 document) {
// Авторизует кооператив (действие проводится через бэкенд ключом кооператива).
// Согласие председателя при этом не теряется: оно подтверждается криптографически
// его личной подписью на документе (verify_document_or_fail ниже) и проверкой
// is_valid_chairman — без валидного подписанного председателем документа авторизация невозможна.
require_auth(coopname);
boards_index boards(_soviet, coopname.value);
autosigner_index autosigner(_soviet, coopname.value);
@@ -27,7 +31,9 @@ void soviet::authorize(eosio::name coopname, eosio::name chairman, uint64_t deci
verify_document_or_fail(document);
decisions.modify(decision, chairman, [&](auto &d){
// RAM-плательщик — кооператив: транзакцию авторизует coopname (require_auth выше),
// поэтому увеличивать RAM аккаунта председателя нельзя (он tx не подписывал).
decisions.modify(decision, coopname, [&](auto &d){
d.authorized_by = chairman;
d.authorized = !decision -> authorized;
d.authorization = document;
@@ -9,30 +9,23 @@
* @note Авторизация требуется от аккаунта: @p coopname
*/
void soviet::cancelexprd(eosio::name coopname, uint64_t decision_id) {
require_auth(coopname);
// Общая ветка отказа решения: при наличии полей коллбэка отправляет инициатору
// коллбэк отклонения, затем удаляет решение из оперативной таблицы. Вызывается
// из cancelexprd (просрочка) и voteagainst (отрицательный консенсус).
void decline_and_erase_decision(eosio::name coopname, uint64_t decision_id, const std::string &reason) {
decisions_index decisions(_soviet, coopname.value);
auto decision = decisions.find(decision_id);
eosio::check(decision != decisions.end(), "Решение не найдено");
// Проверка истечения срока
bool expired = true;
if ( decision -> expired_at.has_value() ) {
expired = (eosio::time_point_sec(eosio::current_time_point()) > decision->expired_at.value());
}
eosio::check(expired, "Срок действия решения ещё не истёк");
// Проверка наличия всех необходимых полей для отправки коллбэка
if (decision->callback_contract.has_value() &&
decision->decline_callback.has_value() &&
decision->hash.has_value() &&
decision->callback_contract.value() != ""_n &&
decision->confirm_callback.value() != ""_n &&
if (decision->callback_contract.has_value() &&
decision->decline_callback.has_value() &&
decision->hash.has_value() &&
decision->callback_contract.value() != ""_n &&
decision->confirm_callback.value() != ""_n &&
decision->decline_callback.value() != ""_n
) {
// Отправка коллбэка отклонения
Action::send<decline_callback_interface>(
decision->callback_contract.value(),
@@ -41,10 +34,61 @@ void soviet::cancelexprd(eosio::name coopname, uint64_t decision_id) {
_soviet,
coopname,
decision->hash.value(),
std::string("Отклонение по истечению срока")
reason
);
}
// Удаление решения
decisions.erase(decision);
}
void soviet::cancelexprd(eosio::name coopname, uint64_t decision_id) {
require_auth(coopname);
decisions_index decisions(_soviet, coopname.value);
auto decision = decisions.find(decision_id);
eosio::check(decision != decisions.end(), "Решение не найдено");
// Проверка истечения срока
bool expired = true;
if ( decision -> expired_at.has_value() ) {
expired = (eosio::time_point_sec(eosio::current_time_point()) > decision->expired_at.value());
}
eosio::check(expired, "Срок действия решения ещё не истёк");
decline_and_erase_decision(coopname, decision_id, std::string("Отклонение по истечению срока"));
}
/**
* @brief Явное отклонение решения советом по отрицательному консенсусу
* Проводится председателем (ключом кооператива), когда против решения
* проголосовало большинство состава совета, до истечения срока. Развязано с
* cancelexprd: просрочка гасится автоматически по сроку, а отрицательно принятое
* решение снимается с повестки только этим явным действием.
* @param coopname Наименование кооператива
* @param decision_id Идентификатор решения для отклонения
* @ingroup public_actions
* @ingroup public_soviet_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
void soviet::declinedec(eosio::name coopname, uint64_t decision_id) {
require_auth(coopname);
decisions_index decisions(_soviet, coopname.value);
auto decision = decisions.find(decision_id);
eosio::check(decision != decisions.end(), "Решение не найдено");
// Отрицательный консенсус: против должно высказаться больше половины состава
// совета (порог зеркален votefor — 50%, строгое «больше»).
auto board = get_board_by_type_or_fail(coopname, "soviet"_n);
auto [votes_for_count, votes_against_count] = decision->get_votes_count();
uint64_t total_members = board.get_members_count();
uint64_t consensus_percent = 50;
eosio::check(votes_against_count * 100 > total_members * consensus_percent,
"Решение не отклонено советом: нет большинства голосов против");
decline_and_erase_decision(coopname, decision_id, std::string("Отклонено советом: большинство голосов против"));
}
@@ -10,11 +10,14 @@
* @param coopname Имя кооператива
* @param decision_id Идентификатор решения для исполнения
*
* @note Авторизация требуется от аккаунта: @p executer
* @note Авторизация требуется от аккаунта: @p coopname
*/
void soviet::exec(eosio::name executer, eosio::name coopname, uint64_t decision_id) {
require_auth(executer);
void soviet::exec(eosio::name executer, eosio::name coopname, uint64_t decision_id) {
// Исполнение проводится через бэкенд ключом кооператива. Параметр executer остаётся
// в payload как аудит-метка (кто инициировал исполнение) и не используется для авторизации
// inline-эффектов — те выполняются под собственными правами контракта (_soviet/_gateway/_fund).
require_auth(coopname);
decisions_index decisions(_soviet, coopname.value);
auto decision = decisions.find(decision_id);
@@ -0,0 +1,38 @@
/**
* @brief Удаление пайщика из кооператива при выходе
* Стирает запись пайщика из реестра совета по завершении процедуры выхода
* (после возврата паевого взноса). Зеркало `addpartcpnt`. Если выбывающий
* пайщик был активным и имел право голоса уменьшает счётчик активных
* пайщиков в registrator (как это делает `block`).
* @param coopname Наименование кооператива
* @param username Наименование выбывающего пайщика
* @ingroup public_actions
* @ingroup public_soviet_actions
* @note Авторизация требуется от аккаунта в белом списке контрактов
*/
void soviet::delpartcpnt(eosio::name coopname, eosio::name username) {
check_auth_and_get_payer_or_fail(contracts_whitelist);
get_cooperative_or_fail(coopname);
participants_index participants(_soviet, coopname.value);
auto participant = participants.find(username.value);
eosio::check(participant != participants.end(), "Пайщик не найден в кооперативе");
// Если пайщик был активным и имел право голоса — уменьшаем счётчик активных
// пайщиков (счётчик живёт в registrator, см. block.cpp / decparticpnt.cpp).
bool had_vote = participant->has_vote;
bool was_active = participant->status == "accepted"_n;
if (was_active && had_vote) {
action(
permission_level{_soviet, "active"_n},
_registrator,
"decparticpnt"_n,
std::make_tuple(coopname, username)
).send();
}
participants.erase(participant);
}
@@ -124,7 +124,7 @@ void soviet::make_base_coagreements( eosio::name coopname, eosio::symbol govern_
row.type = "user"_n;
row.coopname = coopname;
row.program_id = 0;
row.draft_id = 3;
row.draft_id = 4;
});
//создаём соглашение для шаблона политики конфиденциальности
@@ -132,7 +132,7 @@ void soviet::make_base_coagreements( eosio::name coopname, eosio::symbol govern_
row.type = "privacy"_n;
row.coopname = coopname;
row.program_id = 0;
row.draft_id = 4;
row.draft_id = 3;
});
}
@@ -1,3 +1,5 @@
#include <array>
/**
* @brief Миграция данных системы
* Выполняет миграцию данных системы при обновлении контракта.
@@ -10,14 +12,62 @@
void soviet::migrate() {
require_auth(_soviet); // Проверяем авторизацию
// Миграция program_type для кооператива voskhod, программа id=4 -> blagorost
eosio::name coopname = "voskhod"_n;
programs_index programs(_soviet, coopname.value);
auto program_it = programs.find(4);
if (program_it != programs.end() && program_it->program_type != "blagorost"_n) {
programs.modify(program_it, _soviet, [&](auto &pr) {
pr.program_type = "blagorost"_n;
});
// Откат program_type для кооператива voskhod, программа id=4: blagorost -> capital.
// Предыдущая версия этой миграции переименовывала programs.program_type в
// 'blagorost', но coagreements.type для той же программы (program_id=4) всегда
// оставался 'capital' и никогда не переименовывался — таблицы разъехались.
// 'capital' — каноничное значение (используется coagreements.type, contract-
// константой _capital_program и всем controller-кодом); эта миграция приводит
// programs.program_type к нему же. Идемпотентно.
{
eosio::name coopname = "voskhod"_n;
programs_index programs(_soviet, coopname.value);
auto program_it = programs.find(4);
if (program_it != programs.end() && program_it->program_type != "capital"_n) {
programs.modify(program_it, _soviet, [&](auto &pr) {
pr.program_type = "capital"_n;
});
}
}
// ────────────────────────────────────────────────────────────────────────
// Разовая очистка ФАНТОМНЫХ участников кооператива fgrtejiwnynn (инцидент
// 2026-06-04). Удаляем строки soviet::participants для 6 дублей-аккаунтов,
// оставшихся на цепи после 3-кратного прогона install.interactor (см.
// ~/gorozhane-dup-install-cleanup.md). Эти участники не голосовали и не
// входят в совет (createboard для них не выполнялся).
//
// Список username синхронизирован с registrator::migrate и ledger2::migrate.
// Дедуп-гард: удаляем фантомов ТОЛЬКО если в коопе остаётся хотя бы один
// НЕ-фантомный участник (никогда не опустошаем реестр). Идемпотентно.
{
const eosio::name PHANTOM_COOP = "fgrtejiwnynn"_n;
const std::array<eosio::name, 6> PHANTOMS = {
"bbsezpgufvmm"_n, "errwcgjwverm"_n, "hcfsluqsfehw"_n,
"kgkzdadfpzki"_n, "nzuyijobapsv"_n, "tplwfwbujugq"_n,
};
auto is_phantom = [&](eosio::name u) {
for (const auto& p : PHANTOMS) if (p == u) return true;
return false;
};
participants_index participants(_soviet, PHANTOM_COOP.value);
// Гарантируем, что после удаления останется ≥1 реальный участник.
uint64_t real_participants = 0;
for (auto it = participants.begin(); it != participants.end(); ++it) {
if (!is_phantom(it->username)) real_participants++;
}
if (real_participants > 0) {
for (const auto& u : PHANTOMS) {
auto it = participants.find(u.value);
if (it != participants.end()) {
participants.erase(it);
}
}
}
}
@@ -60,8 +60,12 @@ void soviet::voteagainst(
auto board = get_board_by_type_or_fail(coopname, "soviet"_n);
eosio::check(board.is_voting_member(username), "У вас нет права голоса");
decision -> check_for_any_vote_exist(username);
decision -> check_for_any_vote_exist(username);
// Голос «против» только фиксируется. Отрицательный консенсус НЕ затирает
// решение автоматически: отказ при достигнутом большинстве «против» проводит
// председатель явным действием declinedec (развязано с авто-отменой по сроку
// cancelexprd). Так принятое отрицательно решение не исчезает само из повестки.
add_vote_against(coopname, username, decision->id);
};
@@ -3,10 +3,11 @@
# программу «Цифровой Кошелёк» (SHARE_FUND_PAY).
#
# Четырёхактовый процесс с одной авторизацией советом. Запись заявки живёт в
# таблице `withdraws` от создания до выплаты или отказа. На закрывающем
# действии completewthd срабатывает одна операция o.wal.wthcpl — сжигание
# заблокированной суммы (BURN_BLOCKED w.wal.share): деньги уходят из системы
# пайщику банковским переводом, получателя на цепи нет.
# таблице `withdraws` от создания до выплаты или отказа. На подаче заявки сумма
# переводится в кошелёк-резерв (WITHDRAW_PENDING, w.wal.wpend). На закрывающем
# действии completewthd срабатывает операция o.wal.wthcpl — сжигание суммы из
# резерва (BURN w.wal.wpend): деньги уходят из системы пайщику банковским
# переводом, получателя на цепи нет.
#
# Источники правды в коде:
# • cpp/wallet/wallet.hpp — actions
@@ -43,8 +44,8 @@ actions:
actor: contributor
role: opener
purpose: >
Пайщик создаёт заявку на возврат паевого взноса. Контракт блокирует
сумму на кошельке SHARE_FUND_PAY (статус available → blocked) и
Пайщик создаёт заявку на возврат паевого взноса. Контракт переводит
сумму с кошелька SHARE_FUND_PAY в кошелёк-резерв WITHDRAW_PENDING и
создаёт повестку в совете о возврате с callback'ом авторизации.
- name: wallet::authwthd
human: Авторизовать выплату
@@ -60,16 +61,17 @@ actions:
role: closer
purpose: >
Gateway подтвердил списание средств в пользу пайщика. Это закрывающее
действие: применяется операция o.wal.wthcpl (BURN_BLOCKED w.wal.share)
заблокированная сумма сжигается, в бухгалтерии проходит обратная
действие: применяется операция o.wal.wthcpl (BURN w.wal.wpend)
— сумма из резерва сжигается, в бухгалтерии проходит обратная
проводка Дт 80 / Кт 51. Запись заявки удаляется.
- name: wallet::declinewthd
human: Отклонить
actor: soviet
role: reject
purpose: >
Отказ на любом из этапов до выплаты. Сумма разблокируется на кошельке
пайщика (UNBLOCK через o.wal.wthdec), запись заявки удаляется.
Отказ на любом из этапов до выплаты. Сумма возвращается из резерва на
кошелёк пайщика (o.wal.wthdec: WITHDRAW_PENDING → SHARE_FUND_PAY),
запись заявки удаляется.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
entity: wallet::withdraw
@@ -80,8 +82,8 @@ states:
- name: pending
human: Ожидает решения совета
description: >
Заявка создана, сумма заблокирована на кошельке пайщика (available → blocked),
повестка отправлена в совет.
Заявка создана, сумма переведена в кошелёк-резерв (SHARE_FUND_PAY →
WITHDRAW_PENDING), повестка отправлена в совет.
kind: normal
- name: authorized
human: Выплата отправлена
@@ -92,15 +94,15 @@ states:
- name: completed
human: Возврат выплачен
description: >
Gateway подтвердил списание, заблокированная сумма сжигается на кошельке
пайщика (BURN_BLOCKED w.wal.share). В бухгалтерии прошла обратная
Gateway подтвердил списание, сумма из резерва сжигается
(BURN w.wal.wpend). В бухгалтерии прошла обратная
проводка Дт 80 / Кт 51. Запись заявки удалена.
kind: final
- name: removed
human: Отклонено
description: >
Возврат не состоялся: сумма разблокирована (UNBLOCK через o.wal.wthdec),
запись удалена.
Возврат не состоялся: сумма возвращена из резерва на кошелёк пайщика
(o.wal.wthdec: WITHDRAW_PENDING → SHARE_FUND_PAY), запись удалена.
kind: virtual
virtual: true
@@ -172,7 +174,7 @@ scenario:
- Сумма возврата ≤ доступного остатка.
post:
- В таблице withdraws создана запись со статусом `pending`.
- На кошельке пайщика сумма переведена available → blocked.
- Сумма переведена с SHARE_FUND_PAY в кошелёк-резерв WITHDRAW_PENDING.
- Совет получил повестку о возврате.
- step: 2
@@ -197,13 +199,13 @@ scenario:
action: wallet::completewthd
description: >
Gateway подтверждает списание средств в пользу пайщика. Контракт
применяет ledger2-операцию o.wal.wthcpl (BURN_BLOCKED w.wal.share с
применяет ledger2-операцию o.wal.wthcpl (BURN w.wal.wpend с
обратной проводкой Дт 80 / Кт 51) и удаляет запись заявки.
pre:
- Заявка в статусе `authorized`.
- Gateway подтвердил списание.
post:
- Заблокированная сумма сожжена с SHARE_FUND_PAY (w.wal.share).
- Сумма из резерва сожжена (WITHDRAW_PENDING, w.wal.wpend).
- В ledger2 применена операция o.wal.wthcpl.
- Запись заявки удалена.
@@ -214,8 +216,8 @@ scenario:
actor: soviet
description: >
Совет принял отрицательное решение либо Gateway отклонил платёж.
Сумма разблокирована на кошельке пайщика (UNBLOCK через o.wal.wthdec),
запись удалена.
Сумма возвращена из резерва на кошелёк пайщика (o.wal.wthdec:
WITHDRAW_PENDING → SHARE_FUND_PAY), запись удалена.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
documents:
@@ -234,45 +236,45 @@ documents:
# ── Секция 6. Операции ──────────────────────────────────────────────────────
operations:
- ledger_code: o.wal.wthreq
human_name: Блокировка паевого под запрос на возврат
wallet_op: BLOCK
human_name: Резервирование паевого под запрос на возврат
wallet_op: TRANSFER
wallet_from: w.wal.share
wallet_to: ~
wallet_to: w.wal.wpend
debit: ~
credit: ~
amount_ref: withdraw.quantity
triggered_by: wallet::createwthd
description: >
На создании заявки контракт блокирует сумму на кошельке пайщика
(SHARE_FUND_PAY): available → blocked. Бухгалтерская проводка не
создаётся — движение внутри одного бухсчёта 80.
На создании заявки контракт переводит сумму с кошелька пайщика
(SHARE_FUND_PAY) в кошелёк-резерв (WITHDRAW_PENDING). Бухгалтерская
проводка не создаётся — движение внутри одного бухсчёта 80.
- ledger_code: o.wal.wthdec
human_name: Разблокировка паевого после отклонения запроса на возврат
wallet_op: UNBLOCK
wallet_from: w.wal.share
wallet_to: ~
human_name: Снятие резерва паевого после отклонения запроса на возврат
wallet_op: TRANSFER
wallet_from: w.wal.wpend
wallet_to: w.wal.share
debit: ~
credit: ~
amount_ref: withdraw.quantity
triggered_by: wallet::declinewthd
description: >
Зеркало REQUEST_WITHDRAW: blocked → available на кошельке пайщика.
Бухгалтерская проводка не создаётся.
Зеркало REQUEST_WITHDRAW: сумма возвращается из резерва (WITHDRAW_PENDING)
на кошелёк пайщика (SHARE_FUND_PAY). Бухгалтерская проводка не создаётся.
- ledger_code: o.wal.wthcpl
human_name: Возврат паевого взноса пайщику
wallet_op: BURN_BLOCKED
wallet_from: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
wallet_op: BURN
wallet_from: w.wal.wpend # кошелёк-резерв возвратов
wallet_to: ~ # деньги уходят из системы, получателя на цепи нет
debit: 80 # Паевой фонд (складочный капитал)
credit: 51 # Расчётный счёт
amount_ref: withdraw.quantity
triggered_by: wallet::completewthd
description: >
Обратная операция к o.wal.depcpl: заблокированная сумма сжигается с
кошелька пайщика «ЦПП Цифровой Кошелёк» (w.wal.share) — деньги уходят
из системы банковским переводом пайщику. На цепи получателя нет.
Обратная операция к o.wal.depcpl: зарезервированная сумма сжигается с
кошелька-резерва (w.wal.wpend) — деньги уходят из системы банковским
переводом пайщику. На цепи получателя нет.
В бухгалтерии прошла проводка Дт 80 / Кт 51 — паевой фонд уменьшился,
деньги ушли с расчётного счёта пайщику.
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/contracts",
"version": "2026.5.23-4",
"version": "2026.7.21-1",
"private": true,
"type": "module",
"scripts": {
@@ -9,9 +9,6 @@
"build:one": "./build.sh $1",
"build:one:test": "contract=$1 mode=test ./build.sh",
"enter": "./boot/scripts/enter.sh",
"testnet": "git checkout testnet && git merge dev && lerna publish prerelease && git push && git checkout dev && git merge testnet",
"production": "git checkout main && git merge -X theirs testnet && lerna publish --conventional-commits --conventional-graduate && git push && git checkout dev && git merge main",
"docs-publish": "pnpm gh-pages --nojekyll -d docs/site --repo git@github.com:coopenomics/monocoop.git",
"standards:dev": "pnpm --filter @coopenomics/standards-site dev",
"standards:build": "pnpm --filter @coopenomics/standards-site build",
"standards:preview": "pnpm --filter @coopenomics/standards-site preview"
+7 -6
View File
@@ -34,6 +34,13 @@ SMTP_USERNAME=
SMTP_PASSWORD=
EMAIL_FROM="'КО-ПОЧТАЛЬОН' <mail@coopenomics.world>"
# Напоминатель об отчёте по авансу под отчёт (extensions/expenses).
# Интервал тика сканера зеркала расходов (мс). Дефолт 6 ч. Письмо пайщику —
# не чаще раза в неделю (троттлит идемпотентность Центра уведомлений).
EXPENSE_ADVANCE_REMINDER_INTERVAL_MS=21600000
# Сколько дней аванс может висеть без отчёта до первого напоминания. Дефолт 3.
EXPENSE_ADVANCE_REMINDER_GRACE_DAYS=3
# BLOCKHAIN
BLOCKCHAIN_RPC=http://127.0.0.1:8888
CHAIN_ID=cae86058a6d8698833afb474ab8a5ad8599c6cf54f9ebcf275dbac7055c16fe1
@@ -71,12 +78,6 @@ SENTRY_DSN=
GITHUB_TOKEN=
# OpenSearch (поисковая система документов, опционально)
OPENSEARCH_NODE=https://opensearch:9200
OPENSEARCH_ENABLED=false
OPENSEARCH_USERNAME=admin
OPENSEARCH_PASSWORD=
LIVEKIT_URL=
LIVEKIT_API_KEY=
LIVEKIT_API_SECRET=
+6
View File
@@ -1,3 +1,9 @@
# Convert text file line endings to lf
* text eol=lf
*.js text
# Бинарные шрифты — без EOL-нормализации (перекрывает "* text eol=lf")
*.ttf binary
*.otf binary
*.woff binary
*.woff2 binary
+1 -1
View File
@@ -472,7 +472,7 @@ BaseExtModule
- `ChairmanBlockchainAdapter` — взаимодействие с контрактом `soviet`
- **Резолверы**: `ApprovalResolver`, `ChairmanOnboardingResolver`
- **Крон**: Проверка решений каждые N минут (`checkInterval`)
- **Конфигурация**: `checkInterval`, `cancelApprovedDecisions`, `onboarding_*` — флаги прохождения мастера настройки
- **Конфигурация**: `checkInterval`, `onboarding_*` — флаги прохождения мастера настройки
### `capital` — Благорост (целевая программа)
- **Структура**: `domain/`, `infrastructure/`, `application/` — самое крупное расширение
+8
View File
@@ -27,11 +27,19 @@ RUN apt-get update && apt-get install -y \
libharfbuzz-dev \
libcairo2 \
libpango1.0-dev \
fontconfig \
&& rm -rf /var/lib/apt/lists/* \
&& python3 -m venv /venv \
&& /venv/bin/pip install WeasyPrint==67 \
&& rm -rf /root/.cache/pip
# Шрифт Arial устанавливаем СИСТЕМНО (а не встраиваем base64 в каждый html).
# Иначе weasyprint парсил бы ~270 КБ TTF на каждый рендер (≈1.3 сек/документ).
# Системный шрифт fontconfig парсит один раз и кеширует. Файл тот же —
# глифы/вёрстка идентичны прежним.
COPY assets/fonts/arial.ttf /usr/share/fonts/truetype/arial/arial.ttf
RUN fc-cache -f && fc-match Arial
# Установка пути к виртуальному окружению в PATH
ENV PATH="/venv/bin:$PATH"
Binary file not shown.
@@ -0,0 +1,126 @@
import { createHash } from 'node:crypto';
import type { DataSource } from 'typeorm';
import mongoose from 'mongoose';
import config from '~/config/config';
type MigrationLogger = {
info: (message: string) => void;
error: (message: string) => void;
warn: (message: string) => void;
};
const CAPITAL_DOC_REGISTRY_ID = 994;
const TARGET_COOPNAME = 'voskhod';
const COLLECTION_NAME = 'doc_private_data';
const capitalProgramPrivateData = {
generator_program_purpose:
"развитию информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) – под названием “Кооперативная Экономика”, с целью самоорганизации и интеграции в социально-экономическую среду Российской Федерации., на основе международных кооперативных принципов и законодательства Российской Федерации",
eoap_definition:
"информационная экосистема, интегрируемая в социально-экономическую среду Российской Федерации, состоящая из комплекса программных продуктов на базе технологии распределенного реестра, обеспечивающих широкое экономическое и социальное взаимодействие физических и юридических лиц, включая нерезидентов различных юрисдикций и организационно-правовых форм, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских кооперативов (обществ) под названием “Кооперативная Экономика”",
generator_task_goal:
"центра привлечения и интеграции передовых инновационных цифровых разработок, а также экономических и социальных методов и решений",
idea_unit_cost:
"50",
idea_unit_cost_words:
"пятьдесят",
blagorost_goal_expansion:
"развитии информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) – под названием “Кооперативная Экономика”, с целью самоорганизации и интеграции в социально-экономическую среду Российской Федерации",
blagorost_goal_reason:
"вследствие увеличения количества Участников информационной кооперативной экосистемы - ЕОАП - для расширения и повышения социальной эффективности их экономического взаимодействия в некоммерческом формате",
blagorost_task_expansion:
"расширение участников ЕОАП - информационной кооперативной экосистемы как центра экономического взаимодействия в некоммерческом формате",
blagorost_task_development:
"развитие ЕОАП как центра привлечения и интеграции инновационных цифровых разработок, а также экономических и социальных методов и решений",
return_source_description:
"аппаратно-программная сеть узлов распределенного реестра в формате «СМЭВ+SWIFT», построенная на принципах самоорганизации и самофинансирования деятельности технологической инфраструктуры ЕОАП, обеспечивающей консенсус ее распределенных узлов по формированию базового продукта ЕОАП - полного цикла документооборота по синхронному взаимодействию пайщиков и кооперативов - участников экосистемы ЕАОП - через использование цифровых контрактов, с одновременным выполнением функций нотариата и учета финансовых и юридических событий (подробнее на Сайте)",
return_additional_source:
"взносы пользователей ЕОАП, его отдельных программных продуктов и приложений, переданных Обществу или создаваемых в рамках Общества, которые интегрируются в ЕОАП"
};
function stableStringify(value: unknown): string {
if (value === null || typeof value !== 'object') return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(',')}]`;
const entries = Object.keys(value as Record<string, unknown>)
.sort()
.map((key) => {
const v = (value as Record<string, unknown>)[key];
return `${JSON.stringify(key)}:${stableStringify(v)}`;
});
return `{${entries.join(',')}}`;
}
function calculateSha256(value: unknown): string {
return createHash('sha256').update(Buffer.from(stableStringify(value), 'utf8')).digest('hex').toUpperCase();
}
export default {
name: 'Backfill capital_program_doc_data_hash for voskhod',
validUntil: new Date('2027-05-01T00:00:00Z'),
async up({ dataSource, logger }: { dataSource: DataSource; logger: MigrationLogger }): Promise<boolean> {
if (config.coopname !== TARGET_COOPNAME) {
logger.info(`COOPNAME=${config.coopname}: миграция предназначена только для ${TARGET_COOPNAME}, пропускаю.`);
return true;
}
const hash = calculateSha256(capitalProgramPrivateData);
let mongo: mongoose.Connection | null = null;
try {
mongo = await mongoose.createConnection(config.mongoose.url).asPromise();
const db = mongo.db;
if (!db) {
throw new Error('Подключение к MongoDB установлено, но db не инициализирована');
}
const collection = db.collection(COLLECTION_NAME);
await collection.createIndex({ hash: 1 }, { unique: true });
await collection.updateOne(
{ hash },
{
$setOnInsert: {
hash,
registry_id: CAPITAL_DOC_REGISTRY_ID,
payload: capitalProgramPrivateData,
_created_at: new Date(),
},
},
{ upsert: true }
);
const result = await dataSource.query(
`UPDATE extensions
SET config = jsonb_set(COALESCE(config, '{}'::jsonb), '{capital_program_doc_data_hash}', to_jsonb($1::text), true),
updated_at = NOW()
WHERE name = 'capital'`,
[hash]
);
logger.info(`capital_program_doc_data_hash=${hash} сохранён для ${TARGET_COOPNAME}; extensions updated=${result?.[1] ?? 'unknown'}.`);
return true;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error(`Ошибка миграции capital_program_doc_data_hash для ${TARGET_COOPNAME}: ${message}`);
return false;
} finally {
await mongo?.close();
}
},
async down({ dataSource, logger }: { dataSource: DataSource; logger: MigrationLogger }): Promise<boolean> {
if (config.coopname !== TARGET_COOPNAME) {
return true;
}
await dataSource.query(
`UPDATE extensions
SET config = COALESCE(config, '{}'::jsonb) - 'capital_program_doc_data_hash',
updated_at = NOW()
WHERE name = 'capital'`
);
logger.warn('capital_program_doc_data_hash удалён из config capital; запись doc_private_data оставлена как идемпотентный audit trail.');
return true;
},
};
+3 -1
View File
@@ -1,4 +1,6 @@
{
"watch": ["./src"],
"ext": "ts"
"ext": "ts",
"delay": 2000,
"signal": "SIGTERM"
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "create-nodejs-express-app",
"version": "2026.5.23-4",
"version": "2026.7.21-1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "create-nodejs-express-app",
"version": "2026.5.23-4",
"version": "2026.7.21-1",
"license": "MIT",
"dependencies": {
"@a2seven/yoo-checkout": "^1.1.4",

Some files were not shown because too many files have changed in this diff Show More