Двухподписный акт падал у второго подписанта: "Хэш метаданных не
совпадает с существующим подписанным документом". Причина — в storage
фабрики, не в генерации (генерация версий в разное время корректна).
saveDraft ключевал черновик только по hash (= sha256 PDF-тела). Тело
впечатывает created_at лишь до минуты, а meta.block_num тянется
getCurrentBlock() на каждой генерации. Две генерации одного акта в одной
минуте → один hash, разный block_num → upsert $set затирал предыдущую
версию meta. Второй подписант через buildDocumentAggregate читал черновик
по doc_hash и получал чужую (позднюю) meta → meta_hash не сходился с
подписью первого → SDK signDocument отбивал подпись.
Фикс (storage-only, генерацию не трогаем):
- MongoDBConnector.saveDraft — компаунд-ключ (hash + meta.block_num):
все версии черновика сосуществуют, ничего не теряется.
- DocumentRepository.findByHash(hash, block_num?) — тянет точную версию
по block_num, фолбэк на hash-only для легаси-черновиков.
- DocumentAggregator.buildDocumentAggregate — отдаёт второму подписанту
ровно ту версию, которую подписал первый (по signedDoc.meta.block_num).
Общий фикс класса багов для всех двухподписных документов (capital/meet/
agreement), не только marketplace-АПП.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Стол выдачи был q-table на сотни строк — оператор не видел «кому что
отдать». Переделано в карточки по заказчику: одна карточка = получатель
(ФИО + аккаунт), внутри список единиц, разнесённый по стадии:
«К выдаче» (ACCEPTED_TO_COOP, открыть подписью председателя) и
«Ждут получения» (READY_TO_RECEIVE, ждём заказчика). Заказчики с тем,
что можно выдать сейчас, — наверх; итог по сумме и числу позиций.
Backend без изменений: два существующих статуса ленты и есть стадии
выдано/не-выдано с точки зрения оператора. QR-скан и шаг открытия —
прежние.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
marketplaceListSupplierPickupOrders обогащал заказы без
withParticipantNames — supplier_name/orderer_name приходили null, и
фронт показывал account («ant») вместо ФИО. Резолвер уже ограничен
ролью Receiving и членством в КУ, поэтому резолв имён здесь допустим
(как в приёмке/выдаче). Схема не изменилась — codegen не нужен.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BaseButton рендерит default-слот только при !iconOnly, а q-menu был
вложен именно в него — для icon-only кнопки меню не монтировалось.
Перенёс q-menu в слот #icon-left (рендерится всегда), меню привязано
к кнопке. Поправил вложенность q-list/q-item.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Плоский список «позиция + поле Полка + кнопки» был непонятен: не было
ощущения, ГДЕ что лежит и зачем раскладка. Переделано в доску:
- колонка «Поступило» — принятое, ещё не разложенное;
- колонки-полки — что физически лежит на каждой полке;
- раскладка = перетащить карточку на полку (нативный DnD) или меню «⋮»;
- «+ Полка» создаёт новую полку; снять с полки = вернуть в «Поступило»;
- этикетка и раскладка по количеству — в меню карточки;
- штрих-код показан в карточке; отдельный лист — только для печати.
Цель страницы теперь читается: разложить так, чтобы при выдаче
заказчику сразу найти, на какой полке заказ. Backend без изменений
(assignInventoryShelf/splitInventory/generateInventoryLabel), без новых
зависимостей (DnD на HTML5). Канон: токены --p-*, убран хардкод #b3261e.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Раскладка по полкам теперь перераспределяет ВЕСЬ непромаркированный
RECEIVED-пул заказа, а не один кусок: оператор может собрать имущество
с полок обратно (merge), разложить иначе (re-split) или перенести —
в одном действии «Разложить». Промаркированные единицы (штрих-код)
фиксированы и в пул не входят.
Backend: splitInventory собирает пул по order_id (непромаркированные
RECEIVED), сверяет сумму долей с суммой пула, схлопывает лишние куски
(новый repo.deleteById). Сигнатура GraphQL не изменилась — codegen не нужен.
Desktop: штрих-код показывается в той же строке, что имущество и полка
(нижний печатный лист скрыт на экране, разворачивается только при печати);
диалог раскладки предзаполняется текущим раскладом заказа по полкам.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Поставщик и председатель КУ видят и подписывают доставку ЦЕЛИКОМ одной
кнопкой, а не каждую единицу имущества по отдельности. Под капотом по каждому
акту приёма-передачи — отдельный документ и отдельная транзакция (блокчейн не
проведёт всё одной tx); подпись группы = последовательный цикл по receptions с
прогрессом «подписано N из M».
- shared/lib/marketplace/groupAplReceptions: группировка актов в сводную
поставку (ПВЗ: поставщик+КУ+способ+статус; поставщик: КУ+способ+статус),
агрегация строк по товару + сумма
- стол ПВЗ «Требуют подписи» и стол поставщика «Подпись приёмки» → сводные
карточки, одна кнопка на группу
- оба диалога подписи принимают группу и подписывают циклом, показывая прогресс
и частичные ошибки
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ожидаемые поставки агрегируются по поставщику (одна карточка = один
поставщик на КУ): ФИО, состав по товарам (одинаковый товар суммируется),
способы доставки, общая сумма
- диалог приёмки группирует единицы по товару (строки по заказчикам внутри),
факт/цена правятся per-Order — бухгалтерия не меняется
- предлагаем к приёмке только заказы, чья партия ещё SUPPLY_PREPARED: заказы
партий в RECEPTION_IN_PROGRESS (ждут подписи) повторно не показываем — иначе
create падал «ожидался SUPPLY_PREPARED» при повторном приезде поставщика
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Карточка показывала аккаунт вместо имени — ФИО берём из состава (supplier_name
заказа), аккаунт остаётся в AccountBadge. Сумма перенесена из верхней строки в
итоговую «Сумма поставки» под составом (рубли — это итог, а не «привезёт по
факту», что относится к составу).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Список ожидаемых поставок перевёрстан на тот же BaseCard-grid, что и акты на
подпись (Avatar + имя + AccountBadge, бейдж способа доставки, сумма mono, состав
в surface-2 блоке с получателем под товаром). Унифицированы классы __card* —
один паттерн карточки для поставок и актов (DRY).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Самовывоз по факту больше не отдельной таблицей: для ПВЗ нет разницы,
сформировал поставщик партию заранее или привезёт самовывозом — и то и другое
дойдёт до участка. Слиты в один computed expectedDeliveries; самовывоз показан
способом доставки «Самовывоз» без даты формирования.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
«Требуют подписи» перевёрстаны на BaseCard-grid: Avatar + имя поставщика +
AccountBadge в шапке, статус-бейдж справа, состав в surface-2 блоке, действие
подписи в футере на hairline-разделителе. Хинт переписан человеческим языком.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Принятые кооперативом акты (ACCEPTED_TO_COOP) уже на складе — на столе приёмки
не нужны; таблица АПП заменена карточками только требующих подписи актов
(PENDING_*), показывается лишь когда есть что подписать. Убран дублирующий
заголовок «Ожидают приёмки» (раздел назван в шапке), хинт сжат до двух строк,
пустое состояние — в каноне через EmptyState.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Оператор не прыгает между «жду» и «принимаю»: единый стол показывает что/когда/кому
везут (дата формирования партии + получатель в карточках состава), запуск приёмки —
только скан QR/ввод кода, тут же статусы и подпись актов. Удалён избыточный
KPI-дашборд incoming-shipments и сама страница (api оставлен). defaultRoute → reception.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Десктоп к приёмочной модели склада (Путь A):
- Приёмка партии: убраны кнопки-лаунчеры («Принять партию», «Принять
самовывоз»). Приёмка запускается ТОЛЬКО сканом QR/вводом кода (идентификация
обязательна). Ожидающие партии и самовывоз показаны карточками со составом
(«что везут» — товар + кол-во) без проваливания.
- «Маркировка имущества» → «Раскладка и маркировка»: список принятых позиций
склада; на строке — полка (свободный input), «Разложить» (split одной позиции
по нескольким полкам), «Этикетка» (опциональный штрих-код). Снесены выбор
партии и стратегии PER_ORDER/UNIT/PACKAGE. Печать этикеток сохранена.
- Склад моего КУ: показывает всё принятое (статус RECEIVED «Принято»), колонка
«Полка», штрих-код опционален (—), сортировка/возраст по received_at.
SDK-обёртки inventory перегенерированы под новые мутации.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Корень: склад показывал только промаркированное (inventory рождался лишь на
маркировке). Теперь позиция склада рождается на приёмке кооперативом по акту
(ACCEPTED_TO_COOP), статус RECEIVED — склад показывает ВСЁ принятое, штрих-код
и полка опциональны.
Бэкенд (controller, DB-only composite, synchronize + миграция v9):
- inventory: barcode_value/format/labeled_* → nullable; ADD shelf, received_at,
received_by_operator_account; новый статус RECEIVED (стартовый).
- apl-reception.signAsChairman материализует склад: на каждый принятый Order
(fact_quantity>0) позиция RECEIVED (qty=факт, expiry=received_at+warranty_secs),
идемпотентно по countByOrder.
- inventory-label сервис переписан под раскладку/маркировку: assignShelf,
splitInventory (разложить позицию по нескольким полкам, RECEIVED-only),
generateLabel (наклеить штрих-код RECEIVED→LABELED). Снесены стратегии
PER_ORDER/UNIT/PACKAGE и пакетная маркировка партии.
- мутации: marketplaceAssignInventoryShelf / marketplaceSplitInventory /
marketplaceGenerateInventoryLabel (взамен labelInventory/labelShipmentInventory).
- крон списания скоропорта сканирует RECEIVED+LABELED.
Codegen: schema → zeus-клиент → sdk build. tsc controller зелёный.
Десктоп (стол ПВЗ) — следующим коммитом в этой ветке.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Кнопка сканирования кода передачи переехала из строки «Ожидают приёмки»
в шапку стола (канон Teleport, как «Мой код для ПВЗ» в подготовке
отгрузки) — единое предсказуемое место сверху.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Новый пункт меню «Отгрузить партию» после «Подготовки отгрузки»: один
account-bound Pickup-QR во всю страницу (зеркало «Получить заказ» заказчика).
- Обобщил виджет ReceiveCode → HandoffCode (kind=Receive|Pickup, тексты в copy.ts):
один контент/диалог на оба стола. Подключил у заказчика (страница + диалоги
«Мои заказы»/деталь) и у поставщика (страница + диалог из шапки «Подготовки
отгрузки» вместо inline-QR).
- Снёс мёртвый виджет CreateOrderDialog (дубль MarketplaceCatalog/OrderCreateDialog).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- «Получить заказ» вернул из переадресации в диалог (ReceiveCodeDialog),
кнопка в шапке и на «Моих заказах», и на детали заказа — в одном месте;
отдельная страница меню остаётся. Общий ReceiveCodeContent (DRY).
- OfferGallery: точки-навигация выключены по умолчанию (убраны со всех экранов).
- Вынес SupplyPartyCard — единая карточка партии для заказчика
(«Коллективный заказ») и поставщика («Входящие заказы»), вместо дублей.
- Подчистил мёртвые стили карусели/плейсхолдера в CatalogOfferCard и
странице оферты.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Убрана разнобойная вёрстка: накопитель показывался прогрессом, а принятая
партия — метрик-сеткой с сырым UUID партии + вложенными OrderCard'ами (две
разные сущности на одинаковые по смыслу поставки). Теперь все партии рисуются
ОДНОЙ карточкой: шапка с бейджем этапа, прогресс-бар всегда, состав компактными
строками (поставщику важен общий объём, не кто заказал — имена справочно, без
дробления на отдельные карточки), действия «Принять/Отклонить» — только пока
партия копится. Удалены метрик-сетка, UUID партии и вложенные OrderCard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Вынесен переиспользуемый OfferGallery (карусель изображений) вместо inline-
каруселей: каталог, деталь оферты и деталь заказа теперь используют один
виджет (DRY). На детали заказа вместо одиночного q-img — полноценный слайдер.
- Деталь заказа: больше воздуха (увеличены gap'ы шапки/фактов, обложка 144px).
- Кнопка «Получить заказ» (ведёт на страницу QR) продублирована в шапке
«Моих заказов» и в действиях детали заказа — код выдачи под рукой везде,
не только на отдельной странице.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Детальная страница заказа: добавлена обложка товара (догруз оферты по
offer_id), хронология переведена на канон-ленту ActivityTimeline вместо
плоских строк, добавлен воздух над кнопкой «К моим заказам».
- Подпись на странице «Получить заказ»: убрано непонятное «показать заранее»
→ «показать с экрана телефона или с распечатки».
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Убрал кнопку «Мой код получения» из шапки «Моих заказов» и вынес в отдельный
пункт меню «Получить заказ» (OrdererReceiveCode) после «Коллективного заказа»:
одна страница с account-bound QR-кодом на всю ширину и пояснением, больше
ничего. Так код очевидно findable — пайщику не нужно объяснять, где искать
его на пункте выдачи; пункт всегда виден в меню стола.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Сократили навигацию заказчика: получение перестало быть отдельной страницей.
- Удалён стол «Готово к получению» (OrdererReadyToReceive) из навигации.
- «Мой код получения» (account-bound QR) вынесен в шапку «Моих заказов».
- «Подписать и получить» (финальная подпись signiss2) — действие прямо в
карточке заказа со статусом READY_TO_RECEIVE; диалог финализации переехал
в MyOrders. READY_TO_RECEIVE стал обычным этапом общего списка.
- Клик по карточке открывает детальную страницу заказа (новый скрытый роут
marketplace-order-detail, OrdererOrderDetail) на marketplaceGetOrder: состав,
ПВЗ, хронология этапов, факт выдачи, те же действия.
- OrderCard: проп openable (клик по телу → emit open), действие receive.
Чистый фронт: marketplaceGetOrder/ListMyReadyToReceive/finalizeIssuance уже
есть, codegen не нужен. MarketplaceOrderView самодостаточен для диалога
(issuance_fact в общем orderSelector).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Убрал разговорные обороты («исчезает отсюда», «копится») из пустого
состояния и подсказки страницы; текст переписан нейтрально и грамотно.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Страница-трекер сбора показывает лишь партии на этапе накопления и
подготовки поставки (ACTIVE…ACCEPTED_TO_COOP). Открытые на выдачу
(READY_TO_RECEIVE и далее), полученные, возвращённые и отменённые заказы
уходят со страницы — её цель наблюдать накопление, а не историю.
Получение/история — на «Моих заказах». Пустое состояние переформулировано.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Переделал страницу: все партии (на сборе и уже принятые) рендерятся ОДНИМ
шаблоном — шапка (товар + КУ + бейдж этапа), прогресс-бар сбора, свои заказы
строками. Убраны разные по вёрстке виды (метрик-сетка + большие OrderCard для
принятых), убран сырой UUID партии. Отличие стадий — только заполнение бара и
подпись этапа, а не разные сущности.
Бэкенд: group_accumulated_quantity/group_min_volume теперь заполняются на любой
стадии — для принятых партий коллективный объём берётся суммой по cycle_id
(новый агрегат sumByCycleIds), для собирающихся — суммой активного пула пары
оферта×КУ. Так прогресс-бар осмыслен на каждой карточке.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
В партии-накопителе выводил полные OrderCard, из-за чего название товара/КУ
повторялось в шапке партии и внутри — выглядело как забытая старая карточка.
Привёл к канону стола поставщика: накопитель показывает компактные строки
своих заказов (без больших карточек), большие OrderCard остаются только в
сформированной (принятой) партии. Накопитель и сформированная партия теперь
визуально разной формы, не читаются как близнецы.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Стол заказчика «Коллективный заказ» (бывш. «Сводный заказ») теперь зеркалит
карточку поставщика: партии-накопители показывают коллективный прогресс сбора
(накоплено всеми пайщиками / целевой минимум КУ), без ФИО других заказчиков.
КУ выводится человекочитаемым именем (delivery_point_name) вместо служебного
braname.
Бэкенд: Order-DTO обогащается group_accumulated_quantity (сумма ACTIVE по паре
оферта×КУ всех пайщиков) и group_min_volume (целевой минимум КУ) — новый
батч-агрегат sumActiveByOfferBranch + флаг withGroupProgress в display-сервисе.
Заодно ленты заказов (my/supplier) обогащаются именами участников
(withParticipantNames): на столе поставщика «кто заказал» теперь реально
показывает ФИО, а не аккаунт.
Codegen: generate-schema/client + zeus-клиент sdk и controller.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Во время подписания+перехода (redirecting) одновременно крутились два
спиннера: полностраничный q-inner-loading по центру и спиннер кнопки
«Подписать оферту» внутри gate. Прячем gate на время redirect — остаётся
один полностраничный лоадер.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
В подготовке поставки поставщик сам привозит товар на ПВЗ — это «самоввоз»,
не «самовывоз» (самовывоз — когда забирает получатель).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Колонка «Кто заказал» (входящие заказы поставщика) и «Заказчик» (выдача
оператора) показывали полное имя или, при пустом, username. Теперь —
сокращённое «Фамилия И.О.» через новый строковый хелпер formatShortFio
(заказчик всегда физлицо, поэтому сокращение безопасно). Полное ФИО
с бэкенда (orderer_name) остаётся доступным для документов.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Карточка в «Модерации» теперь кликабельна (курсор-pointer + hover) и
ведёт на полную страницу предложения в рамках стола администратора
(market-admin, hidden-маршрут marketplace-admin-offer-detail, Order:read:all)
— без перехода на стол заказчика. Модератор видит полное описание,
участки поставки с объёмами и гарантию.
Страница MarketplaceOfferDetailPage читает meta.readonly: в режиме модератора
прячет кнопку «Заказать» и диалог заказа, «назад» ведёт в «Модерацию».
Карточка модерации показывает категорию + ФИО поставщика (как в каталоге);
избыточный inline-блок деталей убран.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
quantity_available уже исключает заблокированное количество (инвариант
counters: available + blocked + consumed = опубликовано; при заказе
available уменьшается сразу). Повторное вычитание quantity_blocked на
карточке давало двойное списание: опубликовал 100, заказали 1 → показывало
98 вместо 99.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Убрано объяснение про партию/группировку/порог. Осталось: «Отметьте
кооперативные участки, на которые готовы обеспечить доставку, и укажите объём
поставки на каждое.» Подзаголовок шага — «Участки и объём поставки».
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GeocodeStatus/KuDetailsStatus импортировались как `import type`, но используются
в рантайме (GeocodeStatus.OK и т.п.) — тип стирается при компиляции, отсюда
ReferenceError в hasCoords и краш рендера всей страницы после сохранения ПВЗ
(лоадер «крутился вечно», UI замерзал). Импорт переведён в value.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hide-bottom-space на time-input'ах убирает зарезервированную пустую полосу под
сообщения — видимое поле центрируется по строке дня (align-items:center), а не
висит в верхней части высокой ячейки.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Поля времени рендерятся всегда; при выключенном дне они disabled (серые), а не
скрыты по v-if. Высота строки одинакова для включённого и выключенного дня —
переключение больше не «скачет» по высоте.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Заголовок диалога показывает человекочитаемое имя участка (branchName), а не
служебный braname. Режим работы: каждый день — одна строка (переключатель слева,
поля «Открытие/Закрытие» справа на той же линии); включение дня больше не
добавляет вторую строку и список не удлиняется.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Шаги 3–4 (участки, ПВЗ) больше не вынесены во вторую карточку «Дальнейшая
настройка» — они идут тем же сквозным списком после шагов совета (extraSteps
в CouncilOnboardingCard), нумерация продолжается. Доп.шаги видны всегда, в том
числе после утверждения документов.
Лоадер вынесен из оверлея на всё окно (WindowLoader, .absolute-full) внутрь
карточки (q-inner-loading с резервом высоты) — больше не «болтается» поверх уже
отрисованной страницы.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Карточка каталога несёт только категорию, поставщика (ФИО) и краткое
описание (≤200 символов); полное описание, участки поставки с минимальным
объёмом, гарантия и галерея раскрываются на отдельной странице предложения
по клику. Быстрый заказ остаётся на кнопке «Заказать».
- backend: query marketplaceGetOffer(id) (enrichment через field-резолверы)
- sdk: Queries.Marketplace.GetOffer + регенерация zeus-клиента
- desktop: новая страница MarketplaceOfferDetail + скрытый маршрут
marketplace-offer-detail (offer/:offerId); CatalogOfferCard — категория +
поставщик + отсечка описания; каталог открывает деталь по клику
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
«от 1» без единицы и сырой braname/аккаунт в карточках были непонятны.
- CreateMarketplaceOffer: поле «Мин. объём» получило суффикс выбранной единицы;
превью участков поставки — «<КУ> (от N <ед.>)»; исправлен leftover-баг после
SSOT — превью брало несуществующее `.label` вместо `.name` КУ.
- ChairmanModeration: «Участки поставки» теперь «<наименование КУ> (от N <ед.>)»
вместо braname без единицы; «Поставщик» — ФИО (supplier_name) вместо аккаунта.
Имена/единицы берутся из backend-полей оферты (supplier_name, delivery_points.name)
и общего хелпера marketplaceUnitShort — без сторонних дозапросов.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
После утверждения Советом двух документов председателю было неочевидно, что
дальше нужно добавить кооперативные участки и отметить их как ПВЗ. Добавил на
страницу онбординга блок «Дальнейшая настройка» с двумя шагами-подсказками:
- шаг 3 → ссылка на «Кооперативные участки» (route `branches`);
- шаг 4 → ссылка на «Пункты выдачи» (route `marketplace-issuance-points`).
Без дублирования функционала — только текст + кнопки-переходы на профильные
столы. Видны сразу, чтобы был понятен полный путь подключения.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Строжайшее правило: статусы (status/geocodeStatus) — всегда enum, никогда
строковый литерал; объявляются на backend, прокидываются в Zeus и используются
на фронте. Раньше KU/geocode/offer статусы были `@Field(() => String)` +
сравнения по строкам — нет единого источника значений, легко опечататься/
забыть набор.
Backend:
- KuDetailsStatuses / GeocodeStatuses / MarketplaceOfferStatuses → registerEnumType
(GraphQL enum MarketplaceKUStatus / MarketplaceGeocodeStatus / MarketplaceOfferStatus);
`@Field(() => Enum)` вместо String.
- Все строковые литералы статусов в сервисах/адаптерах/мапперах KU/offer/order
заменены на enum-члены (ku-details, moderation, order-create, order-cancel,
catalog-resolver, offer/order repo-адаптеры, order-delta mapper).
- SetKUStatusInput → @Field(() => MarketplaceKUStatus) + @IsEnum.
- Geocoder-port union (OK/FAILED) и TypeORM @Column enum-DDL оставлены — это
отдельный port-контракт и DDL-объявление, не surfaced-статусы.
Frontend:
- entities/MarketplaceKUDetails: KuDetailsStatus/GeocodeStatus = Zeus.MarketplaceKUStatus
/ Zeus.MarketplaceGeocodeStatus (единые значения с backend).
- AdminIssuancePoints / CreateMarketplaceOffer: сравнения geocode/статуса — на
enum-члены (script + pug-шаблон).
Codegen: schema + zeus + sdk build. Unit КУ 12/12, tsc controller clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Корень рассинхрона: адрес/контакты участка хранились копией в ku_details и
правились отдельно на столе ПВЗ, расходясь с правкой председателя в
«Кооперативные участки». Теперь детализация ПВЗ их не хранит — наименование,
адрес и контакты резолвятся живьём из организации участка (единый источник
правды) через field-резолвер MarketplaceKUDetails (как supplier_name/точки
поставки у оферты).
Backend:
- ku_details: drop contact_phone/contact_email; address_full → geocoded_address
(nullable) как внутренний кэш-ключ геокода (bootstrap-v8, synchronize:true).
- MarketplaceDetailKUInput больше не принимает адрес/контакты — только режим
работы/описание; KuDetailsService берёт адрес из организации, ленивый
reconcile геокода на чтении списка при дрейфе адреса.
- MarketplaceOrderDisplayService: контакты/адрес веток из организации,
resolveBranchContacts; enrich заказов больше не читает локальный адрес КУ.
- Фикс DI: MarketplaceOrderDisplayService добавлен plain-class провайдером —
field-резолверы оферты/КУ инжектят его по классу (латентный баг Chunk 1).
Frontend:
- Диалог ПВЗ: адрес/контакты read-only (правятся на «Кооперативные участки»),
редактируются только режим работы/описание.
- CreateMarketplaceOffer: список КУ берёт name/address прямо из KU-details,
без отдельного join branches.
- KU-details типы: name + nullable реквизиты; AdminIssuancePoints fallback.
Codegen: schema (83 резолвера) + zeus client + sdk build. Unit КУ 12/12.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Оферта теперь отдаёт отображаемые реквизиты живьём с бэкенда, чтобы фронт
не дозапрашивал их сторонними запросами:
- supplier_name — ФИО физлица/ИП или наименование организации поставщика;
- по каждому delivery_point: name (наименование участка) + address — живьём
из организации кооперативного участка (единый источник правды, правится
председателем в «Кооперативные участки»), lat/lng — из геокода КУ.
Реализовано field-резолверами на типах MarketplaceOffer и
MarketplaceOfferDeliveryPoint (как ленивый images-резолвер) — применяется
ко всем запросам, где возвращается оферта (каталог, мои предложения,
модерация), без дублирования. Резолв реквизитов участка переиспользует
MarketplaceOrderDisplayService (live-резолв, дедуп, best-effort); промис
кешируется на инстансе точки, чтобы 4 поля не дёргали БД повторно.
Codegen: schema + Zeus-клиент + sdk build; selector оферты расширен.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
В списке кооперативных участков при создании оферты вместо служебного кода
участка (braname, напр. iondlofolmqy) показываем человеческое наименование
филиала (short_name/full_name) и адрес. Рядом — маленькая кнопка карты
(icon-only), открывающая всплывашку MapView с точкой по координатам
геокодера — тот же паттерн, что в админке ПВЗ (AdminIssuancePoints).
Наименование берём из branchStore.loadBranches (join по braname), адрес и
координаты — из kuStore.details (геокодер). Кнопка карты только при
geocodeStatus OK и наличии lat/lng.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Страница модерации дёргала client.Query(ListCategories) прямо из .vue.
Перевёл loadCategories на типизированную обёртку fetchCategories из
MarketplaceCatalog/api (reuse, без дубля). Импорты client/Queries из vue убраны.
Канон: никаких client.Query/Mutation в .vue — только функции api/ со строгой
типизацией из @coopenomics/sdk IInput/IOutput.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Страница создания предложения поставщиком грузила список КУ запросом
marketplaceListKUDetails с пустым data: {}, из-за чего GraphQL отклонял
запрос «Field "coopname" of required type "String!" was not provided» и
кооперативные участки не загружались.
Переведён на entity-store MarketplaceKUDetails (как AdminIssuancePoints):
coopname берётся из systemStore.info, onlyActive: true. Инлайн client.Query
с пустым data убран (DRY: используем готовую обёртку entity).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
schema.gql — генерируемый артефакт (NestJS autoSchemaFile + generate-schema),
числился в .gitignore (строка 26), но был случайно затрекан → постоянный шум
в diff при регенерации. SDK build (typecheck+unbuild) его НЕ читает; читает только
скрипт generate-index-comments (docs/update:comments), которому нужна свежая
локальная генерация, а не закоммиченная копия.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OffererIncomingOrders переписан на партии вместо плоского списка заявок:
- ACTIVE-заказы группируются по «оферта × КУ» в партию-накопитель с прогресс-баром
к min_supply_volume (цель сбора, не порог), краткий состав партии вместо россыпи карточек
- «Принять партию (N)»/«Отклонить» массивом, доступны всегда (принять можно < min)
- min ≤ 1 → поштучный режим без прогресс-бара
- принятые (cycle_id) → сформированная партия (метрики + readonly-карточки)
- фикс: вкладка «Ждут акцепта» → фильтр ['ACTIVE'] (раньше leftover ACCEPTED_PENDING_SUPPLIER*)
- min-карта из marketplaceListMyOffers.delivery_points (хелпер fetchSupplierMinVolumeMap)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Форма оффера: вместо селектора типа поставки + target_volume — список КУ
кооператива (BaseCheckbox + min-объём на каждый, через marketplaceListKUDetails);
убраны cycleTypeOptions/isCollective/onSelectCycle/триггер-кнопка.
- Стол поставщика: accept/decline через единый batch-API (acceptOrdersBatch/
declineOrdersBatch) по order_id[]; убрана cycle_id-маршрутизация.
- Сводный заказ заказчика, модерация: убраны cycle-лейблы → сводка по КУ.
- Удалены cycle-type-view алиасы, marketplaceCycleLabel; ESLint зелёный.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Упразднён тип поставки (cycle_type/target_volume) как сущность. Управление
поставкой сведено к delivery_points оффера: список КУ + min_supply_volume на
каждом (мягкий ориентир группировки, не порог). individual/collective больше
нет нигде.
- Контракт: убран cycle_type из createorder (param + поле orders + namespace
CycleType + валидация). Требует ребут dev-чейна + regen ABI.
- cooptypes: ICreateOrder/IOrder без cycle_type.
- Controller: offer.delivery_points (jsonb) вместо cycle_type/target_volume;
order без cycle_type-снимка; единый batch-accept/decline по order_id[]
(свёрнуты acceptIndividual/acceptConsolidated/declineConsolidated/
triggerCollectiveSupply); удалены cycle-aggregator + consolidated-accept-decline
сервисы; consolidated_request без cycle_type (создаётся в момент batch-accept).
- SDK: AcceptOrdersBatch/DeclineOrdersBatch мутации; delivery_points в
offerSelector; schema.gql + zeus regen.
- Тесты контроллера обновлены, tsc --noEmit зелёный.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Способы поставки сведены к двум: INDIVIDUAL (заказ обслуживается отдельно)
и COLLECTIVE (коллективная закупка — копится партия, старт по объёму или
ручному запуску). Убраны timebased/volumebased/opensubscr. Дефолт order.
cycle_type = INDIVIDUAL, валидация createorder сужена до двух значений.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Меню-вкладки (каталог, мои заказы, входящие заказы) — первый блок
страницы; верхний padding q-page давал лишний зазор над саб-навигацией.
Гасим padding-top: меню прижимается к топбару, контент ниже разводит
flex-gap. PageHint при dismiss не рендерится — пустого слота нет.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
При выборе КУ адрес шёл отдельной строкой под BaseSelect, а поле уже
резервирует место под hint (reserve-hint-space) → двойной зазор и
рассинхрон с центрированными иконкой/бейджем. Адрес кладём в :hint поля,
бейдж прижимаем margin-left:auto в обоих режимах.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ttn_number в shipment/apl_reception: 32→64. Номер ТТН
COOPNAME-TTN-<cycle8>-<ku6>-<suffix8> ≈ 36–42 символов переполнял
varchar(32) → «value too long» при формировании партии экспедитора.
Требует рестарта контроллера (synchronize применит ALTER, увеличение
длины безопасно).
- CreateShipmentDialog: maximized для удобной работы с dual-list.
- OperatorBranchBar: при выборе из нескольких участков убран дублирующий
крупный заголовок (название КУ повторялось в селекте «Участок-Участок»);
селектор стал основным идентификатором, адрес — под ним. Один участок —
по-прежнему текстом.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Стол подготовки отгрузки переведён на модель брифа E14:
- Глобальная кнопка «Сформировать партию» в шапке (Teleport #header-actions-host)
вместо per-cycle кнопок. Лист сформированных партий — основной вид стола.
- Новый диалог формирования: способ доставки (самовывоз / экспедитор по ТТН) →
один кооперативный участок → dual-list заказов («Переместить всё» + откат
отдельных строк назад). Грузим всё, что справа; невыбранное остаётся ACCEPTED.
Гранулярность целая — количество в заказе не дробим.
- Submit группирует выбранные заказы по заявке (cycle_id) и шлёт createShipment
по каждой заявке с order_ids-подмножеством (backend S1). Для экспедитора —
поля ТТН (на каждую партию печатается QR приёмки, S2).
- groupAcceptedByKu(): группировка акцептованных заказов по КУ через все заявки.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Экспедитор не пайщик (нет аккаунта/кабинета) — приёмку открываем строго по
партии из накладной, а не по account-коду поставщика.
- HandoffTokenKind.Shipment + формат blago:shipment:<coopname>:<shipment_id>
(shipment-bound, не account-bound). decode/encode поддерживают оба вида.
- TTNPrintPreview: QR-код приёмки партии на печатном листе ТТН (PNG data-URL,
встраивается в печать и в скачанный самодостаточный HTML).
- buildTtnData: состав ТТН по order.shipment_id (fallback на (cycle,КУ) для
старых партий) + qrValue = shipment-bound токен.
- OperatorReception: ветка скана QR ТТН → openPickupForShipment(id) грузит
СТРОГО состав одной партии, без добора по акцепту («ничего больше»).
Приёмка/план/маппинг переведены с группировки по cycle_id на shipment_id
(при нескольких частичных партиях на одном КУ cycle_id брал не ту партию).
- MarketplaceOrder GraphQL DTO + orderSelector: поле shipment_id (нужно фронту
для фильтрации состава партии). Схема + zeus перегенерированы.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Эпик E14: «отделить акцептованное от реально погруженного». Поставщик
формирует партию из ПОДМНОЖЕСТВА акцептованных заказов КУ, остальное
остаётся ACCEPTED для следующей партии.
Модель: партия = (КУ, вариант доставки, подмножество заказов); на одном
(cycle, КУ) допустимо несколько частичных партий (self+expeditor split,
догрузка остатка). Невключённые заказы остаются ACCEPTED.
Изменения (аддитивно — старый фронт совместим):
- order.shipment_id: связь заказ→партия. Приёмка резолвит состав партии
по shipment_id (findByShipmentId), а не инференцией по (cycle, КУ) —
обязательно при нескольких партиях на одном КУ. Fallback на (cycle,КУ)
для партий, созданных до появления связи.
- MarketplaceShipmentGroupInput.order_ids?: подмножество заказов (пусто →
все ACCEPTED заказы КУ, как раньше).
- createShipment: снят 1:1-инвариант покрытия всех КУ и cycle-level
idempotency (ConflictException). Повторное формирование разрешено;
guard `shipment_id IS NULL` в assignToShipment не даёт включить заказ
в две партии. total_amount по подмножеству.
- assignToShipment(): bulk ACCEPTED→SUPPLY_PREPARED + привязка к партии.
- createExpress: привязка через assignToShipment (консистентно).
- schema.gql + zeus-клиент перегенерированы.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Действие ship в OrderCard (offerer/paid) не обрабатывалось ни одним
родителем — кнопка ничего не делала. Отгрузка идёт через формирование
партии на странице «Подготовка отгрузки», не с карточки заказа.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Предыдущий фикс снимал лоадер, но 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>
Предыдущий фикс снимал лоадер, но 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>
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>
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>
Тот же фикс, что для «Ожидаемых поставок»: слот #no-data у q-table
выравнивает контент влево, а .empty — блок по ширине контента.
Оборачиваю EmptyState в .<block>__nodata (full-width flex center) на
страницах Приёмка / Выдача / Склад участка / Выплаты совета / Готово
к получению.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Слот #no-data q-table выравнивает контент влево, из-за чего EmptyState
(«Ожидаемых поставок нет») прижимался к левому краю. Обёрнут в full-width
flex-контейнер с justify-content:center.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Виджет OfferDetailsDialog больше нигде не используется (модерация перешла на
inline-карточку) — удалён.
- CatalogOfferCard: prop clickable (default true) гейтит mp-card--interactive
(cursor-pointer + hover-zoom) и emit click. Каталог и «Мои предложения» клик
используют — у них без изменений; модерация передаёт clickable=false, чтобы
карточка не выглядела кликабельной (по ней ничего не открывается).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Страница модерации открывала по клику OfferDetailsDialog с полным описанием и
кнопками одобрить/отклонить — лишний клик. Вдобавок диалог показывал
плейсхолдер вместо картинок: selectedDetail не маппил preview/images, а диалог
рендерит offer.preview (карточка же тянет carousel через marketplaceOfferImageUrls).
Теперь решение принимается прямо в карточке:
- CatalogOfferCard получил слот #details (после описания) — родитель кладёт туда
доп. данные, не открывая отдельный диалог.
- Модерация заполняет #details мета-строками (категория, тип отсечки, гарантия,
поставщик) и выносит обе кнопки (Отклонить + Одобрить) в #actions карточки.
- Клик-открытие диалога, selectedDetail, openDetails и сам OfferDetailsDialog с
страницы убраны; подсказка «нажмите на карточку» тоже.
- Подписи cycle_type вынесены в общий marketplaceCycleLabel (shared/lib/consts) —
карта дублировалась в OfferDetailsDialog/CreateMarketplaceOffer/OrdererConsolidated.
Виджет OfferDetailsDialog больше нигде не используется (осиротел) — оставлен на
случай detail-view в каталоге; удалять отдельным решением.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
На столе заказчика после подписи оферты ЦПП requires_gate становился false →
gate скрывался → на время loadDesktop + переустановки маршрутов + router.push
мелькал баннер «Вы уже подключены к Столу заказов». Пользователь не хочет
промежуточный экран — подписал, сразу на стол.
Добавлен флаг redirecting: выставляется в начале onAccept, прячет баннер
alreadyDone и держит q-inner-loading спиннер до самой навигации. Убран
SuccessAlert-поздравление — редирект на стол и есть подтверждение. Баннер
«уже подключены» остаётся для легитимного захода на онбординг-URL уже
подключённым пайщиком (redirecting=false).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
После утверждения Советом обоих документов ЦПП «Стол заказов» расширение
подключается и grants в getDesktop меняются, но фронт перечитывал desktop
workspace только на init и при install/enable расширения — для онбординга
триггера не было. Итог: рабочие столы и страницы Стола заказов появлялись лишь
после ручной перезагрузки страницы.
Добавлен watcher на isCompleted в useMarketplaceOnboarding: при переходе
false→true (оба шага completed) вызывается desktopStore.loadDesktop(), который
перечитывает getDesktop с новыми grants. Меню/столы появляются сразу.
loadDesktop идемпотентен (мерджит маршруты); watch не срабатывает на initial
mount, только на реальном завершении онбординга в течение сессии.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Страница «Подключение ЦПП Стол заказов»:
- Лоадер на «Объявить»: карточка использовала локальный submitting и
закрывала диалог сразу после синхронного emit('step-submit'), пока
реальная async-отправка проекта решения шла в composable (handleStepSubmit)
с отдельным submitting, не пробрасываемым в карточку. Итог — диалог
схлопывался мгновенно, лоадера не было нигде. Теперь submitting проброшен
из composable в CouncilOnboardingCard: кнопка «Объявить» крутит лоадер,
диалог держится открытым до завершения транзакции и закрывается по watcher'у
(submitting true→false).
- Ширина: убран max-width 860px у карточки онбординга — растягивается на всю
ширину страницы.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Глобальный ::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>
Глобальный ::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>
operations.ts — рукопашное зеркало contracts/.../operations.hpp OPERATION_REGISTRY
(генератора нет: gen:from-cpp покрывает только wallets.generated.ts). Зеркало
устарело — держало legacy клиринговые o.mkt.supply/o.mkt.recv (p.mkt.reqst),
которых нет в контракте. Из-за этого MARKETPLACE_OP_CODES (фильтр по
contract==='marketplace') не знал o.mkt.purch/return/... → инвариант I3
(счёт 10 Материалы) считал их немаркетплейсными и падал.
- operations.ts: заменены 2 legacy-записи на 9 canonical marketplace-операций
(lock/conv/lockm/unlock/purch/payout/consum/return/wroff), включая новые из
R#1 PR #50: o.mkt.conv (паевой→членский «Стола заказов») и o.mkt.lockm
(добор резерва с членского). unlock/return → w.mkt.member (членский программы),
синхронно с operations.hpp.
- process-hash-locator.ts: удалён мёртвый locator p.mkt.reqst (больше не
референсится из реестра).
- marketplace-process-trace-coverage.spec.ts: EXPECTED-списки приведены к
9 операциям; walletTo unlock/return w.wal.member→w.mkt.member.
Тесты: marketplace-ledger2-invariants.spec.ts + marketplace-process-trace-coverage.spec.ts
= 73 passed (ранее 2 I3-теста были красные на baseline). Сигнатуры actions/ABI
не менялись (операции ledger2 внутренние) — regen schema/sdk не требуется.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
После переориентации денежных потоков на w.mkt.member (ревью PR #50#1):
- I5 reserve-consistency: добавлен o.mkt.lockm (w.mkt.member→w.mkt.order) как
приход резерва; o.mkt.unlock теперь сверяется по walletTo=w.mkt.member.
- spec-фикстуры unlock/return переведены на w.mkt.member.
27 тестов проходят (как на baseline). 2 теста I3 (счёт 10) остаются красными —
предсуществующий разрыв: cooptypes/ledger2/operations.ts не содержит канонических
marketplace-операций (только legacy o.mkt.supply/recv), из-за чего MARKETPLACE_OP_CODES
их не распознаёт. Полный синк operations.ts с operations.hpp (+ o.mkt.conv/lockm) —
отдельный фоллоуап (затрагивает process-hash-locator/ledger2.service).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Страницы 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>
Денежные потоки заказа деноминируются на членском «Стола заказов»
(w.mkt.member), а не на универсальном членском, и доплата по факту НЕ
списывается с паевого напрямую:
- ledger2: новый кошелёк w.mkt.member (USER_SHARED, program 2) + операции
o.mkt.conv (паевой→членский «Стола заказов», Дт 80/Кт 86) и o.mkt.lockm
(членский→резерв заказа).
- signiss2 при факт>план: o.mkt.conv(diff)+o.mkt.lockm(diff) вместо прямого
списания с паевого; при факт<план остаток → w.mkt.member.
- o.mkt.unlock (отмена/decline/expire/недовыдача) и o.mkt.return (гарантийный
возврат) переориентированы с w.wal.member на w.mkt.member.
- стандарты p.mkt.supply/p.mkt.return: кошельки, операции, проводки и проза.
- cooptypes wallets.generated.ts регенерирован (17 кошельков, 8 mapping).
Контракт собирается (constexpr-валидаторы реестра ledger2 зелёные); деплой
wasm/abi — отдельным шагом пайплайна. Подпись signiss2 не менялась — conv/lockm
суть ledger2-операции, не контракт-экшены: cooptypes-actions/codegen не затронуты.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- desktop API (OperatorReception/OperatorIssuance): все обёртки client.Query/
Mutation принимают `data: IInput['data']` из SDK и передают `{ data }` целиком,
без позиционных аргументов и разворачивания полей (канон desktop). Call-sites
обновлены (приёмка/выдача/подпись/председатель КУ).
- handoff-token: префикс кода передачи `mp1` → `blago`.
- удалён глобальный src/shims-marketplace-qr.d.ts: типы `qrcode` через @types/qrcode
(dev-dep), `BarcodeDetector` — узким локальным типом в QrScanner (нет в lib.dom).
Фронт-only, codegen не нужен.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Карточка реестра вела на страницу документа, передавая 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>
Кнопка «Создать акт приёмки» на ожидающей партии звала createAplReception
напрямую, без формы сверки — оператор принимал по заказанным кол-вам и
исходным ценам (риск «приняли как 100, хотя привезли 80»). Перенаправлена в
openPickupForSupplier («Принять партию») — тот же диалог коррекции кол-ва/цены,
что и QR-скан и «самовывоз по факту». Коррекция доступна на ВСЕХ путях приёмки
без исключений. createReceptionForShipment удалена. Фронт-only, codegen не нужен.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Реестр документов переведён с таблицы на канон плоские строки (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>
B3 — backend-обогащение display-сервиса именами участников (ФИО физлица/ИП
или short_name организации) через UserCertificateInteractor. Резолв только в
уже авторизованных резолверах приёмки/выдачи (оператор/председатель КУ или сам
поставщик), приватные имена не утекают в общие ленты заказов.
- MarketplaceOrder: orderer_name + supplier_name (заполняются в лентах выдачи).
- MarketplaceAplReception: offerer_name; позиции fact_quantity_per_order несут
product_name + unit_of_measure (для таблицы сверки).
- order-repo: findByIds (батч для обогащения позиций приёмки).
- desktop: выдача — колонка «Заказчик» = ФИО; приёмка — заголовок = наименование
поставщика.
B4 — общий read-only виджет ReceptionLinesTable (позиции/кол-во/цена/сумма) +
кнопка «Показать акт» (рендер html подписываемого документа) в диалогах подписи
поставщика и председателя. У председателя сверка строго на чтение.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
activeWorkspaceName инициализируется асинхронно — при холодном переходе по
прямой ссылке на документ совета скоуп мог упасть в скоуп пайщика и документ
«не находился». Имя роута (document-details / user-document-details) доступно
сразу и детерминированно.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Прошлый подход (фильтр реестра по ?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>
Коррекция фактического количества и цены теперь доступна оператору на ВСЕХ
путях приёмки без исключений, включая express-самовывоз (добор по акцепту и
"самовывоз по факту" без партии). Раньше express принимался по цене и
количеству заказа без возможности правки.
- createExpressReception проводит fact_quantity_per_order сквозь create →
signsupp/signchair (контракт не меняется — переиспользуется B2).
- desktop: addon-юниты в диалоге приёмки редактируемы (кол-во+цена); кнопка
"Принять самовывоз" открывает ту же форму коррекции, что и QR-скан.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Оператор правит при открытии не только количество, но и цену за единицу
(привезли хуже/замена → принимаю со скидкой; цену меняю на месте).
Канон-имя параметра — actual_unit_price (asset), достраивает пару к
actual_quantity.
Контракт:
- signiss2: +actual_unit_price; fact_cost = actual_quantity × actual_unit_price
(ветки возврата/доплаты по стоимости — без изменений).
- signchair: +actual_quantity +actual_unit_price; кооператив книжит
поставщику o.mkt.purch на fact_cost (итоговая стоимость к получению),
вместо o.total_cost. ABI собран и проверен.
cooptypes: ISignIss2 +actual_unit_price; ISignChair +actual_quantity
+actual_unit_price.
controller:
- issuance: open захватывает actual_unit_price → issuance_fact.fact_unit_price;
finalize шлёт его в signiss2 (заказчик факт не редактирует).
- reception: fact-entry несёт fact_unit_price; signchair получает кол-во+цену
из снапшота; total_amount/проводка от факт-цены.
- diff_state выдачи теперь по СТОИМОСТИ (цена могла измениться, не только кол-во).
- DTO + GraphQL ObjectType + selectors + полный codegen (schema/client/sdk).
desktop:
- CorrectionTable: редактируемая колонка цены (предзаполнена), эмит factPrice.
- IssueActOpenDialog: правка цены при открытии выдачи.
- OperatorReceptionPage: поле цены per-позиция при открытии приёмки.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Клик по результату поиска вёл в никуда (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>
В экране ожидания регистрации (WaitingRegistration) «до 24 часов» — неактуально:
реальный регламентный срок рассмотрения советом — до 30 дней (обычно решение
принимается за день-два). Поправили текст под фактический срок.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Кардинальный дефект: финальную подпись заказчика держал стол оператора
(2-й скан штрих-кода + ввод приватного ключа ЗАКАЗЧИКА на устройстве
оператора). Перенесено корректно, без изменения контракта:
- signiss1 не берёт actual_quantity (только акт), signiss2 берёт →
relocation чисто backend+frontend.
- openIssuance принимает actual_quantity (правка оператора): зашивается в
акт председателя и сохраняется снапшотом issuance_fact на открытии.
- finalizeIssuance больше не принимает actual_quantity/delivery_signer —
backend берёт их из заказа (issuance_fact, chairman_account); заказчик
факт не редактирует.
- Полный codegen: schema.gql + zeus (controller+sdk).
- Frontend оператора: IssueActOpenDialog получил таблицу сверки кол-ва +
«Показать акт»; IssueActFinalizeDialog удалён, «Завершить выдачу»/2-й
скан убраны, READY_TO_RECEIVE на столе оператора = «Ждём подпись
заказчика».
- Frontend заказчика: новый OrdererFinalizeIssuanceDialog (read-only
сверка + «Показать акт» + «Подписать и получить» своим ключом) на столе
«Готово к получению».
Правка ЦЕНЫ (не только кол-ва) — отдельным шагом B2 (затрагивает контракт).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Косметика/канон/тексты без backend (дефекты предыдущих итераций):
- A1: суммы через formatAsset2Digits (срез нулей + ru-группировка) вместо
сырого "750.0000 ₽" — оба диалога подписи приёмки, диалог открытия
выдачи, столы выдачи оператора и заказчика, форма приёмки.
- A2: убран технический жаргон из инфо-виджетов (registry_id, signiss,
o.mkt.*, "ключом текущей сессии", "backend отправит на цепь") —
заменён одной бизнес-строкой.
- A3: SignAplReceptionChairmanDialog переведён с сырого q-dialog/q-btn на
BaseDialog/BaseButton (Правило №1); формы приёмки/подписи развёрнуты
на весь экран (maximized).
Класс B (выдача-кошмар: финальная подпись на стороне заказчика, цена,
ФИО вместо username, сверка-таблицы) — следующими шагами, см. epics.md
блок "КОРРЕКЦИИ ПОСЛЕ ОЧНОГО ПРОГОНА".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Уточнённая модель приёмки на ПВЗ (Story 14.3): «акцепт ≠ привезли»,
потолок приёмки = акцепт, партия/ТТН — разметка, не лимит.
R4: единый базис. Новый read-query marketplaceListSupplierPickupOrders
(braname, offerer_account) отдаёт Order'ы поставщика на КУ в статусах
SUPPLY_PREPARED (по партии/ТТН) и ACCEPTED (добор по акцепту) через
MarketplaceOrderDisplayService. Суммирование лент партии+express убрано —
лента одна, партия — разметка через статус.
R7a: диалог приёмки = плоский список единиц имущества с разделителем
«задекларировано в партии (ТТН) / добор по акцепту», без партийной группировки.
R7: per-unit выбор «принять»; партия без выбранных единиц не создаётся и
ждёт (кейс экспедитора). Добор — отдельный тумблер.
R5: правка факта per-Order на приёмке (поле «факт», потолок = заказано).
Инвариант-guard в buildFactQuantity: fact > order.quantity → отказ
(«сверх акцепта не принимаем»).
Остаётся: per-unit факт/выбор для добора (express), отмена акцепта
поставщиком R6 (контракт).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Большая пустая полоса между строкой поиска и первым результатом мешала —
снят верхний padding секции результатов (q-pt-none), список начинается ближе.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Поиск показывал 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>
Документы хранят дату в локализованном формате `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>
Обратной совместимости в разработке нет — мусор не оставляем. Account-bound код
становится единственным путём передачи на ПВЗ.
- Оператор приёмки/выдачи: убран fallback-декод по id; нераспознанный скан
отвергается с понятной ошибкой. Только account-bound токен.
- Поставщик: убран per-партия QR (кнопка/диалог/колонка «Передача» в таблице
сформированных партий) — остаётся единый «Мой код для ПВЗ».
- Заказчик: убран per-заказ QR (кнопка/диалог/колонка «Получение») — остаётся
единый «Мой код получения».
- handoff-token: doc-комментарий decode без упоминания legacy-пути.
Десктоп-путь без камеры сохранён: выбор партии «Создать акт приёмки» из списка
ожидающих и кнопки выдачи в таблице (это канон, не QR-fallback).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Код привязан к аккаунту человека, а не к партии/заказу («как в Ozon»):
один показ = оператор резолвит аккаунт против ленты своего КУ и принимает/
выдаёт разом все ожидаемые единицы этого человека.
- shared/lib/marketplace/handoff-token: encode/decode токена
`mp1:<kind>:<coopname>:<account>` (pickup|receive). Детерминирован от
личности+намерения → генерируется заранее/оффлайн, backend не минтит.
Привязка к КУ — на стороне оператора (резолв против своей ленты); coopname
отсекает чужой кооператив.
- Поставщик: «Мой код для ПВЗ» (pickup-токен) на «Подготовке отгрузки».
- Заказчик: «Мой код получения» (receive-токен) на «Готово к получению».
- Оператор приёмки: скан pickup-кода → диалог «Принять всё» (сформированные
партии SUPPLY_PREPARED + самовывоз по факту разом).
- Оператор выдачи: скан receive-кода → панель всех заказов заказчика на выдачу;
пошаговое открытие/завершение (каждый шаг — своя двойная подпись).
- Legacy-QR (сам shipment_id/order_id) продолжает работать — decode вернёт null
и код падает на прежний путь по id (обратная совместимость).
Подписанный/одноразовый токен-сервис с TTL остаётся доработкой (mp2): аккаунт-
биндинг уже даёт Ozon-UX, криптоподпись пользователь явно отложил.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Оператор принимает самовывоз поставщика, не сформировавшего партию заранее.
Backend: marketplaceCreateExpressReception синтезирует SELF-партию из ACCEPTED-
заказов поставщика на этом КУ (per-cycle) и открывает приёмку через
существующий create(); marketplaceListExpressPickupsByBraname — лента
поставщиков, ожидающих самовывоза на КУ (агрегат). Добавлен фильтр
delivery_braname в order-repo. Жёсткий 1:1-акцепт всех КУ цикла здесь намеренно
не применяется — принимается только привезённое на этот КУ.
Desktop: на «Приёмке» раздел «Самовывоз по факту» со списком поставщиков и
кнопкой «Принять самовывоз». Двухподписный акт и переход в ACCEPTED_TO_COOP —
без изменений.
Codegen: schema.gql + zeus regenerated, SDK-обёртки CreateExpressReception /
ListExpressPickupsByBraname.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Виджет TTNPrintPreview подключён в стол поставщика: для партий Варианта Б
(экспедитор) в «Сформированных партиях» — кнопка «ТТН», открывающая накладную
с составом (позиции из заказов SUPPLY_PREPARED партии). Печать через изолированный
iframe (печатается только лист А5, без хрома приложения) + скачивание
самодостаточного HTML (в т.ч. «Сохранить как PDF» из браузера). Без новых
зависимостей. Виджет приведён к канону: BaseButton вместо q-btn, токены --p-*.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Убран ручной ввод shipment_id + кнопка «Создать акт» (ломались: оператор
копировал обрезанный для показа id → invalid input syntax for type uuid).
Вместо этого — список ожидающих приёмки партий КУ (статус SUPPLY_PREPARED из
listShipmentsByBraname) с кнопкой «Создать акт приёмки» на каждой; QR-сканер
остаётся для приёмки с телефона. Оба пути зовут createAplReception({shipment_id}).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Системно заменён прямой Notify.create на канонические алерты (правый нижний
угол, тёмный фон, единый визуал) во всех 10 страницах marketplace:
positive→SuccessAlert, negative→FailAlert(e) (сам извлекает GraphQL-ошибку),
info/warning→NotifyAlert. Notify убран из quasar-импортов.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Зеркальный сценарий выдачи на ПВЗ, переиспользует HandoffQr/QrScanner (DRY):
- заказчик (OrdererReadyToReceive): колонка «Получение» с кнопкой QR →
диалог с QR заказа (value = order id);
- оператор выдачи (OperatorIssuance): кнопка «Сканировать QR заказа» →
сканер → onQrScanned находит заказ в ленте КУ и запускает нужный шаг
(ACCEPTED_TO_COOP → открыть выдачу, READY_TO_RECEIVE → завершить);
заказ не на этом КУ / не в статусе выдачи → FailAlert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Основной сценарий ПВЗ: поставщик показывает QR сформированной партии,
оператор сканирует — идентификатор подставляется и акт приёмки открывается
без ручного ввода shipment_id.
Два переиспользуемых канон-виджета (DRY, под Story 14.4 заказчик→выдача):
- HandoffQr — генерация QR из идентификатора (qrcode, toDataURL) + копируемый
код как запасной путь;
- QrScanner — нативный BarcodeDetector (Chromium) + камера, с ручным вводом
кода если камера/API недоступны; зависимостей не добавляем.
Поставщик (OffererSupplyPreparation): колонка «Передача» с кнопкой QR на
партии в статусе SUPPLY_PREPARED/RECEPTION_IN_PROGRESS → диалог с QR.
Оператор (OperatorReception): кнопка «Сканировать QR» → диалог сканера →
onQrScanned подставляет код и создаёт акт приёмки.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GraphQL-enum MarketplaceShipmentDeliveryVariant принимает значения по ИМЕНИ
(SELF/EXPEDITOR), а не по внутреннему коду 'A'/'B' — самовывоз падал с
"Value 'A' does not exist in enum". Берём значения из Zeus-enum SDK.
Notify.create заменён на канонические SuccessAlert/FailAlert (тост справа внизу).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Страница «Подготовка отгрузки» получила раздел «К формированию»: принятые
(ACCEPTED) заказы группируются по заявке→КУ; по кнопке «Сформировать партию»
открывается диалог с выбором способа доставки ПО КАЖДОМУ КУ — самовывоз
(Вариант А) или экспедитор+ТТН (Вариант Б, форма из 7 полей) — и вызовом
marketplaceCreateShipment. Раздел «Сформированные партии» — прежняя таблица с
колонкой «Следующий шаг». Refresh унифицирован на общий RefreshButton.
Логика группировки и типы вынесены в lib/shipmentFormation.ts (переиспользуются
диалогом и страницей). Закрывает разрыв Story 5.1: единый явный путь отгрузки
для индивидуальных и пакетных заказов (вместе со Story 14.1 на бэкенде).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Раньше acceptIndividual авто-формировал партию Варианта А (самовывоз, без ТТН)
через synthesizeIndividualShipment — поставщик был лишён выбора доставки. Теперь
синтезируется только заявка (cycle) в статусе ACCEPTED, а партию поставщик
формирует явно (marketplaceCreateShipment) с выбором варианта — единый путь с
пакетными заказами. Order после accept остаётся ACCEPTED до явного формирования.
Убраны неиспользуемые зависимости shipmentCreate + импорт варианта; spec и
комментарий forward-guard приведены в соответствие. tsc зелёный.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Страница была read-only и выглядела «мёртвой»: ни кнопок, ни подсказки, что
делать. Добавлена колонка-подсказка «Следующий шаг» по статусу+варианту партии
(самовывоз → привезти на КУ; экспедитор → передать по ТТН; идёт приёмка;
принято кооперативом) и честный баннер. Активная подготовка (выбор варианта,
ТТН, QR-передача) — доработки Эпика 14, здесь пока не реализованы.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Бейдж заказа в SUPPLY_PREPARED показывал «Поставка готовится» (warn), хотя
доменно это завершённое состояние: партия уже сформирована (вариант доставки
выбран, для экспедитора выпущена ТТН) и готова к отгрузке/приёмке. Лейбл
расходился со страницей партий («Готова к отгрузке») и вкладкой «Поставка
готова» у поставщика. Приведено к «Поставка готова» (info). Вкладка заказчика,
объединяющая принят/готова/у кооператива, переименована «Готовятся» → «В работе»
во избежание противоречия с бейджем.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Каталог: чип-фильтр категорий + плавающая сортировка → канон PageTabs
(категории как вкладки со счётчиком) + BaseSelect в слоте #actions.
- «Мои заказы»: вкладки покрывают ВСЕ статусы заказа (был баг — заказ в
SUPPLY_PREPARED не попадал ни в одну вкладку, кроме «Все»). Добавлена
вкладка «Готовятся» (ACCEPTED/SUPPLY_PREPARED/ACCEPTED_TO_COOP), «Отменены»
расширена истёкшими циклами и возвратами.
- «Входящие заказы» поставщика: аналогичное исчерпывающее покрытие статусов.
- Кнопка «Обновить» вынесена в общий виджет widgets/Marketplace/RefreshButton
(дублировалась ~15× по столам, по-разному свёрстана) и поставлена в шапку:
на «Сводный заказ» и «Готово к получению» — через канон Teleport в
#header-actions-host, у поставщика — в слот вкладок PageTabs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OrderCard переверстан: заголовок и бейдж в одной строке, №·дата — отдельной
строкой на всю ширину (раньше «висла» вокруг бейджа); Сумма крупным числом-
героем + Кол-во; ПВЗ отдельным блоком (наименование КУ + адрес). Убрано
задвоение статуса — раньше показывались и грубый card-status («Размещён»), и
доменный («Ждёт поставщика»).
Единый orderStatusDisplay (подпись + вариант бейджа) в виджете OrderCard —
устранены три дубля STATUS_LABEL (MyOrders / OperatorIssuance /
OrdererConsolidated). Доменный статус и cancellable прокинуты в модель карточки;
действия заказчика — отмена по cancellable, без нерабочих «Открыть»; добавлен
readonly для обзорного сводного заказа.
Фильтры столов заказчика и поставщика переведены на канон PageTabs.
Фикс бага: вкладка «Ждут акцепта» (поставщик) и «Ждут поставщика» (заказчик)
теперь покрывают оба статуса ожидания акцепта — ACCEPTED_PENDING_SUPPLIER и
ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL. Раньше индивидуальный заказ с кнопкой
«Принять» в эти вкладки не попадал (отдельная вкладка «Индивидуальные» путала).
OrdererConsolidated переведён на общий toOrderCardModel — раньше показывал сырой
offer_id как заголовок и braname вместо наименования+адреса ПВЗ.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- OrderCard: ПВЗ двумя строками — наименование КУ (основная) + адрес
(приглушённая), запасной вариант braname если оба пусты.
- toOrderCardModel вынесен в виджет OrderCard (DRY): «Мои заказы» и
«Входящие заказы» больше не дублируют STATUS_TO_CARD и маппинг.
- Таблицы выдачи (оператор / готово к получению): колонка «Товар»,
единица измерения у количества, ПВЗ через наименование·адрес.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- delivery_point_name на заказе: live-резолв наименования кооперативного
участка (org.short_name) по braname через core ORGANIZATION_REPOSITORY,
без денормализации (копия отставала бы при переименовании КУ).
- buildDisplayFields вынесен из order-резолвера в общий
MarketplaceOrderDisplayService (DRY): теперь обогащает и оба стола, и
ленты выдачи (listIssuancesByBraname, listMyReadyToReceive) — товар,
единица, наименование и адрес ПВЗ. Best-effort, без N+1.
- schema/zeus/sdk-селектор регенерированы под новое поле.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OrderCard переведён на канон MONO Platform v2: BaseCard/BaseBadge/BaseButton,
токены --p-*, моноширинный номер, статус-пилюля с точкой; убраны --mp-*-токены,
прямой q-card, тени и кастомный .mp-status-chip.
Столы заказчика («Мои заказы») и поставщика («Входящие заказы») показывают
название товара (product_name) вместо UUID предложения, единицу измерения
через общий marketplaceUnitShort и адрес ПВЗ (delivery_point_address) вместо
служебного идентификатора — с запасным значением, если реквизит недоступен.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Заказ ссылается на предложение и ПВЗ по id и не нёс отображаемых реквизитов —
карточки столов показывали сырой offer_id и служебный braname. Резолверы
listMyOrders/listSupplierOrders/getOrder теперь обогащают DTO заказа:
product_name и unit_of_measure из предложения (батч offerRepo.findByIds без
N+1), delivery_point_address из детализации ПВЗ (kuRepo.findByCoopname).
Поля nullable, best-effort: нет предложения/ПВЗ — остаются пустыми, клиент
показывает запасной вид.
Схема и zeus-клиент перегенерированы, orderSelector дополнен; offer-репо
получил findByIds (+ моки в unit-тестах).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Переинсталл (--no-frozen-lockfile) после удаления vue3-openlayers из
desktop/package.json в прошлой сессии: lockfile синхронизирован, node_modules
восстановлен (ts-node/eslint), что разблокировало канон-цикл
generate-schema/generate-client.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Зависимость удалена — чистим устаревший пример тяжёлой библиотеки в
комментарии про отключение vue-tsc в dev. Только комментарий, рестарт
dev-сервера не требуется.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
На странице пунктов выдачи (стол администратора) диалог карты падал с
«Failed to resolve component: ol-map»: boot/map.ts с регистрацией
vue3-openlayers никогда не был подключён в quasar.config (массив boot),
поэтому ol-* не резолвились и карта не рисовалась.
OpenLayers в проекте больше нигде не используется — переводим shared/ui/Map
на Yandex Maps JS API (как рабочий виджет KUMapWithList) и сносим OpenLayers:
- shared/ui/Map/Map.vue: single-point карта на ymaps (метка — pin.svg),
тот же публичный API long/lat; деградированный баннер без ключа.
- shared/lib/yandexMaps: загрузчик SDK вынесен из widgets/KUMapWithList в
shared (DRY) — общий для виджета списка ПВЗ и single-point карты.
- удалён boot/map.ts (мёртвый, не в boot) и зависимость vue3-openlayers.
Lockfile подровняется при следующей установке (vue3-openlayers пока остаётся
orphan-записью).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Свежие desktop-фиксы из dev: форматирование сумм в тостах ошибок (formatAssetsInText),
SSR-фиксы Sentry/темы, гонка Teleport-host шапки + обновление грантов после регистрации,
плейсхолдеры форм ИП/организации, фикс выбора BaseRadioCard при наведении.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ошибки цепи возвращают суммы с сырой precision=4 ("100.0000 RUB"). Добавлен formatAssetsInText() рядом с formatAsset2Digits и применён в FailAlert — единая точка для всех тостов ошибок. Суммы вида <целое>.<>=3 знаков> <ТИКЕР> приводятся к 2 знакам с группировкой; уже отформатированные не трогаются (идемпотентно).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
Глобальный ::selection красит текст в var(--p-ink) (тёмный на свет-теме)
поверх полупрозрачного фона — на тёмной поверхности тоста выделенный текст
пропадал. Scoped-override для .q-notification/.toast: полупрозрачная светлая
подложка + светлый текст, читаемо на всех типах тостов.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Баг 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>
Только неочевидное поле — наименование банка получило подсказку ПАО "Сбербанк".
Остальные поля ИП самоописательны (формат даты, кол-во цифр, «как в паспорте»).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Поля краткого/полного наименования, должности представителя и наименования
банка получили placeholder-подсказки (ПК "Ромашка", Председатель совета,
ПАО "Сбербанк") — показываются при фокусе пустого поля.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Заголовок workspace-row получал outline только когда стол активный
(.is-active .row.is-selected). На неактивных столах при навигации курсор
«терялся»: только лёгкая смена фона на surface-2, без рамки — в отличие
от page'ей, которые подсвечиваются outline'ом всегда. Выровнял правило:
outline на любом .workspace-row.is-selected.
На верхней/нижней позиции стрелка дальше прыгала в противоположный
конец списка из-за (idx+delta+len)%len. Заменено на clamp в [0..len-1]:
курсор остаётся на месте, как в нативном listbox/меню. Пустое выделение
+ ↓ ставит на первый, ↑ — на последний.
Активный workspace был position:sticky top:0 — при навигации стрелкой
вверх курсор-выделение «исчезал» за sticky-overlay'ем, потому что
scrollIntoView({block:'nearest'}) считал элемент видимым в normal flow,
не учитывая sticky-перекрытие. Активный стол и так идёт первым по
сортировке + есть badge «Активный», sticky не нужен.
- 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().
- 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.
Фон и текст тоста (.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>
q-pa-lg на flat-карточке SignAgreementDialog — документ больше не прижат
к границе на странице кошелька пайщика.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Глобальный 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.
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+&.
Ошибки цепи возвращают суммы с сырой precision=4 ("100.0000 RUB"). Добавлен formatAssetsInText() рядом с formatAsset2Digits и применён в FailAlert — единая точка для всех тостов ошибок. Суммы вида <целое>.<>=3 знаков> <ТИКЕР> приводятся к 2 знакам с группировкой; уже отформатированные не трогаются (идемпотентно).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
8 страниц стола ПВЗ (market-pvz): внутристраничный .__head с .t-h2 (дублировал
заголовок навигации) заменён на закрывающуюся подсказку PageHint с прежним
пояснительным текстом. На «Маркировке» сохранён .no-print. На «Доверенных лицах»
постоянный инфо-баннер стал закрывающимся (условный state-плейсхолдер оставлен).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Вынесен domain-компонент shared/ui/domain/PageHint: инкапсулирует хук
useDismissibleBanner (скрытие через LocalStorage без мигания) и канон-разметку
.banner.banner--info + крестик. Текст подсказки — через слот, ключ хранения —
prop storage-key. Устраняет ~28-кратное повторение inline-баннера по столам (DRY).
Стол заказчика (5 страниц): убран внутристраничный дубль-заголовок «Каталог /
Каталог» (.__head с .t-h2 дублировал заголовок навигации) — вместо него
PageHint с прежним пояснительным текстом и крестиком.
Стол поставщика (5 Offerer* + AdminIssuancePoints): inline-баннер + локальный
хук/SCSS заменены на PageHint без изменения визуала.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Аудит кастов по Столу оператора (ПВЗ) и Столу администратора:
- template-кастов 0, `as any` 0 — опасных/рантайм-фатальных нет;
- два реальных хака в admin-страницах убраны без каста:
- BoardAgendaWriteoff: `(... ) as never` — локальный строковый WriteoffStatus
не стыковался с SDK-входом statuses (enum MarketplaceWriteoffProposalStatus).
Добавлена явная таблица STATUS_TO_ENUM (5 видимых совету статусов → enum),
statuses строится map'ом. Тип доказан образцом соседней AdminWriteoffsPage,
которая так же кормит listWriteoffProposals enum-массивом.
- OnboardingCoopAcceptCpp/composable: `stepToRegistryId[meta.id] as number` →
typeof-guard (робастно к noUncheckedIndexedAccess).
НЕ тронуты api-слойные `return result as <ViewType>` (OperatorIssuance/Reception/
InventoryLabeling/ReturnClaims, AdminWriteoffs): это НЕСУЩЕЕ сужение Zeus-скаляров
(DateTime/ID типизированы Zeus как unknown; view-тип сужает их до string/
NonNullable через Omit&{…}). Убрать = сломать типы (unknown не присваивается
string) или добавить бессмысленный рантайм-.map(). Направление каста безопасное
(unknown→точный тип), у каждого инлайн-обоснование. Это не `as any`.
ESLint 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Глобальный ::selection красит текст в var(--p-ink) (тёмный на свет-теме)
поверх полупрозрачного фона — на тёмной поверхности тоста выделенный текст
пропадал. Scoped-override для .q-notification/.toast: полупрозрачная светлая
подложка + светлый текст, читаемо на всех типах тостов.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Глобальный ::selection красит текст в var(--p-ink) (тёмный на свет-теме)
поверх полупрозрачного фона — на тёмной поверхности тоста выделенный текст
пропадал. Scoped-override для .q-notification/.toast: полупрозрачная светлая
подложка + светлый текст, читаемо на всех типах тостов.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
В MyOrdersPage в template-выражении был TS-каст
`(order as any).domainStatus as MarketplaceOrderStatusView` (тащился из
оригинала). Vue компилирует template-выражения в обычный JS, поэтому `as`
валит render-функцию в рантайме: `[Quasar] boot error: SyntaxError:
Unexpected identifier 'as'`. Так как install.ts импортирует MyOrdersPage
на старте (markRaw), падал весь boot приложения.
- MyOrders: доменный статус берём из items по id в script-хелперах
(rowStatusLabel/isOrderCancellable) — типобезопасно, без `as` и без `any`.
- MarketplaceCatalog: убран избыточный `cc as ...[]` (api уже типизирован),
`value as CatalogSort` → type-guard isCatalogSort по sortOptions.
- OperatorInventoryLabeling: `v as BarcodeStrategy/BarcodeFormat` в сеттерах
BaseSelect → type-guard'ы isStrategy/isFormat по спискам значений enum.
Во всех мной написанных страницах/диалогах marketplace type-assertion-кастов
больше нет (import-алиасы `type X as Y` — не касты). ESLint 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Устаревшая формулировка в docstring backfill-сервиса: после перехода на ключ
doc_hash идемпотентность/уникальность реестра — по (coopname, doc_hash), а НЕ по
(coopname, package). package сознательно не уникален (один процесс = несколько
разных документов с разными подписантами), поэтому ключом записи он быть не может.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ось упорядочивания версий подписи одного 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>
Стол ПВЗ переориентирован с «списка всех ПВЗ кооператива» на рабочее место
оператора ЕГО кооперативного участка (председатель КУ или доверенное лицо).
- entities/OperatorBranch: стор активного КУ оператора на marketplaceWhoAmI
(+ детали ПВЗ + best-effort core getBranches) и шапка OperatorBranchBar
(название/адрес/статус ПВЗ, без кодов; селектор при нескольких КУ).
- Операторские страницы (поставки/приёмка/маркировка/выдача/возвраты/склад/
сводный) переведены на активный КУ из стора — убраны ручные поля «Код КУ»
и чипы-коды, автозагрузка по активному участку.
- Новая страница «Доверенные лица»: председатель КУ добавляет/снимает
доверенных (features/Branch/ManageTrusted → addTrustedAccount/
deleteTrustedAccount); правки гейтятся chairman.
- Страница «ПВЗ кооператива» (PvzList) удалена со Стола ПВЗ — управление
сетью ПВЗ живёт на столе администратора.
- Админ-страница ПВЗ: кнопка «Карта» (OSM, без ключа) для точки с координатами.
Бэкенд-фоллоуапы (вне scope): обогащение whoAmI именем/доверенными для
оператора-только-доверенного; auth addtrusted для trustee КУ (сейчас chairman
кооператива); грант-нюанс — admin видит Стол ПВЗ через :all.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Новая страница market-admin: соединяет список КУ (core getBranches) с их
ПВЗ-детализациями (marketplaceListKUDetails), председатель подключает участок
как пункт выдачи (адрес/контакты/режим работы → marketplaceDetailKU) и
управляет статусом/геокодингом. Диалог детализации переведён на канон
(BaseDialog/BaseButton + raw q-form с :rules), предзаполнение из карточки КУ,
правила времени/email вынесены в script (pug съедает backslash в inline-regex).
Статус-бейдж без dot (dot скрывал подпись), тултип с ошибкой геокода.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Два уточнения по замечаниям:
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>
Самоаудит по замечанию: 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>
Переверстка остальных страниц стола поставщика на MONO Platform v2 (как
«Мои предложения»):
- Входящие заказы: инфо-баннер с persist-скрытием, канон-`.tabbar` с deep-link
?status=, скелетоны вместо q-inner-loading, EmptyState, акцепт/отказ на карточке.
- Подготовка отгрузки, Подпись приёмки, История выплат: canon-таблица .table-wrap
+ TableSkeleton (каркас не дёргается), статусы через BaseBadge (точка+цвет из
токенов), EmptyState с иконкой, ghost-refresh, инфо-баннер.
- Диалог подписи акта: BaseDialog + BaseButton + .banner вместо q-dialog/q-banner
bg-primary.
Убраны дубли заголовков (заголовок в топбаре), легаси --mp-*/mp-role-*,
FontAwesome → Material q-icon. Скрываемый баннер вынесен в общий composable
useDismissibleBanner (DRY, 5 страниц).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Самоаудит видимости документов выявил две регрессии относительно прежнего 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>
«Мои предложения»: info-карточка .banner с persist-скрытием, скелетоны вместо
лоудеров, канон-вкладки с deep-link ?status=, кнопка «Создать» в правом верхнем
углу через teleport, republish для WITHDRAWN. Форма создания/редактирования:
единый заголовок (без дублей), один .banner, sticky нижний бар «Отменить/Далее/
Сохранить» фикс-высоты, форматирование цены при префилле. REJECTED правится и
переотправляется через форму с показом причины отклонения.
Изображения: единая галерея — добавление (не замена), удаление и смена обложки
у любой картинки с самого начала. Backend отдаёт/принимает bucket_key (сохранить
существующее) либо base64 (новое); чужой ключ отклоняется. Signed-URL чтения
стабилизирован в окне 300с — не мигает при polling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Завершение перехода одним 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>
Старый 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>
Картинки оферт SPA грузит как <img> с другого origin (в dev — другой
порт, чем у бэкенда :3028). Глобальный helmet ставит CORP same-origin,
браузер режет загрузку (ERR_BLOCKED_BY_RESPONSE.NotSameOrigin) — CORS-
обход на это не влияет, это другой механизм. Переопределяем CORP на
cross-origin на успешном ответе ручки /api/storage: доступ к объекту
защищён HMAC-подписью с TTL в URL, а не origin'ом, поэтому ослабления
безопасности нет. Работает и в dev, и в prod независимо от раскладки
origin'ов.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Поиск по документам — в основном по фамилии подписанта, а не 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>
- Поставщика после публикации возвращаем на стол «Мои предложения»
(там сразу видна оферта «На модерации»), а не в каталог заказчика.
- CatalogOfferCard: медиа-область теперь q-carousel (свайп/стрелки,
без точек, изображение во всю ширину) вместо одиночного <img>;
проп images[] (обратно совместимо с preview).
- Прокинули изображения оферты в карточку на всех столах (мои
предложения, модерация, каталог) — раньше всегда была заглушка.
- Единицы измерения по-русски: вынес общий справочник
shared/lib/consts/marketplace-units (опции + короткая подпись) и
helper shared/lib/utils/marketplaceOfferImages (обложка первой);
убрал 4 дубля карты единиц и 2 дубля сортировки картинок (DRY).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Убрал из SuccessAlert технический id и английскую константу статуса
(result.status) — пользователю это не нужно. Сам SuccessAlert не трогаю
(правится в dev-ветке).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Карусель предпросмотра: убран дефолтный padding слайда q-carousel
(изображение во всю ширину), убраны точки-навигация — только свайп и стрелки.
- Черновик формы теперь сохраняет и изображения (base64 + data-URL превью
после reload), если суммарный размер укладывается в бюджет (~2M base64
символов); сверх бюджета или при QuotaExceeded — поля сохраняются без
картинок. Сохранение дебаунсится (400мс), чтобы не сериализовать base64
на каждое нажатие; coverIndex тоже в черновике.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- persistentState: при создании предложения форма (поля + текущий шаг)
автосохраняется в LocalStorage и восстанавливается после перезагрузки;
очищается при успешной публикации. Изображения не сохраняются (object-URL
невалиден после reload, base64 не влезает в LocalStorage).
- Карусель предпросмотра: slide-right/slide-left транзишн — изображения
сматываются вбок неразрывно, а не исчезают/появляются.
- Карточка-предпросмотр переверстана по канону: тень --p-shadow-card,
название + категория в шапке, цена крупно слева и наличие чипом справа
(pos/neg), блок «Характеристики» с разделителем и аккуратными строками.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Цена: символ валюты из useSystemStore().governSymbol (блокчейн-параметр
root_govern_symbol), а не хардкод «₽» — при смене символа цепи фронт не правим.
- Количество и чекбокс «без ограничения» в одной строке (поле уже не во всю
ширину); чекбокс выровнен, строка не прыгает.
- Уменьшен вертикальный gap между полями (p-4 → p-3) — ближе к канону.
- Убраны дубли заголовков шагов «Фотографии товара»/«Как набираются заказы»
(их рисует VerticalStepper из step.description).
- Предпросмотр: q-carousel со свайпом и стрелками вместо нижних миниатюр
(место под миниатюрами зарезервировано под будущие варианты товара).
- BaseRadioCard: hover по выбранной карточке больше не перекрашивает её
в белый (selected:hover сохраняет выбранный фон).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Фундамент 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>
- Поля формы плотнее (dense + no-error-icon) по канону MONO.
- Блок количества не «прыгает»: чекбокс «без ограничения» сверху,
поле количества всегда отрисовано (disable при безлимите), не v-if.
- Цена — два знака после запятой (рубли/копейки): regex 2 знака,
ввод через точку или запятую, отображение через formatAsset2Digits.
- Изображения: выбор обложки кликом, непрозрачный бейдж «Обложка»,
заметная кнопка удаления; обложка уходит в payload первой.
- Шаг «Проверка» — карточка-предпросмотр (главное фото + слайдер
миниатюр + название/категория/цена/наличие/характеристики).
- Ошибки/успех через канон FailAlert/SuccessAlert (правый нижний угол,
разворот GraphQL-ошибки вместо [object Object]); убран полноэкранный
Loading — лоадер на кнопке.
- app.ts: express.json/urlencoded limit 2mb → 160mb (base64-загрузка
изображений в GraphQL); зеркально в playbooks nginx.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Баг 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>
Только неочевидное поле — наименование банка получило подсказку ПАО "Сбербанк".
Остальные поля ИП самоописательны (формат даты, кол-во цифр, «как в паспорте»).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Поля краткого/полного наименования, должности представителя и наименования
банка получили placeholder-подсказки (ПК "Ромашка", Председатель совета,
ПАО "Сбербанк") — показываются при фокусе пустого поля.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Форма «Создать предложение» переверстана по канону MONO Design System как
VerticalStepper-мастер (Товар → Цена и наличие → Условия поставки → Изображения
→ Проверка; эталон CreateMeetForm): бизнес-язык вместо time_based/volume_based,
BaseRadioCard под отсечку, reserve-hint-space против layout shift.
Изображения товара (закрыт gap parent-UX, где MVP-epics уронили offer-фото):
- jsonb-колонка marketplace_offer.images ({bucket_key,content_hash,mime_type}[],
индекс 0 = обложка); миграция v7 (DDL через synchronize);
- MarketplaceOfferImagesService на @UseBucket('stol-zakazov:images', AR31) —
зеркало return-claim-канона; ключ content-addressed offers/<coop>/<owner>/<sha>;
- base64-инпут images в create/update DTO (≤8, ≤10МБ, jpeg/png/webp), декод+put
в offer.service + cleanup сирот при провале записи;
- @ResolveField images → HMAC-signed URL (ленивый резолв, не в каждом list);
- codegen: schema.gql + zeus (controller+sdk) + offerSelector.
DRY: fileToBase64 вынесен в shared/lib/utils, два return-claim-дубля переключены.
Research вариантов продукта (off-chain PG-only, контракт не трогается; счётчики
3.4 на variant-уровень — самая инвазивная часть; FR18a снимается, не нарушается)
зафиксирован в плане проекта (Эпик 13, research-only).
Тесты: offer-service 43/43 (вкл. 3 новых на images), moderation+counters зелёные;
tsc контроллера 0, ESLint desktop 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Два бага L3-онбординга заказчика:
1) Председатель (и совет) видели рабочие страницы стола заказчика
(Каталог/Мои заказы/Сводный) до подписи оферты: страницы требовали
Offer:read / Order:read:own, а admin/board дают голый Offer:read и
Order:read:all, который разворачивается в :own — гранты «протекали»
на стол заказчика. Фикс: рабочие страницы требуют orderer-эксклюзивный
Order:create (нет у admin/board, не покрывается :all-разворотом) —
стол заказчика гейтится персональной подписью для любой роли.
2) Подпись оферты не долетала: getOnboardingState читал AgreementRepository
(soviet::agreements3), а ЦПП «Стол заказов» — ПРОГРАММА (program_id=2),
и wallet::signagree пишет подпись в wallet::users.programs[]. Источник
истины — UserAgreementRepository.findProgram(program_id) (канон
AgreementService.fetchProgrammatic). Из-за чтения не той таблицы
requires_gate оставался true навсегда. Фикс: читаем программную подпись.
3) Авто-переключение после подписи: фронт коротко поллит getOnboardingState
пока подпись синхронизируется в PG (wallet::signagree уже подтверждён
цепочкой), затем loadDesktop+навигация — без ручного refresh.
Тесты: onboarding-service переписан под программный источник (6), grants (8) — 14/14.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Стол заказчика до подписи персональной оферты ЦПП показывал все
страницы (Каталог и т.д.) — провайдер грантов гейтил только на уровне
кооператива (coopAcceptance), а L3-подпись конкретного пайщика в гранты
не заходила. Страница подключения требовала Offer:read — тот же грант,
что и Каталог, поэтому не отличалась от рабочих страниц.
- backend resolveGrants: для orderer-роли спрашиваем
MarketplaceOnboardingService.requires_gate; пока не подписал —
вместо orderer-прав выдаём маркер Onboarding:orderer (симметрия
L1-гейту председателя Extension:configure). Прочие роли не зависят.
- install.ts: страница подключения первой в столе, requires
Onboarding:orderer; после подписи авто-скрывается (grant-driven).
- страница: один подписываемый документ (оферта), убран дубль-заголовок,
refresh грантов после подписи (loadDesktop+loadExtensionRoutes), без
поллинга и без loadDesktop на каждом переходе.
- unit-тест провайдера: L1/L3-гейты, 8/8.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- WorkspaceSwitcher: видимость столов через канон desktop.isWorkspaceVisible
(grants) вместо локального meta.roles — marketplace-столы (Заказчик/Поставщик/
ПВЗ) больше не видны до приёма ЦПП Советом. Был рассинхрон с CmdkMenu: при
merge grants-канона переключатель в шапке забыли перевести на канон.
- OnboardingCoopAcceptCpp: убрана дублирующая шапка страницы; «Не подключено»
и таймер сведены в один ряд в CouncilOnboardingCard (новый слот #status +
проп subtitle); список шагов разрежён, чип «Ожидаем решение совета» больше
не на всю ширину; экран «подключено» переведён на канон EmptyState с
success-акцентом иконки.
- Стол заказчика: иконка «Готово к получению» fa-box-check → fa-box-open
(первой нет в FA6 free — не рисовалась).
- Экосистема: meta.hidden=true (скрыта из меню/поиска; маршрут и код оставлены
до публикации раздела).
- Дизайн-система: страница-витрина удалена полностью + вычищены ссылки на неё
в extensions/market/CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LeftDrawerMenu.filteredRoutes и default.vue::paletteWorkspaces переключены
с legacy meta.roles.includes(userRole) на DesktopStore.isPageVisible /
isWorkspaceVisible. Для grant-стола (marketplace, etc) видимость пунктов
сверяется с meta.requires против выданных бэкендом прав; для legacy-стола
canon-функции сами fall-back'аются на meta.roles по core-роли — поведение
по умолчанию сохранено.
meta.hidden и meta.conditions остаются дополнительными фильтрами
(participant/chairman/soviet используют conditions; capital — hidden).
Marketplace2-only: canon-grants (isPageVisible/isWorkspaceVisible)
существуют только в marketplace2; в dev этих функций в DesktopStore нет —
там фильтр остаётся legacy meta.roles.
Заголовок workspace-row получал outline только когда стол активный
(.is-active .row.is-selected). На неактивных столах при навигации курсор
«терялся»: только лёгкая смена фона на surface-2, без рамки — в отличие
от page'ей, которые подсвечиваются outline'ом всегда. Выровнял правило:
outline на любом .workspace-row.is-selected.
На верхней/нижней позиции стрелка дальше прыгала в противоположный
конец списка из-за (idx+delta+len)%len. Заменено на clamp в [0..len-1]:
курсор остаётся на месте, как в нативном listbox/меню. Пустое выделение
+ ↓ ставит на первый, ↑ — на последний.
Активный workspace был position:sticky top:0 — при навигации стрелкой
вверх курсор-выделение «исчезал» за sticky-overlay'ем, потому что
scrollIntoView({block:'nearest'}) считал элемент видимым в normal flow,
не учитывая sticky-перекрытие. Активный стол и так идёт первым по
сортировке + есть badge «Активный», sticky не нужен.
- 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().
- 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.
Фон и текст тоста (.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>
Фон и текст тоста (.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>
q-pa-lg на flat-карточке SignAgreementDialog — документ больше не прижат
к границе на странице кошелька пайщика.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
q-pa-lg на flat-карточке SignAgreementDialog — документ больше не прижат
к границе на странице кошелька пайщика.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Восстановленный после merge dev сервис не был добавлен провайдером
в MarketplaceExtensionApplicationModule — NestJS падал при boot:
"Nest can't resolve dependencies of the MarketplaceReturnClaimResolver
... Symbol(MarketplaceBranchOwnershipService) at index [2]".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Резолв конфликтов:
- ledger2/operations.hpp: оставлен CONVERT_TO_MEMBER (marketplace2) +
принята новая логика withdraw из dev (WITHDRAW_PENDING вместо BLOCK).
- ledger2/wallets.hpp: реестр расширен до 16 кошельков
(USER_SHARED=6 с MARKETPLACE_ORDER_LOCK + COOPERATIVE=10 с WITHDRAW_PENDING).
- WalletProgramWidget.vue / WalletWidget.vue: убраны неиспользуемые helper'ы из marketplace2,
принят рефактор dev.
- schema.gql + zeus/* (controller + sdk): полная регенерация после merge.
Сопутствующие правки:
- Восстановлен marketplace-branch-ownership.service.ts (отсутствовал в origin/marketplace2,
но был импортирован из marketplace-return-claim.resolver.ts — взят из ветки
chore/trustees-ownership 46f5269b89).
- Исправлена опечатка MARKETPLACE_KU_CHAIRMEN_SERVICE → MARKETPLACE_KU_CHAIRMAN_SERVICE
в marketplace-desktop-grants.provider.ts.
- orderSelector.ts: blocked_amount → locked_amount, удалены did_convert/did_assign
(привести selector к новой схеме MarketplaceOrderCreateTxSnapshot).
- Пересобраны cooptypes и inter (stale dist после merge).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Глобальный 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.
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+&.
Снижает минимальный порог состава совета кооператива на этапе
Install Coop с 5 до 3 человек. Единственный ограничитель —
контракт soviet::createboard (см. consts.hpp + createboard.cpp:58).
Backend (install.interactor) и desktop (SetSovietForm) собственных
числовых проверок не имеют — фронт требует только «хотя бы одного»,
бэкэнд передаёт массив как есть в blockchainPort.createBoard.
ResourceInfoWidget в extensions/powerup импортирует ModalBase через
barrel: import { ModalBase } from 'src/shared/ui'. После сноса
re-export'а vite валился SyntaxError: does not provide an export.
Возвращаю строку обратно — папка ModalBase уже восстановлена
предыдущим коммитом.
В прошлой волне снёс 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 и канон-токены — отдельная волна.
При 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.
В 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>
Простые модальные диалоги переведены с 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>
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>
* Глобальный 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>
* 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>
* 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>
Замечания ревью @ant Review 31:
(1) o.mkt.lock теперь идёт **прямо** w.wal.share → w.mkt.order
с проводкой Дт 80 / Кт 86 — без промежуточного w.wal.member и без
conditional o.wal.conv. Один шаг ledger2 в createorder.
- createorder: убран каскад conv+lock; lock сам делает работу
- signiss2 actual>ordered: убран conv, остался только lock(diff)
- Достаточность средств: только w.wal.share.available
(2) Убраны из комментариев и YAML фразы «без транзита 91» / «через транзит 91»
как нерелевантный шум (никого не интересует прошлое):
- cancelorder.cpp, accretrn.cpp, execwroff.cpp
- processes.hpp: пересчитан состав операций под новую схему
- controller TS: marketplace-return-claim.{types,service}.ts,
marketplace-canonical-blockchain.port.ts, marketplace-order.types.ts,
marketplace-application.module.ts
(3) Убраны упоминания «может быть выведено пайщиком на паевой» —
с w.wal.member на w.wal.share вывод НЕ предусмотрен в этой схеме.
Инварианты I5 переписаны под новый источник lock (w.wal.share вместо
w.wal.member). spec.ts фикстуры обновлены: happyPathOrderFlow теперь
4 операции (lock + purch + payout + consum) без conv. 31/33 теста зелёные,
2 fail — pre-existing baseline (Story 11.1 cooptypes).
C++ контракт пересобран через ARM-native CDT (dicoop/blockchain_v5.1.1:dev) —
0 errors, marketplace.wasm + .abi созданы успешно.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
build-all.sh / build.sh хардкодят dicoop/blockchain:latest (amd64-only) —
на ARM падают «exec format error». Multi-arch образ dicoop/blockchain_v5.1.1:dev
имеет нативный arm64 manifest; команда для marketplace в одну строку
зафиксирована в новой секции «Сборка контрактов на ARM-машине».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
В 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, который НЕ канон — пришлось переверстать
тосты. Фиксирую правило, чтобы не повторять.
Завершаю аудит — добавил 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 под шапкой) — отдельная
задача, тут только мобильная читаемость шапки.
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, чтобы кнопка не растягивалась на всю высоту шапки.
- 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>
- 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>
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>
Обновлены описания операций o.mkt.block / o.mkt.unblk / o.mkt.consum
в стандарте процесса под reserve-wallet модель (введён 2026-05-28):
- Заголовок раздела ledger2 (комментарий перед секциями): добавлен новый
USER_SHARED-кошелёк w.mkt.order, обновлено описание w.mkt.member.
- o.mkt.block: BLOCK → TRANSFER w.mkt.member → w.mkt.order; L3 теперь
пара движений на двух кошельках вместо split available/blocked на одном.
- o.mkt.unblk: UNBLOCK → TRANSFER w.mkt.order → w.mkt.member; зеркало 12c.
- o.mkt.consum: REVOKE → BURN с w.mkt.order; резерв сжигается без получателя
при подписании АПП выдачи (signiss2).
- Список ledger2-кошельков в Секции 6 — упомянут w.mkt.order.
Бизнес-словарь: «блокировка» → «резерв», «разблокировка» → «снятие резерва»,
«заблокированная сумма списывается» → «резерв сжигается». Сами op_code
(o.mkt.block / o.mkt.unblk / o.mkt.consum) и amount_ref / triggered_by /
human_name (последний обновлён для consistency) — сохранены, чтобы
бухгалтер мог пройти по тем же шагам процесса в журнале операций.
p.mkt.return.standard.yaml и p.mkt.wroff.standard.yaml не требуют правок —
эти flow используют ISSUE/NONE, без BLOCK/UNBLOCK/REVOKE.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Инвариант I5 переименован: `BlockedConsistency` → `ReserveConsistency`.
Считает резерв через пары TRANSFER + BURN в новой модели:
Σ TRANSFER(w.mkt.member → w.mkt.order) // o.mkt.block — резерв входит
− Σ TRANSFER(w.mkt.order → w.mkt.member) // o.mkt.unblk — резерв снят
− Σ BURN(w.mkt.order) // o.mkt.consum — резерв сожжён
= Σ balance(w.mkt.order у пайщиков)
Раньше I5 сверялся с suma blocked у w.mkt.member-кошельков (поле `.blocked`).
Поле `.blocked` для marketplace-кошельков теперь deprecated (всегда 0) —
резерв выражается через `.available` отдельного кошелька w.mkt.order.
Инвариант I6 переименован: `NoOrphanedBlocks` → `NoOrphanedReserves`.
Логика парности по process_hash (consum без block, unblk без block,
block+unblk+consum = двойное закрытие) не меняется — op_code'ы
o.mkt.block/unblk/consum сохранены.
Фикстуры в spec.ts переписаны под новую модель: walletFrom/walletTo
для block теперь w.mkt.member → w.mkt.order, для unblk наоборот,
для consum — walletFrom=w.mkt.order (BURN).
ВНИМАНИЕ — pre-existing baseline:
`marketplace-process-trace-coverage.spec.ts` и 4 теста в `*-invariants.spec.ts`,
которые опираются на `MARKETPLACE_OPERATION_CODES` (фильтр через
`Ledger2.LEDGER2_OPERATION_REGISTRY.contract === 'marketplace'`), падают
с pre-existing проблемой: `cooptypes/ledger2/operations.ts` содержит только
2 legacy marketplace-операции (`o.mkt.supply`/`o.mkt.recv`) вместо 13 новых.
Это известный блокер Эпика 11 Story 11.1 (синхронизация TS-стороны cooptypes
с C++ operations.hpp) — выходит за scope этого PR.
Проверено: 32/36 тестов в marketplace-ledger2-invariants.spec.ts passed
(те же 4 теста были сломаны на pre-state baseline).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DTO `MarketplaceMemberWallet.wallets` теперь возвращает массив из 4 USER_SHARED-кошельков
пайщика для Стола заказов:
1. w.wal.share — Паевой | Цифровой Кошелёк
2. w.wal.member — Членский | Цифровой Кошелёк
3. w.mkt.member — Членский | Стол Заказов (свободные средства программы)
4. w.mkt.order — Резерв | Стол Заказов (под конкретный Order; новый, 2026-05-28)
Поле `blocked` остаётся в DTO для wallet/withdraw flow и legacy данных, но для
marketplace-кошельков (w.mkt.member, w.mkt.order) всегда `0` после миграции
с BLOCK/UNBLOCK на пары TRANSFER — резерв выражается через `.available`
отдельного кошелька w.mkt.order. Обновлены description'ы @Field и @Query на
бизнес-словаре (без story-ссылок).
Regen:
- cooptypes/ledger2/wallets.generated.ts: wallets=16, mapping=8 (из C++).
- controller/schema.gql, controller/zeus/index.ts, sdk/zeus/index.ts:
только description-only diff в MarketplaceMemberWallet/Entry, никаких
breaking changes в типах GraphQL/zeus.
Unit-тесты marketplace-member-wallet-resolver.test.ts обновлены под 4-й
кошелёк (4/4 passed, runInBand).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Семантика in-code не меняется: Ledger2::apply(coopname, op, amount, ...)
вызывается с теми же operation_id (o.mkt.block / o.mkt.unblk / o.mkt.consum),
изменения только в doc-блоках и inline-комментариях:
- createorder.cpp: "BLOCK на w.mkt.member" → "TRANSFER w.mkt.member → w.mkt.order"
- cancelorder.cpp: "сумма остаётся на .available" → уточнено, что снятие резерва
- declineorder.cpp: "резерв возвращается заказчику" → уточнено через TRANSFER
- expireorder.cpp: тот же flow
- signiss2.cpp: actual<ordered → снятие части резерва (UNBLOCK_ON_CANCEL),
actual>ordered → доп. резерв (BLOCK_FOR_ORDER),
CONSUME_BY_MEMBER теперь BURN с w.mkt.order
Поведенческих изменений в C++ нет — вся семантика выражена через изменения
в OPERATION_REGISTRY (предыдущий коммит). Этот коммит — только документация
точки вызовов, чтоб не путать читателя.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Вводит USER_SHARED-кошелёк w.mkt.order (MARKETPLACE_ORDER_LOCK) — резерв
средств пайщика под конкретный Order. Заменяет механику BLOCK/UNBLOCK
на отдельных balance-полях кошелька w.mkt.member на пару кошельков:
- o.mkt.block (BLOCK_FOR_ORDER): TRANSFER w.mkt.member → w.mkt.order
- o.mkt.unblk (UNBLOCK_ON_CANCEL): TRANSFER w.mkt.order → w.mkt.member
- o.mkt.consum (CONSUME_BY_MEMBER): BURN с w.mkt.order (вместо BURN_BLOCKED w.mkt.member)
Operation_id'ы (o.mkt.block / o.mkt.unblk / o.mkt.consum) сохранены —
изменилась только реализация в OPERATION_REGISTRY, не имена; внешние
op_code в blockchain_actions читаются без сдвига смысла.
Параметр Ledger2::apply(coopname, op, amount, ...) не меняется — сигнатура
вызовов в .cpp идентична, отдельная правка реализаций следующим коммитом
ограничивается обновлением doc-комментариев.
LEDGER2_WALLET_REGISTRY: 15 → 16 (USER_SHARED 6 → 7).
LEDGER2_USER_SHARED_PROGRAM_MAPPING: 7 → 8 (w.mkt.order → program_id=2).
Это marketplace-сторона миграции, повторяющая подход dev для wallet/withdraw
(WITHDRAW_PENDING кошелёк-резерв; ADR — общий принцип «убрать blocked,
заменить на пары TRANSFER в выделенные кошельки»). Enum WalletOp в этом PR
НЕ модифицируется — значения BLOCK/UNBLOCK/BURN_BLOCKED остаются для legacy
wallet/withdraw flow, который мигрирует следующим merge dev → marketplace2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Конфликты в 4 marketplace-резолверах (apl-reception, inventory, issuance,
return-claim) разрешены в пользу этой ветки: ownership через
MarketplaceKuChairmanService (trustee+trusted), а не branchOwnership из
marketplace2 (PR #16). Осиротевший marketplace-branch-ownership.service.ts
и его проводка удалены — заменены kuChairmanService.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Без 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>
extensionRepository.update заменяет config JSONB целиком; chairman/powerup/meet-tracker писали захваченный при initialize снимок поверх свежих данных. Теперь read-modify-write по актуальному config из БД с наложением только владомых полей.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
В левом дровере был маленький горизонтальный скроллбар, и трекпадом его можно
было «оттянуть», обнажая правую границу — дровер выглядел скроллируемым, хотя не
должен. Причина: у .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>
Если проголосовать и сразу нажать «Утвердить», утверждение падает с ошибкой —
бэкенд ещё не учёл голос из блокчейна. После успешного голоса держим состояние
загрузки пункта (а значит и кнопку «Утвердить» в :loading) ещё VOTE_SETTLE_MS
(3с), за это время голос успевает обработаться. На ошибке загрузка снимается
сразу. Заодно ручное мутирование processingDecisions + setTimeout-хак заменены
аккуратным реактивным хелпером setProcessing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Повестка совета показывает все НЕутверждённые вопросы. Голос «за»/«против» не
должен убирать пункт — он остаётся неутверждённым, лишь помечается отметкой
голоса. Убрал ошибочное actedDecisionIds.add из onVoteFor/onVoteAgainst
(оставлен только тихий рефетч). Скрытие через actedDecisionIds оставлено только
в onAuthorizeDecision — там пункт исполняется и должен уйти из повестки без
возврата от отстающего поллинга.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Две проблемы при голосовании/утверждении в повестке совета:
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>
Две независимые проблемы при выдаче разрешения на уведомления:
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>
Кнопки взноса и возврата на столе пайщика недоступны, пока пайщик не
подписал главное соглашение цифрового кошелька (type='wallet') — как и
сама карточка кошелька, которая не появляется без подписанного соглашения.
Геттер isWalletAgreementSigned в сторе Wallet читает тот же список
соглашений пайщика, что и RequireAgreements.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
«(опционально)» в заголовок + пояснение в описании: если есть пайщики —
импортируйте CSV, если нет — двигайтесь дальше. Чтобы шаг не воспринимался
обязательным.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
readonly outlined q-input рендерится с dashed-рамкой (дефолт Quasar) и
обрезал значение (…UJR4) — выпадало из канона и выглядело плоско.
Сделал ключ surface-2 панелью: eyebrow-лейбл + copy-иконка в шапке,
mono-значение целиком (word-break) ниже. Даёт вес и убирает «ёлочку».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Прошлый вариант разъезжался: «Скопировать» висел сиротой справа, провалы
между блоками, узкая потерянная кнопка, карточка 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>
Голый «Потребительский Кооператив» в примере провоцировал вводить просто
ОПФ без расширения. Дал полный пример «...Социального Комплекса» во всех
трёх падежах — чтобы вводили расширенную ОПФ+, а не базовую форму.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Это не переменные, а постоянные параметры, которыми настраивается фабрика
документов кооператива. Переименовал label и intro шага.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Микроотступ стал лишним и снова раздвигал поля слишком широко — откатываю.
Разделение полей обеспечивает сам reserve-hint-space, дополнительный
глобальный отступ не нужен.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Постоянный hint под полем занимал место и отвлекал. Перенёс примеры
наименований ОПФ+ в placeholder (видны только в пустом поле), убрал hint.
Добавил reserve-hint-space всем q-input формы — строка под ошибку
резервируется без текста, валидация не вызывает layout shift.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Без зазора текст hint/error верхнего поля почти касался границы нижнего.
Вернул маленький отступ, но системно и один раз — глобально в quasar-canon:
margin-bottom: var(--p-1) только полям .q-field--with-bottom (те, что
резервируют нижнюю строку). Работает везде (BaseForm, формы установки,
регистрация), в стеках не складывается в дыру, инлайновые поля не трогает.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Канон-плотность полей (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>
- 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>
.extension-page уже внутри .catalog-shell__content с padding var(--p-6);
собственный padding давал двойной зазор от шапки до кнопки «Назад».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Весь контент страницы расширения обёрнут в канон-панель (surface+border),
больше не лежит на голом фоне
- В режиме настроек добавлена кнопка «Отменить» (CancelButton) рядом с
«Сохранить» — явный выход без сохранения; кнопка «Назад» страницы тоже работает
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Кнопка «Назад» убрана из шапки (useBackButton снят), добавлена канон-кнопка
под шапкой на самой странице
- Логотип AutoAvatar центрирован в колонке
- DesktopsList: центрированный inset-блок (выделяется), а не плоский список
- ext-actions toggle: «включено» и «удалить» снова в одной строке
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Триггер ринг-анимации перенесён с hover самого SVG на hover карточки
(.app-card:hover :deep(.ring-seg)).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Статус идёт сразу под заголовком (динамично) — без дыры под однострочными
названиями. Обрезку в 2 строки оставили, чтобы длинный заголовок не ломал сетку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Заголовок резервирует 2 строки (clamp+min-height), аватар по верху —
статус и описание на всех карточках начинаются на одной линии, не прыгают.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Верхний ряд: логотип + (заголовок, под ним статус); описание во всю
ширину ниже; футер «Подробнее» прижат к низу для выравнивания карточек.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Палитра колец → мягкие приглушённые тона + CSS saturate(.5)/opacity .85,
чтобы знак читался как тихая текстура, а не неоновое пятно.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- AutoAvatar параметризован: props size/radius/background/ringColor
с обратно-совместимыми дефолтами (CoopCard не затронут)
- ExtensionCard: буква-монограмма заменена на уникальный rings-логотип,
seed = имя расширения; цвет кольца детерминирован по seed из палитры
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
Кооперативные участки, Регистрационные взносы, Ключ кооператива,
Провайдер платежей, Контакты кооператива:
- убран дублирующий заголовок страницы (есть крошка в шапке);
- описание перенесено с голого фона на канон-поверхность .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>
Заголовок страницы уже показан крошкой в шапке — повтор h2 убран.
Пояснительный текст вынесен с голого фона на канон-поверхность (.banner
с инфо-иконкой). ApprovalsPage не трогаем — там заголовка страницы нет.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
Шаблон двухкорневой (кнопка + Teleport затемнения), class из родителя
(LeftDrawerMenu) не наследовался автоматически. inheritAttrs:false +
v-bind=$attrs на корневую кнопку — атрибуты направлены явно.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Подписи секций «Вопрос на повестке»/«Проект решения» переведены в
надстрочные метки (uppercase, трекинг, ink-3) — отдельный ярус ниже
заголовка окна, чтобы не казаться конкурирующим заголовком рядом с
крупным заголовком самого документа. Текст вопроса — читаемый body.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
- OnboardingStepsCard: под заголовком «Адаптируйте кооператив…» добавлен
поясняющий текст (что происходит дальше: решения совета в электронной
форме, вступление в «Восход», импорт пайщиков, общее собрание) + плашка
срока адаптации с отсчётом.
- Чек-лист шагов переведён на канон-вертикальный степпер (.stepper--v):
пройденные — галочка/заливка primary, текущий — подсветка номера,
действия — канон BaseButton; agenda/import/meet типы сохранены.
- Диалог «Предложение повестки» (объявить собрание совета) переведён
с ModalBase на канон BaseDialog + BaseButton.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- CommonHeader: крошка строится из route.matched (путь внутри стола),
напр. «Отчётность › Календарь»; корень стола не выводится; плоские
страницы дают одну крошку как прежде; pageTitleOverride сохранён.
- CoopWalletsPage: переводы временно скрыты под флагом transferEnabled=false
(открывались с L3-кошельков пайщиков; вернём для кооп-кошельков позже).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
- 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>
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>
Канон-инпут вместо q-input: BaseInput расширен поддержкой type="date"
и clearable (+ emit clear) — это нужно для фильтра по датам. Фильтры
в ExtensionLogsList переведены на BaseInput.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Монитор ресурсов: баннер-предупреждение на всю ширину сверху, ряд
«Как это работает» (2/3) + кошелёк AXON (1/3), ряд CPU/NET/RAM.
Убран 3D-флип карт (он же «прыгал») — детали раскрываются inline.
Все виджеты/страницы/лог сведены к канон-токенам surface/line/ink.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Реестр платежей: кнопки действий больше не 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>
- 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>
Таблицы документов и платежей уже на канон-токенах и самообрамлены
(.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>
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>
Страница получила канон-отступ (--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>
Развёрнутая строка пайщика теперь показывает только его данные;
документы пайщика живут в отдельном «Реестре документов» и здесь не дублируются.
ParticipantDetails упрощён (без q-tabs/ListOfDocumentsWidget), tab-логика
вычищена из таблицы и страницы. Мобильная ParticipantCard переведена на канон-токены
(убраны хардкод-цвета, CardStyles и q-badge статуса → disabled-checkbox).
Таблица сохранена, сортировка по дате вступления (DESC) без изменений.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Убран EntityIdBadge. Зелёная плашка-аватар слева теперь несёт сам номер вопроса
(он же Decision ID): клик по плашке копирует id (тултип «Скопировать №»), hover —
инверсия в сплошной teal. Заголовок снова просто текст вопроса.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EntityIdBadge перенесён из отдельной строки под ФИО в начало строки заголовка:
[#id] → сразу текст вопроса. Inline, vertical-align middle. Раньше висел снизу
и пусто растягивал верхнюю строку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EntityIdBadge с id вопроса под заголовком/ФИО: отображает #<id>, по клику копирует
чистый id (copy-on-click). Канон-компонент, клик не раскрывает карточку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
«Истекает через месяц» перенесён из строки под заголовком в постоянную нижнюю
полоску (footer): срок слева, «Утвердить» справа (только председателю). У обычного
пайщика — только срок, узкая панелька.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Срок теперь подписан «Истекает через месяц» (было просто «через месяц» — неясно).
Удалён статус-чип «Вы за/против» — направление голоса уже видно по подсветке кнопок,
бейдж был избыточным. Убраны связанные computeds и стили.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Кнопки голосования прижаты к правому краю строки. «Утвердить» вынесена вниз
отдельной строкой-footer (справа, hairline сверху) — больше не теснит данные.
Срок «через месяц» и статус-чип «Вы за» перенесены влево под заголовок/ФИО.
Клик по строке раскрывает документ, по кнопкам голосования и footer — нет.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Карточка вопроса перестроена в единую горизонтальную строку: иконка + вопрос/ФИО
слева (flex), компактный блок органов управления (голосование + «Утвердить»),
срок + статус-чип и шеврон — справа, друг рядом с другом. Убрана центрированная
vote-зона с пустотой по бокам. Клик по строке раскрывает документ, по органам
управления (@click.stop) — нет. Подсказка «Утвердить» возвращена в tooltip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Шапка карточки (вопрос/ФИО/срок/статус) кликабельна целиком и раскрывает документ,
справа шеврон-индикатор. Органы управления голосованием — отдельная зона ниже шапки
(сиблинг, не аккордеон): клик по ним голосует/утверждает и документ не раскрывает.
Убрана отдельная кнопка «Документ» — раскрытие теперь по клику на шапку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
QuestionCard: VotingButtons + «Утвердить» вынесены на верхний уровень карточки —
голосовать можно без раскрытия; раскрытие («Документ») открывает только содержимое
документа. Истечение срока остаётся в шапке.
VotingButtons: вернул чек-индикатор «принято советом» (закрашивается при принятии)
вместо иконки verified.
Header-CTA совета (Предложить/Добавить/Импорт): убран push (q-btn--push не попадал
под канон-правило заливки primary → белый шрифт на нетиловом фоне), подписи с заглавной.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Названия столов в реестре workspace'ов заведены вразнобой («Стол
благороста», «Стол вычислительных ресурсов» vs «Стол Совета»).
Добавил text-transform: capitalize на подписи в WorkspaceSwitcher
(заголовок текущего стола + пункты выпадающего меню) и WorkspaceMenu
(карусель + диалог выбора) — первая буква каждого слова заглавная,
одинаково для всех и для будущих столов, без правки строк-источников.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
По фидбеку переработана композиция:
- убраны заголовки секций «Контакты»/«Руководство» и per-строчные
иконки телефона/почты/адреса (из-за них значения «прыгали» вправо
относительно ИНН/ОГРН);
- ИНН и ОГРН теперь в одну строку (адаптивная сетка полей);
- председатель поднят наверх к реквизитам, поле названо просто
«Председатель совета»;
- контакты — те же поля, телефон/email как ссылки (tel/mailto), всё на
единой левой кромке; одна тонкая линия делит реквизиты и контакты.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Реквизиты/Руководство больше не разносят подпись и значение к
противоположным краям (на полной ширине это давало пустой провал и
оторванные значения). Все секции теперь как ContactSheet: подпись
сверху, значение под ней, слева, hairline между строками — один ритм
по всей карточке.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
- 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>
- 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>
Добавлен 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>
- 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>
Переделка после ревью: 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>
Когда страница не телепортирует действия, .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>
- 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>
Симлинк 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>
Крошка: в #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>
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>
Кошельки (по фидбэку — переносы выглядят плохо):
- .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>
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>
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>
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>
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>
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>
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>
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>
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>
Существующая модель 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.
Реализованы три 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 отложен из-за конфликта имён — существующий компонент
имеет другую роль; нужно согласовать канон-нейминг.
Заменил `.full-width.text-center` + flex без выравнивания на flex-column
со `min-height: 360px` — лоадер больше не прижат к левому верху карточки.
Заодно поправил опечатку «подговка» → «Формируем документ».
Корень: я положил символ валюты в #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>
Корень: 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>
[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>
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>
Пять 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>
Удалены типы '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 состояния.
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%
ширины было визуально 'приплюснуто'.
Реальный формат подписей кооперативного документа — это не «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 заголовок сокращён, чтобы не
обрывался при узкой колонке).
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-* токены.
Раньше compact-вариант рендерился без обрамления — avatar упирался
в левый край контейнера, визуально «проваливался» из общего стека
full-карточек (Screenshot_2026-05-21_21-51-09).
Compact теперь: padding 8/12px (тоньше чем full 16px), тот же border
+ surface + radius. В одну строку avatar + ФИО + AccountBadge + status,
но визуально это всё-таки карточка, не голая строка.
Body раньше был обычным block-контейнером, а .head и .value-row —
оба display: inline-flex. Без явного block-форматирования inline-flex
элементы становятся inline и рендерились в одну строку. Визуально:
Email⭐ivanov@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.
- 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 в одну строку».
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 селекторов.
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>
Старое значение --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>
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>
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>
- 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>
Воздух над заголовком стола внутри WorkspaceSwitcher — заголовок не упирается
в верхнюю границу caption «ПК «ВОСХОД»».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- .rail__top: padding 18px 20px → 16px 8px (унификация боковых отступов)
- WorkspaceSwitcher: убраны :disabled-атрибут и hover:not(:disabled) — у пайщика, которому доступен только один стол, кнопка выглядит как обычно (без not-allowed-cursor, без приглушённого цвета). Меню q-menu просто не открывается через v-if='workspaces.length > 1'. Тихое игнорирование клика вместо явного запрета.
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), даёт больше места под текст
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
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
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 селектор
- .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-токены
- 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 не должен растягиваться во всю ширину карточки
- 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()
- og-image.png — взят brand-постер ~/blago/production/shared/poster-logo-horizontal.png,
отресайзен до 1000px по ширине и центрирован на полотне 1200×630
с белыми краями по вертикали. Шрифт/композиция родные бренда.
- description / og:description / twitter:description: «Цифровой Кооператив —
система управления хозяйством. Регистрация пайщиков, заказы на поставку
и приобретение имущества, собрания совета, общие собрания пайщиков,
взаимные расчёты на блокчейне, автоматический документооборот и бухбаланс
на основе простой электронной подписи.»
- og-image.png перерисован: «Цифровой Кооператив» / «Система управления
хозяйством» / «на платформе БЛАГО — кооперативной экономики для жизни» /
«Регистрация пайщиков · заказы на поставку и приобретение имущества ·
расчёты на блокчейне».
- description / og:description / twitter:description выровнены под этот
же копирайт. Слово «взаимоотношения с пайщиками» убрано как слишком
узкое — управление хозяйством включает в себя ещё заказы, реестры
и документы.
- 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 (новый).
Pug-template передаёт выражения в Vue compiler как строки; конструкции
вида (entry as RailItem).route не превращаются в JS и падают
SyntaxError в браузере: «Unexpected identifier 'as'».
Касты унесены в <script>: добавлены computed flatItems и
normalizedGroups, шаблон работает с уже типизированными массивами без
inline-TS.
- LeftDrawerMenu: импорт useCmdkStore → useCmdkMenuStore (правильное имя).
Без этого ломался ESM build всего drawer'а: «no exported member
useCmdkStore» — RailUserCard не монтировался, поэтому и chevron свёртки
«не работал», и Найти/Пополнить не реагировали.
- AppDrawer.vue, AppHeader.vue, RailUserCard.vue переведены на pug
(lang="pug"). В проекте везде pug — никакого исторического HTML в
canon-компонентах быть не должно.
- 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 (как остальной проект)
- 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, чтобы фон страницы соответствовал шапке и рейлу
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).
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-разметку.
Переписан 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.
Корневая причина «оторванной» галочки в 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 класс.
Quasar по умолчанию даёт ~12–16px padding на .q-checkbox__label, в
compact-форме сохранения ключа это смотрелось «оторвано». Уменьшаем до 4px
через :deep override на классе .rk-form__confirm.
Три кадра в одной секции: LostKey (ввод email), ResetKeyForm check-mail
(уведомление «письмо отправлено»), ResetKeyForm save-key (демонстрация
сохранения ключа с моковым сгенерированным аккаунтом). Submit на третьем
кадре регенерирует мок-ключ для повторной проверки.
Заменяет ошибочную секцию ChangeKey из предыдущей итерации.
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.
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 целиком.
Подключение ChangeKeyForm + ChangeKeySuccessDialog к dev-витрине.
Submit запускает заглушку useChangeKey().changeKey() (≈600 мс), по успеху
открывает диалог с новым WIF. Generate генерирует валидный K1 ключ через
SDK и заполняет оба новых поля. Confirmed сбрасывает форму.
Диалог успеха смены ключа: новый 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.
Форма смены ключа пайщика на новых компонентах дизайн-системы.
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.
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>
Override (padding-top:0 + align-self:center на dense) не отрабатывал —
Quasar в dense держит prefix/suffix по baseline вводимого значения,
и пользователю это устраивает. Убираем неработающий override, чтобы
не мешался в дальнейших правках.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
Промахнулся `git add -A` — захватил worktree-symlink node_modules
(он pnpm-shared с основным репо). .gitignore покрывает node_modules/
но симлинк-файл — отдельный путь.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Контекст: после проброса --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>
- 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>
Между нижним инпутом и 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>
Регрессия мерцания границы и приплюснутости — следствие борьбы наших
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>
- 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>
- 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>
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>
Регрессия после 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>
Корень регрессии: жёсткий 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>
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>
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>
Архитектурный 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>
Истинный источник «прыгалки» 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>
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>
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>
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>
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>
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>
Благорост — капитализация РИД (не «накопления на жильё»), Генератор —
генерация РИД (не «доходы от программы»). Кошелёк остаётся «свободный
остаток». Черновые ярлыки риском попадают в production через копипасту.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Канонический .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>
Анимация 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>
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>
По фидбеку:
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>
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>
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>
Скрипт извлечения из 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>
Заносим эталонную дизайн-систему в проект — только базовые элементы для
дальнейшей компоновки. 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>
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>
В левом дровере был маленький горизонтальный скроллбар, и трекпадом его можно
было «оттянуть», обнажая правую границу — дровер выглядел скроллируемым, хотя не
должен. Причина: у .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>
Если проголосовать и сразу нажать «Утвердить», утверждение падает с ошибкой —
бэкенд ещё не учёл голос из блокчейна. После успешного голоса держим состояние
загрузки пункта (а значит и кнопку «Утвердить» в :loading) ещё VOTE_SETTLE_MS
(3с), за это время голос успевает обработаться. На ошибке загрузка снимается
сразу. Заодно ручное мутирование processingDecisions + setTimeout-хак заменены
аккуратным реактивным хелпером setProcessing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Повестка совета показывает все НЕутверждённые вопросы. Голос «за»/«против» не
должен убирать пункт — он остаётся неутверждённым, лишь помечается отметкой
голоса. Убрал ошибочное actedDecisionIds.add из onVoteFor/onVoteAgainst
(оставлен только тихий рефетч). Скрытие через actedDecisionIds оставлено только
в onAuthorizeDecision — там пункт исполняется и должен уйти из повестки без
возврата от отстающего поллинга.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Две проблемы при голосовании/утверждении в повестке совета:
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>
Две независимые проблемы при выдаче разрешения на уведомления:
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>
Кнопки взноса и возврата на столе пайщика недоступны, пока пайщик не
подписал главное соглашение цифрового кошелька (type='wallet') — как и
сама карточка кошелька, которая не появляется без подписанного соглашения.
Геттер isWalletAgreementSigned в сторе Wallet читает тот же список
соглашений пайщика, что и RequireAgreements.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
«(опционально)» в заголовок + пояснение в описании: если есть пайщики —
импортируйте CSV, если нет — двигайтесь дальше. Чтобы шаг не воспринимался
обязательным.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
readonly outlined q-input рендерится с dashed-рамкой (дефолт Quasar) и
обрезал значение (…UJR4) — выпадало из канона и выглядело плоско.
Сделал ключ surface-2 панелью: eyebrow-лейбл + copy-иконка в шапке,
mono-значение целиком (word-break) ниже. Даёт вес и убирает «ёлочку».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Прошлый вариант разъезжался: «Скопировать» висел сиротой справа, провалы
между блоками, узкая потерянная кнопка, карточка 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>
Голый «Потребительский Кооператив» в примере провоцировал вводить просто
ОПФ без расширения. Дал полный пример «...Социального Комплекса» во всех
трёх падежах — чтобы вводили расширенную ОПФ+, а не базовую форму.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Это не переменные, а постоянные параметры, которыми настраивается фабрика
документов кооператива. Переименовал label и intro шага.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Микроотступ стал лишним и снова раздвигал поля слишком широко — откатываю.
Разделение полей обеспечивает сам reserve-hint-space, дополнительный
глобальный отступ не нужен.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Постоянный hint под полем занимал место и отвлекал. Перенёс примеры
наименований ОПФ+ в placeholder (видны только в пустом поле), убрал hint.
Добавил reserve-hint-space всем q-input формы — строка под ошибку
резервируется без текста, валидация не вызывает layout shift.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Корень: на холодной перезагрузке client пересоздаётся без JWT; desktops.loadDesktop() в init-app выполнялся ДО useInitWalletProcess().run()→session.init()→globalStore.init()→client.setToken(). Гостевой getDesktop → grants=[] у market-столов; повторного refetch после авторизации не было. Легаси-столы выживали (видимость реактивна от session.isChairman), grant-столы — нет. session.init идемпотентен, повторный вызов в init-wallet = no-op.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Без зазора текст hint/error верхнего поля почти касался границы нижнего.
Вернул маленький отступ, но системно и один раз — глобально в quasar-canon:
margin-bottom: var(--p-1) только полям .q-field--with-bottom (те, что
резервируют нижнюю строку). Работает везде (BaseForm, формы установки,
регистрация), в стеках не складывается в дыру, инлайновые поля не трогает.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Канон-плотность полей (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>
- 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>
.extension-page уже внутри .catalog-shell__content с padding var(--p-6);
собственный padding давал двойной зазор от шапки до кнопки «Назад».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Весь контент страницы расширения обёрнут в канон-панель (surface+border),
больше не лежит на голом фоне
- В режиме настроек добавлена кнопка «Отменить» (CancelButton) рядом с
«Сохранить» — явный выход без сохранения; кнопка «Назад» страницы тоже работает
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Кнопка «Назад» убрана из шапки (useBackButton снят), добавлена канон-кнопка
под шапкой на самой странице
- Логотип AutoAvatar центрирован в колонке
- DesktopsList: центрированный inset-блок (выделяется), а не плоский список
- ext-actions toggle: «включено» и «удалить» снова в одной строке
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Без 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>
Триггер ринг-анимации перенесён с hover самого SVG на hover карточки
(.app-card:hover :deep(.ring-seg)).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Статус идёт сразу под заголовком (динамично) — без дыры под однострочными
названиями. Обрезку в 2 строки оставили, чтобы длинный заголовок не ломал сетку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Заголовок резервирует 2 строки (clamp+min-height), аватар по верху —
статус и описание на всех карточках начинаются на одной линии, не прыгают.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Верхний ряд: логотип + (заголовок, под ним статус); описание во всю
ширину ниже; футер «Подробнее» прижат к низу для выравнивания карточек.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Палитра колец → мягкие приглушённые тона + CSS saturate(.5)/opacity .85,
чтобы знак читался как тихая текстура, а не неоновое пятно.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- AutoAvatar параметризован: props size/radius/background/ringColor
с обратно-совместимыми дефолтами (CoopCard не затронут)
- ExtensionCard: буква-монограмма заменена на уникальный rings-логотип,
seed = имя расширения; цвет кольца детерминирован по seed из палитры
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
extensionRepository.update заменяет config JSONB целиком; chairman/powerup/meet-tracker писали захваченный при initialize снимок поверх свежих данных. Теперь read-modify-write по актуальному config из БД с наложением только владомых полей.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
Кооперативные участки, Регистрационные взносы, Ключ кооператива,
Провайдер платежей, Контакты кооператива:
- убран дублирующий заголовок страницы (есть крошка в шапке);
- описание перенесено с голого фона на канон-поверхность .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>
Заголовок страницы уже показан крошкой в шапке — повтор h2 убран.
Пояснительный текст вынесен с голого фона на канон-поверхность (.banner
с инфо-иконкой). ApprovalsPage не трогаем — там заголовка страницы нет.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
Шаблон двухкорневой (кнопка + Teleport затемнения), class из родителя
(LeftDrawerMenu) не наследовался автоматически. inheritAttrs:false +
v-bind=$attrs на корневую кнопку — атрибуты направлены явно.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Подписи секций «Вопрос на повестке»/«Проект решения» переведены в
надстрочные метки (uppercase, трекинг, ink-3) — отдельный ярус ниже
заголовка окна, чтобы не казаться конкурирующим заголовком рядом с
крупным заголовком самого документа. Текст вопроса — читаемый body.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
- OnboardingStepsCard: под заголовком «Адаптируйте кооператив…» добавлен
поясняющий текст (что происходит дальше: решения совета в электронной
форме, вступление в «Восход», импорт пайщиков, общее собрание) + плашка
срока адаптации с отсчётом.
- Чек-лист шагов переведён на канон-вертикальный степпер (.stepper--v):
пройденные — галочка/заливка primary, текущий — подсветка номера,
действия — канон BaseButton; agenda/import/meet типы сохранены.
- Диалог «Предложение повестки» (объявить собрание совета) переведён
с ModalBase на канон BaseDialog + BaseButton.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
generate-schema + generate-client + сборка SDK после добавления поля grants.
schema.gql и components/sdk/src/zeus/* теперь содержат grants — фронт штатно
выбирает поле через selectors/desktop/workspaceSelector.ts (канон авторизации
столов активирован end-to-end). dist в .gitignore.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- CommonHeader: крошка строится из route.matched (путь внутри стола),
напр. «Отчётность › Календарь»; корень стола не выводится; плоские
страницы дают одну крошку как прежде; pageTitleOverride сохранён.
- CoopWalletsPage: переводы временно скрыты под флагом transferEnabled=false
(открывались с L3-кошельков пайщиков; вернём для кооп-кошельков позже).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
getDesktop отдаёт DesktopWorkspace.grants (права текущего пользователя), фронт
сверяет meta.requires маршрута с ними (DesktopStore.isPageVisible/isWorkspaceVisible/
hasRouteAccess). Авторизацию считает только backend, фронт отражает. Провайдер
грантов расширения само-регистрируется в @Global ExtensionGrantsRegistry (нет цикла
модулей); getDesktop под OptionalGqlJwtAuthGuard. Онбординг сведён к грантам: до ЦПП
у председателя только Extension:configure → виден лишь Стол админа со страницей
подключения. market переведён на meta.requires; убраны onboarding-поля реестра,
desktop-фильтр и фронтовые beforeEnter/онбординг-ветка. Зачем: убрать дублирующую
логику видимости столов в двух местах и подготовить per-page доступ из бэкенда.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
- 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>
Дописана секция в EXTENSIONS_SCHEMA_SYSTEM.md: контракт реестра
(onboarding_route/onboarding_desktop/isOnboarded), две согласованные половины
гейтинга (backend getDesktop скрывает столы, frontend install.ts ограничивает
стол ролью и одной страницей), редирект при включении, жизненный цикл и
ограничения (нужна перезагрузка после принятия ЦПП), опциональность.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Опциональный generic-канон для расширений с подключением на уровне кооператива:
- IRegistryExtension получает onboarding_route / onboarding_desktop / isOnboarded(config);
- getDesktop, пока isOnboarded(config)===false, отдаёт ТОЛЬКО onboarding_desktop —
остальные столы скрыты из переключателя (для market: только Стол администратора);
- desktop install.ts зеркалит решение бэкенда и ограничивает админ-стол одной
страницей подключения ЦПП и ролью chairman — до решения совета никто, кроме
председателя, не видит ни столов, ни страниц;
- loadExtensionRoutes возвращает workspace-конфиги; кнопки включения/установки
редиректят на defaultRoute первого стола — для расширений с онбордингом это
страница подключения, чтобы администратор не терялся после включения.
Расширения без этих полей работают как раньше (опциональность). Источник
безопасности — backend-резолверы; гейтинг столов — UX-слой поверх них.
Для market: isOnboarded = config.coopAcceptance.accepted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Канон-инпут вместо q-input: BaseInput расширен поддержкой type="date"
и clearable (+ emit clear) — это нужно для фильтра по датам. Фильтры
в ExtensionLogsList переведены на BaseInput.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Возвращён регулируемый параметр списания, но с корректной семантикой: товар
попадает в проект списания, только пролежав N дней ПОСЛЕ истечения срока
(expiry_date <= now - grace), default 7. Свежепросроченное ещё может быть
забрано получателем — списываем лишь окончательно испорченное. Крон остаётся
ежемесячным (каждое 1-е число).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Монитор ресурсов: баннер-предупреждение на всю ширину сверху, ряд
«Как это работает» (2/3) + кошелёк AXON (1/3), ряд CPU/NET/RAM.
Убран 3D-флип карт (он же «прыгал») — детали раскрываются inline.
Все виджеты/страницы/лог сведены к канон-токенам surface/line/ink.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Семантика крона списания исправлена: в кандидаты попадают позиции с УЖЕ
истёкшим сроком годности (expiry_date <= now), а не за N дней до истечения —
списываем испорченное, а не заранее. Параметр expiry_grace_days удалён как
вводящий в заблуждение. auto_proposal_enabled теперь true по умолчанию, чтобы
проект списания формировался без ручной активации. Крон остаётся ежемесячным
(@Cron EVERY_1ST_DAY_OF_MONTH ≈ раз в 30 дней) — устойчив к рестартам, в
отличие от setInterval (лимит ~24.8 сут не покрывает 30-дневный интервал).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Реестр платежей: кнопки действий больше не 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>
- 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>
Родительскому объекту writeoff заданы label «Списание скоропорта» и note
через describeField; рендерер формы (ZodForm) показывает getLabel/getHint и
для объектов-групп, поэтому без описания отображалось техническое «writeoff».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Из формы установки расширения market убраны неиспользуемые в коде поля
enabled/lastSyncTimestamp/debug; coopAcceptance помечен visible:false как
системное состояние совета; настройки writeoff получили русские label/note
через describeField — по канону расширения capital. Чтобы при установке
приложения председатель видел минимальную понятную форму на русском, без
отладочных и внутренних параметров.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Таблицы документов и платежей уже на канон-токенах и самообрамлены
(.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>
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>
Страница получила канон-отступ (--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>
Карточки CatalogOfferCard на модерации и в «Мои предложения» нельзя было
открыть — описание обрезано, действий нет. Добавлен widget OfferDetailsDialog
(+ секция витрины дизайн-системы): клик по карточке открывает модалку с полным
описанием, метой (категория/тип отсечки/гарантия/поставщик), причиной отклонения
и slot actions под ролевые кнопки.
- Модерация (председатель): открыть → прочитать → Одобрить / Отклонить
(добавлена мутация rejectOffer с обязательной причиной — раньше в UI её не было).
- Мои предложения (поставщик): открыть → Редактировать / Снять / Запустить поставку.
- Правка: скрытый роут marketplace-edit-offer переиспользует форму создания в
режиме редактирования (префилл из listMyOffers, сабмит → marketplaceUpdateOffer).
Бэкенд сбрасывает статус в PENDING_MODERATION — правка уходит на повторную
модерацию в рамках того же предложения (баннер предупреждает об этом).
Проверено на живом стенде: диалоги открываются на обоих столах, Редактировать
ведёт на предзаполненную форму. ESLint чист, vue-tsc без ошибок в затронутых файлах.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Развёрнутая строка пайщика теперь показывает только его данные;
документы пайщика живут в отдельном «Реестре документов» и здесь не дублируются.
ParticipantDetails упрощён (без q-tabs/ListOfDocumentsWidget), tab-логика
вычищена из таблицы и страницы. Мобильная ParticipantCard переведена на канон-токены
(убраны хардкод-цвета, CardStyles и q-badge статуса → disabled-checkbox).
Таблица сохранена, сортировка по дате вступления (DESC) без изменений.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Активный стол (activeWorkspaceName) ставился только кликом по плитке,
редиректом с index и из localStorage. При прямом заходе по URL или переходе
кнопкой со стола А на маршрут стола Б активным оставался прежний стол — в
шапке и левом меню «висел» не тот стол (видно было на playwright-кадрах как
постоянный «Председатель»).
- DesktopStore.syncActiveWorkspaceFromRoute(matchedNames): находит стол по
имени родительского маршрута (mainRoute.name) в цепочке to.matched и делает
его активным без навигации и без спиннера.
- navigation-guard: router.afterEach зовёт синк после каждого перехода.
- init-app: однократный досинк после установки расширений (deep-link при
холодной загрузке, когда afterEach сработал до прикрепления маршрутов).
Проверено на стенде: прямой заход на /market-supplier|market-admin|market/*
переключает активный стол и левое меню на нужный.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Расширение market давало один смешанный стол из 21 страницы (заказчик +
поставщик + админ вперемешку) и отдельный Стол ПВЗ, причём market-pvz
вообще не объявлялся в backend AppRegistry → его маршруты молча терялись.
- backend extensions.registry.ts: AppRegistry['market'].desktops теперь
объявляет 4 стола (market/market-supplier/market-pvz/market-admin) —
фронт прикрепляет маршруты только к объявленным backend столам.
- desktop install.ts: возвращает 4 workspace, 29 страниц разнесены по
ролям; Стол администратора виден только chairman+member (meta.roles),
остальные — всем; имена маршрутов сохранены (навигация по имени).
- init-installed-extensions: убран немой catch{} (теперь console.error)
+ warn, когда install вернул workspace, отсутствующий на backend, —
раньше сломанный/несинхронный стол выглядел как «пустой» без диагностики.
Проверено на стенде: getDesktop = 4 стола, Pinia-стор = children 6/6/8/9,
скриншот навигатора подтверждает рендер страниц по столам.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Убран EntityIdBadge. Зелёная плашка-аватар слева теперь несёт сам номер вопроса
(он же Decision ID): клик по плашке копирует id (тултип «Скопировать №»), hover —
инверсия в сплошной teal. Заголовок снова просто текст вопроса.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EntityIdBadge перенесён из отдельной строки под ФИО в начало строки заголовка:
[#id] → сразу текст вопроса. Inline, vertical-align middle. Раньше висел снизу
и пусто растягивал верхнюю строку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Boot-файл ждал window.chatwootSDK через polling БЕЗ таймаута (await
waitForChatWootReady), поэтому недоступный support.coopenomics.world вешал
всю инициализацию Quasar → бесконечный спиннер. Вынес доинициализацию чата
в отделённую (неожидаемую) задачу + жёсткий 15s таймаут: boot завершается
сразу после app.use(chatwoot), недоступный support чат лишь отключает, но
загрузку приложения не блокирует.
EntityIdBadge с id вопроса под заголовком/ФИО: отображает #<id>, по клику копирует
чистый id (copy-on-click). Канон-компонент, клик не раскрывает карточку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
«Истекает через месяц» перенесён из строки под заголовком в постоянную нижнюю
полоску (footer): срок слева, «Утвердить» справа (только председателю). У обычного
пайщика — только срок, узкая панелька.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Срок теперь подписан «Истекает через месяц» (было просто «через месяц» — неясно).
Удалён статус-чип «Вы за/против» — направление голоса уже видно по подсветке кнопок,
бейдж был избыточным. Убраны связанные computeds и стили.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Кнопки голосования прижаты к правому краю строки. «Утвердить» вынесена вниз
отдельной строкой-footer (справа, hairline сверху) — больше не теснит данные.
Срок «через месяц» и статус-чип «Вы за» перенесены влево под заголовок/ФИО.
Клик по строке раскрывает документ, по кнопкам голосования и footer — нет.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Карточка вопроса перестроена в единую горизонтальную строку: иконка + вопрос/ФИО
слева (flex), компактный блок органов управления (голосование + «Утвердить»),
срок + статус-чип и шеврон — справа, друг рядом с другом. Убрана центрированная
vote-зона с пустотой по бокам. Клик по строке раскрывает документ, по органам
управления (@click.stop) — нет. Подсказка «Утвердить» возвращена в tooltip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Шапка карточки (вопрос/ФИО/срок/статус) кликабельна целиком и раскрывает документ,
справа шеврон-индикатор. Органы управления голосованием — отдельная зона ниже шапки
(сиблинг, не аккордеон): клик по ним голосует/утверждает и документ не раскрывает.
Убрана отдельная кнопка «Документ» — раскрытие теперь по клику на шапку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
QuestionCard: VotingButtons + «Утвердить» вынесены на верхний уровень карточки —
голосовать можно без раскрытия; раскрытие («Документ») открывает только содержимое
документа. Истечение срока остаётся в шапке.
VotingButtons: вернул чек-индикатор «принято советом» (закрашивается при принятии)
вместо иконки verified.
Header-CTA совета (Предложить/Добавить/Импорт): убран push (q-btn--push не попадал
под канон-правило заливки primary → белый шрифт на нетиловом фоне), подписи с заглавной.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Колонка chairman_signed_at в QTable не отображала значение (нет format-функции,
сырой ISO не рендерился) — добавлен formatDate с ru-локалью (как в board/payouts),
теперь видна дата открытия выдачи. Обновлён скриншот документации.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Правки по голосовому ревью документации Marketplace:
- orderer-стол «Готово к получению»: заказ доведён по магистрали до
READY_TO_RECEIVE (без финальной подписи), сценарий перестроен ОТ этой
страницы (а не от вкладки «Получены»); проза уточнена — финальная подпись
получения оформляется на столе оператора ПВЗ по штрих-коду.
- board/payouts: лента выплат показывает реальные данные (1 PENDING + 2
COMPLETED) — данные засеяны магистралью; страница уже авто-грузится onMounted.
- удалены legacy-доки orderer/receive.md и branch-chairman/apl-reception-close.md
+ их nav-записи; висячие reference-ссылки перенаправлены на issuance-finalize /
apl-reception-create / ready-to-receive (mkdocs --strict: по marketplace чисто).
- YANDEX_MAPS_API_KEY проброшен через EnvVars + generateConfig + fallback envCache
(ключ только в gitignore'нутом .env; в config.default.js — пустой placeholder);
карта Яндекса на странице ПВЗ рендерится, баннер «не задан ключ» убран; проза
branches.md обновлена (карта есть всегда, точка — после геокодинга адреса).
- harness: устойчивая навигация ready-to-receive/pvz-list (ретрай + полный
перезагруз против гонки core-guard/fetchRoles), ожидание DOM-рендера карты,
dismiss онбординга на pvz-list.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Названия столов в реестре workspace'ов заведены вразнобой («Стол
благороста», «Стол вычислительных ресурсов» vs «Стол Совета»).
Добавил text-transform: capitalize на подписи в WorkspaceSwitcher
(заголовок текущего стола + пункты выпадающего меню) и WorkspaceMenu
(карусель + диалог выбора) — первая буква каждого слова заглавная,
одинаково для всех и для будущих столов, без правки строк-источников.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
По фидбеку переработана композиция:
- убраны заголовки секций «Контакты»/«Руководство» и per-строчные
иконки телефона/почты/адреса (из-за них значения «прыгали» вправо
относительно ИНН/ОГРН);
- ИНН и ОГРН теперь в одну строку (адаптивная сетка полей);
- председатель поднят наверх к реквизитам, поле названо просто
«Председатель совета»;
- контакты — те же поля, телефон/email как ссылки (tel/mailto), всё на
единой левой кромке; одна тонкая линия делит реквизиты и контакты.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Реквизиты/Руководство больше не разносят подпись и значение к
противоположным краям (на полной ширине это давало пустой провал и
оторванные значения). Все секции теперь как ContactSheet: подпись
сверху, значение под ней, слева, hairline между строками — один ритм
по всей карточке.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
- 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>
- 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>
Добавлен 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>
- 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>
Переделка после ревью: 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>
Когда страница не телепортирует действия, .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>
- 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>
Симлинк 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>
При удалении 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>
Дефолтная ветка C9S/coopenomics — master; workflow_dispatch надо слать на
ref=master, иначе Gitea вернёт 404 по ветке.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
После переезда на 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>
Крошка: в #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>
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>
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>
generate-client пишет копию zeus и в components/controller/zeus — обновляю вместе
со schema.gql, чтобы не оставлять дрифт сгенерированных артефактов.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Конструктор MarketplaceOrderSupplierActionService оброс cycleRepo и
shipmentCreate (synthesizeIndividualShipment), тест остался на 4 аргументах
со времён Эпика 4. Добавлены моки cycleRepo.create/shipmentCreate.execute +
orderRepo.assignToCycle, инстанс собирается всеми 6 аргументами. tsc 0 ошибок,
jest 19/19.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- boot: seedMarketplaceKuDetails (idempotent ON CONFLICT) + CLI seed:marketplace-ku
+ шаг в extra_reboot.sh после coopback с ожиданием создания таблицы;
на свежем стенде krg/odn/myt (ACTIVE) появляются автоматически.
- controller: MutationLoggingInterceptor пишет username='anonymous' вместо
undefined для мутаций без JWT (login/register) — INSERT в mutation_logs
больше не падает на NOT NULL, лог анонимной мутации сохраняется для аудита.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
Задача 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>
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>
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>
The selector was declared twice with identical bodies in orderSelector.ts,
so `pnpm --filter @coopenomics/sdk build` failed with TS2451 (cannot
redeclare block-scoped variable). Latent because the SDK was not built
locally. Drop the second declaration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Шаг компиляции контрактов в 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>
Поток III (возврат и списание): wrap signed return docs in
SignedDigitalDocumentInputDTO before toDocument() (flat-doc fix), rehydrate
jsonb-stored ISO dates to Date for the DateTime scalar in the return-claim
mapper. Frontend: orderer/operator return-claim pages + decision dialogs,
writeoff draft editor, branch-orders, issuance finalize, barcode scanner,
process info widgets. Update return-approve/return-claim/writeoff-propose
prose and screenshots.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MarketplaceOnboardingService constructor grew to 4 args (soviet/wallet
blockchain ports) — the onboarding-service and l2-onboarding-flow specs
still called it with 2 and failed to compile. Pass stub ports (getOnboardingState
touches neither). plugin-initialize asserted the old warn-log about an
unmerged PR; production now logs info "File storage отключён конфигурацией".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Operators and КУ-chairmen could read inventory, issuances and acceptance
acts (АПП) of other branches: list resolvers returned coop-wide data and
signable-payload resolvers fetched documents by client-supplied id without
membership checks. Add own-KU scoping (isMemberOfBranch / orderer ownership)
across marketplaceListInventory, marketplaceListIssuancesByBraname,
issuance + apl-reception signable payloads, and tighten canAccess so that
an :all qualifier subsumes :own/:own-KU rather than requiring literal match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ledger2: восстановлены 11 операций o.mkt.* + wallet::CONVERT_TO_MEMBER +
кошелёк w.mkt.member, утерянные кривым merge (operations.hpp/wallets.hpp
откатились на legacy-сторону, тогда как accounts/processes остались на
Эпике 11). CONSUME_BY_MEMBER → существующий WalletOp::BURN_BLOCKED (без
дубля enum-слота). marketplace.wasm снова собирается.
- propwroff: action расширен (document2 statement, std::string meta);
контракт сам ставит повестку inline-вызовом soviet::createagenda от
permission_level{marketplace, active} (marketplace в contracts_whitelist).
- backend: propWroff несёт statement+meta (через SignedDigitalDocumentInputDTO
.toDocument()); отдельный createWriteoffAgenda удалён из порта/адаптера/
сервиса — кооператив не в whitelist, второй вызов создавал бы дубль повестки.
cooptypes IPropWroff +statement+meta. spec 31/31.
- desktop: суммы списания форматируются через formatAsset2Digits на столе
председателя, в ленте совета (фикс поля total_loss_amount→total_amount,
показывалось «—»), в деталях проекта и виджете процесса.
- harness: writeoff-propose снова доводит проект до повестки совета
(ENABLE_COUNCIL_SUBMIT=true после фикса моста).
Все три 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>
- 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>
Штатный синк вместо точечной правки: 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>
Кошельки (по фидбэку — переносы выглядят плохо):
- .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>
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>
Упраздняем субсчёт "заблокировано" на кошельках. Резерв средств под заявку
на возврат паевого теперь выражается переводом на отдельный 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>
Пересняты на зелёной сборке с фиксом доступа председателя КУ: закрывающая
подпись приёмки, первая подпись поставщика, создание АПП — таблицы и диалоги
с русскими подписями статусов/вариантов. Проза приведена к подписям UI,
убран on-chain-жаргон (signchair/композитная транзакция). Обновлены
скриншоты выплат и выдачи.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Убраны дубли роутов reception/labeling/branch-orders в extensions/market/install.ts:
сторонние entry с meta.roles:['chairman'] (core-роль главного председателя)
перекрывали canonical с requireMarketplaceOperator и отбивали председателей КУ
«Недостаточно прав» — регрессия открытия /market-pvz/* для KU-chairman.
Локализованы сырые enum (статус/вариант) в подписях столов: диалоги подписи
приёмки (председатель/поставщик), история выплат, подготовка поставки, выдача,
сводный стол КУ. Приёмки, ждущие подписи, всплывают наверх списка.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Заказчик (мои заказы/каталог/сводный заказ/готово к получению), поставщик
(мои предложения/история выплат), оператор ПВЗ (склад/возвраты), председатель
(доступные категории/сводный склад), совет (повестка списаний/выплаты),
председатель КУ (сводный стол/список ПВЗ), подключение пайщика к программе.
Кадры на реальных данных стенда (заказы ekaterina, выплаты ivanpetrov,
наклейки КУ krg, партии), проза бизнес-языком без dev-жаргона.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
orders: список заказчика теперь непустой (фикс подписи + фильтр-таб «Получены»).
consolidated/payments: переключены на корректную перспективу (ekaterina-заказчик,
ivanpetrov-поставщик) — показывают реальные партии и платёжные запросы магистрали II.
warehouse: логин под chairkrg (read:own-KU) + подписание онбординга — таблица наклеек КУ krg.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
OffererPendingAplReceptionsPage: при сведении worktree→feat обе стороны конфликта
сохранили идентичный блок `const columns` — vue/compiler-sfc падал на «Identifier
already declared», ломая `quasar build` (но не controller tsc и не dev-Vite).
OnboardingMemberPickCpp: убран dev-жаргон из user-facing текста gate
(«Эпик 1 / L3 онбординг пайщика» → «Подключение пайщика к программе»;
«Шаблон документа № N в реестре кооператива» → бизнес-описание; «Marketplace» → «Стол заказов»).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Корневая причина: при pull конфликт объявлялся по условию
`dirty && remoteUpdatedAt !== prev.remote_updated_at`, где `dirty`
определяется сравнением raw-байтового sha файла с content_etag_local.
Серверные метки времени (updated_at/created_at) входят и во frontmatter
файла, и в etag. Когда сервер бьёт _updated_at родителя при дочерней
мутации (создание/удаление issue/story), etag в индексе расходится с
файлом ровно на строку updated_at — файл считается «грязным», а при
изменившемся remote_updated_at pull пишет маркеры слияния на весь файл,
хотя содержательно текст идентичен. Подтверждено: подстановка
remote_updated_at в файл воспроизводит etag байт-в-байт (58 записей на
проде voskhod).
Фикс: в syncEntityFile перед записью маркеров сравниваем локальный и
серверный тексты в каноне без updated_at/created_at. Если совпадают —
это не конфликт: принимаем серверную версию и лечим etag, без маркеров.
Реальная правка тела/заголовка по-прежнему даёт маркеры.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Trial run #148 (PR #30) упал в job controller на 37 TS2307 «Cannot find
module» для @coopenomics/{sdk,inter,notifications}. Локально маскируется
тем, что controller стартует через ts-node, который резолвит .ts напрямую
через tsconfig-paths/pnpm-симлинки и не зависит от dist/. tsc же ищет
types из package.json целевого пакета — а оно показывает на dist/index.d.ts
из unbuild.
Замена локального scoped-build на полный `pnpm lerna run build` (как в
корневом Dockerfile builder-стадии) собирает граф целиком и устойчиво
к добавлению новых workspace-пакетов. Добавляет ~2-3 мин ко времени job'а,
end-to-end оценочно ~21 мин (был 18 на сломанной версии).
Закрывает блокер magistral II документации (см. order-create.md / PLAN.md §9.13):
on-chain marketplace::createorder валил assertion «у пайщика не подписано
соглашение program_id=2 для w.mkt.member», потому что в фабрике документов
отсутствовали adapter'ы для шаблона/инстанса оферты ЦПП «Стол заказов», а
mutation подписи прямо со стола заказов была пропущена в Эпике 1 (помечена
фоллоуапом в onboarding/README.md и в blago issue 598-4).
Что сделано:
cooptypes:
- IVars.marketplace_program / marketplace_offer_template (optional). Юр.текст
оферты остаётся «рыбой» — финальную редакцию готовит юр.отдел.
- UdataKey.MARKETPLACE_AGREEMENT_NUMBER / MARKETPLACE_AGREEMENT_CREATED_AT.
- 1101.MarketplaceOffer.Action расширён двумя обязательными полями: номер
соглашения и дата (передаются явно из L3 service, без Udata-таблицы).
factory (Эпик 1 / Story 1.7 финализация):
- Actions/1100.MarketplaceOfferTemplate.ts + Templates/1100.
MarketplaceOfferTemplate.ts (шаблон — только coop+vars, аналог
998.BlagorostProgramTemplate).
- Actions/1101.MarketplaceOffer.ts + Templates/1101.MarketplaceOffer.ts
(инстанс на конкретного пайщика — с common_user и номером соглашения).
- VarsSchema получил marketplace_program и marketplace_offer_template.
- index.ts и registry.ts factory регистрируют новые registry_id.
controller (Эпик 1 / Story 1.4 фоллоуап):
- DTO MarketplaceSignOnboardingOfferInputDTO — только signed document;
coopname/username берутся из JWT context'а guard'а (защита от подмены).
- MarketplaceOnboardingService.signOnboardingOffer — лукап
soviet::coagreements(coopname, type='marketplace'), вызов
walletBlockchainPort.signProgramAgreement (program_id из coagreement,
draft_id оттуда же, document — подписан фронтом).
- Mutation marketplaceSignOnboardingOffer в marketplace-onboarding.resolver.
ts с возвратом свежего MarketplaceOnboardingState (UI не нужен второй
запрос).
SDK:
- Mutations.Marketplace.MarketplaceSignOnboardingOffer wrapper (graphql-zeus
regen для нового input MarketplaceSignOnboardingOfferInput).
desktop:
- OnboardingMemberPickCpp api: signOnboardingOffer() — генерация документа
через DigitalDocument(registry_id=1101), подпись локальным WIF, вызов
новой mutation.
- OnboardingMemberPickCppPage.vue: onAccept теперь подписывает прямо со
стола (раньше редиректил в Registrator). По успеху — Notify «Оферта
подписана» + redirect /market/catalog.
- OnboardingCPPGate widget: новый пропс busy для loading-state кнопки
«Подписать оферту».
PLAN.md §9.14 — закрывающая запись по выбранному варианту A.
NB: schema.gql и SDK zeus/index.ts регенерированы автоматически — попадают
в коммит как часть SDK сборки. Текст оферты в cooptypes 1100/1101 остаётся
«рыбой» до утверждения юр.отделом; registry_id и подписи менять не
потребуется.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Накопленная работа по реализации e2e сценариев Стола заказов:
backend (MarketplaceKuChairmenService + spec, KU-chairman guard,
notification broadcast), frontend (market-session-store FSD-extension,
beforeEnter роутер-гарды, 11 новых Vue-страниц marketplace), SDK
wrappers (whoAmI, listPendingOffers/myOffers/supplierOrders, moderation
log, CPP queries), harness-сценарии и MD-черновики для magistral II
шагов 1-4.
Перед merge dev (101 коммит с perf-фиксами controller/desktop/mongo)
для дальнейшего прогона magistral II шага 5+.
build образов в release.yaml не ловит TS-ошибки: quasar build идёт через
esbuild с выключенным vueTsc в vite-plugin-checker (см. quasar.config.cjs),
а у controller'а build-скрипта вообще нет — `lerna run build` его молча
пропускает, в проде ts-node стартует и валится на типах только в рантайме.
Поэтому битый тэг мог уехать в DockerHub (кейс PR #392 / rename 1080→1020
в cooptypes — TS2551 в distribution-management проскочил именно так).
Новый reusable workflow .github/workflows/typecheck.yaml:
- desktop: build cooptypes/factory/sdk → quasar prepare → vue-tsc --noEmit --skipLibCheck
- controller: build cooptypes/factory → pnpm typecheck (tsc --noEmit)
- триггер pull_request на dev + workflow_call
В release.yaml добавлен job typecheck (uses: ./.github/workflows/typecheck.yaml),
release-job получил needs: typecheck. Downstream publish-packages /
publish-docs / trigger-coopenomics-docs через release автоматически в цепочке.
Push в dev/testnet/main НЕ триггерит — PR-гейта достаточно, тэги покрыты
через workflow_call. Если vue-tsc упрётся в OOM/таймаут на ubuntu-latest,
fallback на `pnpm --filter @coopenomics/desktop run typecheck` (без SFC).
PR #27 включил transpileOnly через "ts-node" блок в tsconfig.json
для ускорения cold-start dev. Это сломало TypeORM на старте:
DataTypeNotSupportedError: Data type "Object" in "TokenEntity.type"
is not supported by "postgres" database.
Корень: transpileOnly режим ts-node использует ts.transpileModule,
один файл за раз без TypeChecker. Cross-file type aliases — типа
`import type { TokenType } from '~/types/token.types'` где
`TokenType = (typeof tokenTypes)[keyof typeof tokenTypes]` — без
type-checker'а **не резолвятся**. design:type metadata
для `@Column() type!: TokenType` записывается как `Object` вместо
`String`. TypeORM пытается создать колонку Object → unsupported.
Проблема структурная: множество TypeORM Entity в controller'е
используют `import type {SomeAlias}` + `@Column() field: SomeAlias`,
полагаясь на полный type-resolve в metadata-emit. Без явного `type:`
в каждом @Column переход на transpileOnly / SWC невозможен.
Возвращаемся к полному ts-node (cold-start 60+ сек, как было до
PR #27). @swc/core / @swc-node/register остаются в devDeps —
безвредны, не используются. Smoke-suite tests/unit/_swc-readiness/
остаётся как regression-net для будущих попыток (тестирует
emitDecoratorMetadata инвариант).
Будущий путь к ускорению (отдельная задача):
1. Пройтись по всем TypeORM Entity, добавить explicit type: в
каждый @Column — снимет зависимость от cross-file metadata.
2. ИЛИ перевести TokenType-подобные type-aliases в enum (value-
import) — runtime binding позволит metadata эмититься корректно.
3. Тогда transpileOnly / SWC заработают без regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Локально гоняем mongo как standalone. Раньше был --replSet rs0 +
rs.initiate в entrypoint, но проект не использует ни transactions
(нет startSession), ни change streams (нет .watch()), ни
readConcern:'majority' — replica-set серверно избыточен. Клиенты
(mongoose в controller, MongoClient в parser, notifications) уже
подключаются по URL без replicaSet=/directConnection= параметров,
для них переход прозрачен.
Бонусы:
- pnpm run reboot больше не висит на «MongoDB еще не готов
(нет PRIMARY)»: на arm64 sleep 5 в entrypoint не успевал поднять
mongod до того, как rs.initiate пытался выполниться, и rs.status()
внутри try/catch ловил неправильную ошибку — replica config не
применялся, oplog.rs не создавался, PRIMARY никогда не наступал
(инцидент 2026-05-23).
- Старт mongo на 5-10 сек быстрее.
- Меньше состояния в volume — нет oplog/replica-config.
reboot.sh: ждать db.adminCommand({ping:1}) вместо db.hello().isWritablePrimary.
Прод-конфигурация (k8s/swarm) этого файла не использует — там
своя replica-схема для HA, не затронута.
Если в будущем потребуются transactions — вернуть --replSet rs0
+ rs.initiate в entrypoint обратно. Триггер: появление в коде
session = await mongoose.startSession() или .watch().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
Слияние в release.yaml через jobs+needs (последовательно):
- release (контракты+контейнеры+webhook, как раньше)
- publish-packages (если не -alpha) — был publish-packages.yaml
- publish-docs (не -alpha + main) — был publish-docs.yaml
- trigger-coopenomics-docs (не -alpha + main) — был build-contracts-docs.yaml
Удалено:
- build-contracts.yaml (ручной workflow_dispatch, сознательно вынесен из
релиз-пути после инцидента 2026-05-13, пользователем подтверждено удаление)
- publish-packages.yaml, publish-docs.yaml, build-contracts-docs.yaml
(содержимое перенесено в release.yaml как зависимые jobs)
Гейты унифицированы на !contains(github.ref, '-alpha') во всех publish-*
и trigger-* (раньше publish-docs/build-contracts-docs резали ещё
-beta/-rc/-test). IS_PROD в release-job остаётся на (alpha|beta|rc|test)
намеренно — webhook продакшна и тэг :latest должны быть строже.
Telegram-уведомления удалены из release.yaml и build-bootstrap.yaml.
Полагаемся на дефолтные email-уведомления Gitea-инстанса (mailer ENABLED=true).
Секреты TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID после merge можно удалить из репо.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Полный SWC через ts-node наткнулся на TDZ-ловушку при загрузке
src/extensions/1ccoop/oneccoop-extension.module.ts:
ReferenceError: Cannot access 'OneCoopPlugin' before initialization
at Object.get OneCoopPlugin
at oneccoop-secret-key.guard.ts:83 (line in transpiled SWC output)
Корень: circular import между oneccoop-extension.module.ts и
guard.ts (guard импортирует OneCoopPlugin для @Inject(forwardRef(...))
+ type annotation; module импортирует guard для providers). SWC
эмитит `Reflect.metadata("design:paramtypes", [OneCoopPlugin])`
при class-declaration `@Injectable()` — это runtime-обращение к
OneCoopPlugin до того, как oneccoop-extension.module.ts закончил
инициализацию. tsc/CommonJS-loader прощает (hoisted exports +
Object.defineProperty(get) для late-binding), SWC по строгой
ES-семантике — нет.
Масштаб системный, не локальный: 32 файла в src/ используют
forwardRef(() => Class), много из них в pattern @Inject + type
annotation в конструкторе. Под SWC каждый такой файл —
потенциальный TDZ. Чинить по одному (import type + lazy require
в forwardRef) — десятки правок с риском уронить тип-safety.
Решение: остаёмся на ts-node --transpileOnly. Тот же 25x cold-start
(~1 сек на пробе), tsc-семантика прощает циклы, никаких code-changes.
@swc/core и @swc-node/register оставляем в devDependencies —
готовы к будущему полному SWC, когда отдельной задачей разорвём
forwardRef-циклы (заменить class-on-class @Inject через string/Symbol
токены, либо вынести типы в отдельные файлы без cycle).
SWC-options через .swcrc как module.lazy:true проверены — не лечат,
проблема в decoratorMetadata, не в импортах.
Smoke-suite tests/unit/_swc-readiness/ — 4/4 PASS на transpileOnly,
emitDecoratorMetadata через barrel-import сохраняется.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
Финал миграции: вместо ts-node --transpileOnly теперь полный SWC
(Rust-транспайлер) через ts-node "swc" mode. Установлены
@swc-node/register@^1.11.1 и @swc/core@^1.15.33 в controller.
tsconfig "ts-node" → { "swc": true, "files": true } (вместо
transpileOnly). Все 14 ts-node-вызовов очищены от --transpileOnly
(избыточно при swc).
Замер:
- ts-node + tsc (baseline): 26+ сек cold-start на простом скрипте
- ts-node --transpileOnly (Node 20): ~1.0 сек
- ts-node + swc (Node 22): ~1.0 сек (та же скорость, но
без потери типов — SWC всё ещё транспайлит, просто на Rust)
На полном controller-проекте dev-cold-start раньше был 60+ сек,
теперь — secunda-уровень (точно замерить можно только перезапустив
coopback в контейнере).
Node 22:
- nvm alias default 22 (Node 22.22.3 — требование Quasar 2.5.2,
заодно убрал quasar-prepare warning при pnpm install).
- libxmljs2 native binary теперь совместим с runtime (на Node 20
падал tests/unit/reports на NODE_MODULE_VERSION mismatch).
.npmrc в корне (`store-dir=./.pnpm-store`) — фиксирует store в
монорепе. Корень проблемы был: docker-coopback запущен от root и
писал в `/app/.pnpm-store` (= host's /home/ant/mono/.pnpm-store),
host-pnpm дефолтно искал в ~/.local/share/pnpm/store — отсюда
ERR_PNPM_UNEXPECTED_STORE. Relative-path в .npmrc устраняет
mismatch навсегда: и хост, и контейнер видят store через тот же
relative-путь от repo root.
Smoke-suite tests/unit/_swc-readiness/ — 4/4 PASS на SWC, что
формально подтверждает: emitDecoratorMetadata через barrel-import
сохраняется (Nest DI, class-validator, @ValidateNested + @Type все
получают правильные design:type/paramtypes из SWC).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
cooptypes/test/wallets-registry.snapshot — `human_name` для
w.wal.wthdrw в src/ledger2/wallets.generated.ts получил суффикс
"(deprecated, не используется в новых операциях)" (видимо, при
свежей регенерации из C++), но snapshot не обновили. Updating.
controller/tests/unit/process-registry — apply-anchor data поле
переименовано action_code → operation_code в самом сервисе
(src/domain/process-registry/services/process-registry.service.ts),
вместе с константой ACTION_CODE_TO_PROCESS_TYPE → OPERATION_CODE_TO_PROCESS_TYPE.
В тесте оставались старые имена → 1 из 6 кейсов падал на mismatch
regex'а сообщения ошибки. Переименовываю action_code → operation_code
во всём файле и обновляю regex (f2).
Что НЕ починено в этом коммите (pre-existing, не моё):
- 5 case'ов (a..e) в том же файле всё ещё падают: они используют
operation_codes 'sov.axncnv' / 'cap.act2shr' / 'cap.act2ln' /
'wall.depcpl' / 'reg.entrfee' / 'reg.minshare' / 'mig.opncash',
которых нет в Ledger2.LEDGER2_OPERATION_REGISTRY (cooptypes).
Test fixtures устарели относительно текущей канонической
ledger2-онтологии (canonical имена — o.cap.crtnma, o.cap.dbtwrf,
o.cap.lend, o.cap.repay, o.reg.payent, o.reg.putmin и т.п.).
Этот тест-suite надо переписать под актуальный operation registry
отдельным PR (требует знания canonical naming kanon).
Падает и на dev (без моих правок) — это pre-existing tech debt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cold-start controller dev (`pnpm dev`) сейчас 60+ секунд из-за полной
TypeScript-компиляции в ts-node на каждый рестарт nodemon. Решение —
добавить --transpileOnly во все 14 ts-node-скриптов и зафиксировать
это в tsconfig "ts-node" блоке. Транспайл-онли пропускает type-check,
оставляет только TS→JS transform (с сохранением emitDecoratorMetadata).
Замер на standalone-пробе с barrel-импортом + Reflect.getMetadata
(tests/unit/_swc-readiness/transpile-only-probe.ts):
- baseline (полный ts-node): 26+ сек (показал тест ts-jest)
- ts-node --transpileOnly: ~1 сек
Ускорение ~25x на cold-start. dev / start / migration:* / init:* /
analyze:modules / generate-schema — все на --transpileOnly.
Типы продолжаем проверять отдельно: `pnpm typecheck` (tsc --noEmit
по этому же tsconfig.json) — это и так стоит делать перед коммитом
по правилам проекта.
Safety-net — smoke-suite tests/unit/_swc-readiness/swc-readiness.test.ts:
ловит главный риск SWC/transpile-only регрессий — потерю metadata на
barrel-импортах (поведение тех же кодпутей, что и в реальных Nest-
сервисах с injection через @Inject и class-validator DTO).
- ServiceB DI через barrel: design:paramtypes = [ServiceA] (не Object)
- @ValidateNested + @Type через barrel: nested design:type = NestedPayload
- Nest Test.createTestingModule резолвит сервисы через barrel-import
- class-validator/class-transformer не теряют type info
Если позже понадобится **полный SWC** (Rust-транспайлер, ещё в разы
быстрее) — добавить @swc/core + @swc-node/register в devDependencies
и переключить ts-node на swc-режим:
1. `pnpm add -D @swc/core @swc-node/register -F @coopenomics/controller`
2. В tsconfig.json "ts-node" → добавить `"swc": true` либо
использовать `node --import @swc-node/register/esm-register` в скриптах.
Этот шаг не делается сейчас, так как install требует sudo на shared
pnpm-store (root-owned /home/ant/mono/.pnpm-store/v10) — пользователь
утром может сделать руками.
tsc --noEmit зелёный за 32 сек, smoke-suite + 1 case onboarding-ttl + 1 case
onboarding-steps-registry — все зелёные на baseline и после изменения
tsconfig.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
В dev-режиме vue-tsc через vite-plugin-checker удерживал постоянную
100% загрузку CPU и 2–4 GB RAM на больших Vue 3 + Quasar проектах
(Milkdown / BPMN-js / VueFlow / Mermaid / OpenLayers). Каждое
сохранение запускало полный re-typecheck в фоне, что в долгих
сессиях выглядело как утечка памяти и вешало машину.
Типы продолжаем гонять отдельно: `pnpm typecheck` (tsc --noEmit
--skipLibCheck) и через Volar в IDE. eslint в checker'е остаётся —
он лёгкий и полезен для overlay'я.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Существующая модель 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.
Реализованы три 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 отложен из-за конфликта имён — существующий компонент
имеет другую роль; нужно согласовать канон-нейминг.
Заменил `.full-width.text-center` + flex без выравнивания на flex-column
со `min-height: 360px` — лоадер больше не прижат к левому верху карточки.
Заодно поправил опечатку «подговка» → «Формируем документ».
Корень: я положил символ валюты в #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>
Корень: 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>
OffererIncomingOrdersPage был read-only, кнопки «Принять/Отказать» в OrderCard'ах эмитили @action событие, которое родительский page не слушал. Подключил обработчик:
- SDK обёртки Mutations.Marketplace.AcceptIndividualOrder и DeclineIndividualOrder + selector MarketplaceSupplierOrderActionResult.
- api/index.ts: acceptIndividualOrder(order_id) и declineIndividualOrder(order_id, reason).
- Vue: onCardAction → onAccept или onDecline; decline открывает Quasar Dialog.prompt для reason (canon UX: причина показывается пайщику-заказчику в его заказе).
- harness: incoming-orders.mjs шаги 03-individual-pending и 04-after-accept, ждёт mp-order-card вместо waitForTimeout (исключает гонку с fetchSupplierOrders).
E2E прогон: ekaterina создаёт order c8a26513 (статус ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL после фикса monotonic #151), ivanpetrov открывает /market/incoming-orders → таб «Индивидуальные ожидающие» → видит карточку № c8a26513 (2 ед., 240 ₽, ПВЗ krg) с кнопками «Принять/Отказать». Mutation marketplaceAcceptIndividualOrder отправляется на backend, backend дёргает chain action acceptOrder; цепь отвергает по max_transaction_cpu_usage=290000us (303363us занял chain — известный блокер #162, чинится отдельно через setparams).
backend `applyCycleTypeHook` ставит forward-статус (ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL для cycle_type=individual) сразу после submit'а createorder, затем парсер через ~2с проигрывает delta с on-chain status=active и `MarketplaceOrderDomainEntity.updateFromBlockchain` затирает forward-статус. Это блокировало magistral II шаг 4: поставщик не видел заказ в табе «Ждут моего акцепта», статус оставался ACTIVE.
Фикс — monotonic check в updateFromBlockchain: incoming status применяется только если его ранг ≥ текущего ИЛИ статус терминальный (CANCELLED_*, EXPIRED_*, RETURNED, RECEIVED). on_chain_id / on_chain_block_num / on_chain_present обновляются всегда.
+5 unit-тестов в marketplace-order.entity.spec.ts: forward (ACTIVE → ACCEPTED), два backward-кейса игнорируются (ACCEPTED ← ACTIVE и INDIVIDUAL ← ACTIVE), терминальный (CANCELLED_BY_ORDERER поверх ACCEPTED), одинаковый ранг (PENDING_SUPPLIER ↔ INDIVIDUAL). Все 12 тестов в файле зелёные.
Полный 4-шаговый прогон magistral II работает end-to-end:
offer-create (individual) → offer-moderation (approve) → order-create (ekaterina, 240 RUB)
→ incoming-orders (ivanpetrov видит обе карточки с «Принять/Отказать»).
Документируем три корневые причины Vite-залипания и оверлея, которые годами
мешали harness'у работать стабильно после reboot/restart:
1. cooptypes/dist/index.mjs может отсутствовать → Vite виснет на import резолве;
лечится `pnpm --filter cooptypes build` + cp в host'овый
/home/admin/mono-ai-4/components/cooptypes/dist/.
2. Vite optimizeDeps >20s на холодном route — `waitForTimeout(3000)` истекает
до отрисовки; заменили на `waitFor(<якорь страницы>, timeout: 90000)`.
3. localStorage harness:noBranchOverlay читается в watch-branch-overlay только
при mount'е первого auth-tick — setItem ПОСЛЕ loginAs опаздывает; через
addInitScript ДО loginAs флаг гарантированно установлен.
Остаточный issue (не блокирует UI): backend cycle-hook ставит
ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL, парсер перетирает на ACTIVE через 2с —
race с BlockchainConsumerService.
Закрывает magistral II через UI: offer ivanpetrov individual ACTIVE → order ekaterina
status «Размещён» виден в offerer/incoming-orders с действиями «Принять/Отказать».
Три однотипных фикса под холодный Vite optimizeDeps в desktop dev:
— offer-moderation.mjs: ждём заголовок «Модерация предложений» — иначе кнопка
«Одобрить» не находится в пустом Quasar-спиннере и approve не происходит.
— order-create.mjs: ждём mp-onboarding-gate / banner / q-page после goto
member-cpp, и Заказать-кнопку / mp-catalog-empty после goto catalog. До этого
схватывал пустой spinner и падал «В каталоге нет CatalogOfferCard».
— incoming-orders.mjs: harness:noBranchOverlay через addInitScript ДО loginAs.
Реактивность watch-branch-overlay.ts читает localStorage только при mount'е
первого auth-tick — поэтому setItem после loginAs опаздывает и оверлей
«Выберите КУ» рендерится первым, перекрывая страницу incoming-orders.
Прогоны 4/4:
✓ offerer/offer-create — offer ivanpetrov individual PENDING_MODERATION
✓ chairman/offer-moderation — approve, offer → ACTIVE
✓ orderer/order-create — order ekaterina на individual offer, 240 RUB списано
✓ offerer/incoming-orders — обе карточки (15e70181, 09c9d6c8) показаны
— Vite dev на холодном старте долго optimizeDeps'ит chunk'и страницы create-offer;
waitForTimeout(3000) не успевал и шот ловил пустой Quasar-спиннер; теперь ждём
конкретно label "Название" с большим timeout (90s) — надёжный сигнал что форма
отрисовалась, а не SPA bootstrap.
— Quasar q-radio в q-option-group рендерится как <div class="q-radio">, не <label>;
селектор `label.q-radio` молча промахивался и форма уходила с default cycle_type=
time_based (cycle_days=7), magistral II опять упиралась в cron-aggregator.
Сейчас .q-radio:has-text(...) + фолбэк по тексту — radio individual реально
кликается, offer попадает в БД с cycle_type='individual'.
Прогон harness'а: 4/4 шота, новый offer ivanpetrov individual PENDING_MODERATION
в marketplace_offer.
Default time_based с cycle_days=7 заставляет заказ ekaterina висеть в status=ACTIVE
7 дней до перехода в ACCEPTED_PENDING_SUPPLIER (через @Cron aggregator). Для
harness/документации нужен синхронный переход.
cycle_type=individual обрабатывается синхронно в
marketplace-order-create.service.ts:248 — Order сразу попадает в
ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL без cron-aggregator'а.
Добавлен click по radio «Индивидуально (individual)» в форме перед submit.
ПРИМЕЧАНИЕ: на текущем стенде Vite в desktop dev контейнере нестабилен после
нескольких прогонов harness — даёт пустой spinner. Альтернатива через
GraphQL-сидер (Task #122, seed-marketplace-flow.ts) надёжнее и не зависит от UI.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
getStatusLabel() требует Record<PaymentStatusEnum, string>, AWAITING_AUTHORIZATION
был добавлен в enum, но забыт в локальном маппинге → ts-node краш в dev.
`init/cooperative.ts` и `init/participant.ts` импортировали
`signProgramAgreement` из `tests/wallet/`, который на верхнем уровне делал
`import { expect } from 'vitest'`. При запуске `esno src/index.ts boot`
vitest падал с «Vitest failed to access its internal state», потому что
исполнялся вне vitest-воркера.
- Перенёс реализацию в `init/sign-program-agreement.ts`,
`expect(...).toBeDefined()` заменил на обычные `throw new Error(...)`.
- `getCoopProgramWallet` инлайнил (через `walletUtils` тащился второй
module-level `import expect from 'vitest'`).
- `tests/wallet/signProgramAgreement.ts` теперь реэкспортирует из init/,
чтобы существующие тесты продолжали работать.
- Заодно убрал дохлый `import { describe, expect, it } from 'vitest'` из
`init/participant.ts` (символы в файле не использовались) + неиспользуемые
axios/Registry/sendPost.../sleep/GOVERN_SYMBOL/SYMBOL.
Magistral II разблокирована тремя коммитами PR #24 — order-create зелёный, e2e
проходит до Notify «Заказ создан» и списания Main Wallet. При прогоне следующих
страниц (offerer/incoming-orders, orderer/orders, consolidated и т.д.)
зафиксированы пустые empty-state: заказ висит в status=ACTIVE.
Корневая причина: MarketplaceCycleAggregatorService @Cron каждые 5 минут
переводит ACTIVE → ACCEPTED_PENDING_SUPPLIER, но только когда offer.created_at +
cycle_days <= now(). Текущий offerer/offer-create.mjs использует default
cycle_days=7 — заказ ждёт 7 дней. Для документации нужен offer с
cycle_type=individual (синхронный переход в order-create.service.ts:248) или
cycle_days=0.
§9.15 в PLAN.md описывает варианты разблокировки: либо изменение
offerer/offer-create.mjs (создавать individual-offer параллельно), либо
TS-сидер seed-marketplace-flow.ts (Task #122).
Дополнительная находка: некритичный MutationLoggingInterceptor пишет
"null value in column username" — кандидат в отдельный фикс.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* MarketplaceOrderCreateService.normalizeTxResult бросал
«цепь не вернула tx_hash» хотя on-chain createorder проходил
и кошелёк списывался. Причина: код искал `tx?.transaction?.id`
и `tx?.processed?.id`, но @wharfkit/session@1.6.1 возвращает
TransactResult структуры `{ response: { transaction_id,
processed: { id, block_num } }, transaction: ResolvedTransaction
(без id-поля), resolved: ResolvedSigningRequest, ... }`. Поля
`.processed` на верхнем уровне нет, `.transaction.id` либо
undefined либо Checksum256 (не string).
* Правильно: `tx.response.transaction_id` (+ fallback'и на
`resolved.transaction.id` и плоский `transaction.id` для
совместимости с другими wharfkit-версиями — как в `extractTxHash`
marketplace-apl-reception / return-claim / issuance / writeoff
сервисов, где этот pattern уже работал).
E2E подтверждён: ekaterina подписывает ЦПП → marketplaceCreateOrder
проходит → Notify «Заказ создан» → кошелёк списан → offer
quantity_available уменьшился. Magistral II полностью разблокирована.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* desktop OnboardingMemberPickCpp/api: client.Mutation(mut, { input })
не передавал переменную в GraphQL — правильный pattern
`{ variables: { input } }` (как в submitCreateOrder). Backend отвечал
«Variable $input of required type MarketplaceSignOnboardingOfferInput!
was not provided», подпись ЦПП не доходила до on-chain. После фикса
`wallet::signagree` (voskhod/ekaterina program_id=2 draft_id=699)
проходит, marketplace::createorder больше не падает на
«walletop: не подписано program_id=2».
* docs-harness/bin/shoot.mjs: ветка `wasUp && distRebuilt` делала
`pkill -f 'quasar.js dev'` на хосте + spawn хостовый `pnpm run dev`.
Quasar dev живёт в docker-контейнере, pkill на хосте ничего не убивает,
а параллельный хостовый pnpm dev писал в .q-cache под admin uid, пока
контейнер пишет под node uid — EACCES, кэш разваливается, Vite
перестаёт резолвить @coopenomics/sdk. Если порт отвечает — ничего
не трогаем, vite сам подхватит dist через fs-watch.
* harness order-create.mjs: переписан под полный flow magistral II
с шагом подписи L3 ЦПП через /market/onboarding/member-cpp +
debug-снимок 00-marketplace-onboarding-page для будущей диагностики.
Magistral II по L3-подписи разблокирована. Остался отдельный блокер
в MarketplaceOrderCreateService.normalizeTxResult — backend не дождётся
tx_hash от цепи, хотя on-chain action createorder проходит
(out-of-band sync error). Это вне фоллоуапа Эпика 1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[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>
Закрывает блокер magistral II документации (см. order-create.md / PLAN.md §9.13):
on-chain marketplace::createorder валил assertion «у пайщика не подписано
соглашение program_id=2 для w.mkt.member», потому что в фабрике документов
отсутствовали adapter'ы для шаблона/инстанса оферты ЦПП «Стол заказов», а
mutation подписи прямо со стола заказов была пропущена в Эпике 1 (помечена
фоллоуапом в onboarding/README.md и в blago issue 598-4).
Что сделано:
cooptypes:
- IVars.marketplace_program / marketplace_offer_template (optional). Юр.текст
оферты остаётся «рыбой» — финальную редакцию готовит юр.отдел.
- UdataKey.MARKETPLACE_AGREEMENT_NUMBER / MARKETPLACE_AGREEMENT_CREATED_AT.
- 1101.MarketplaceOffer.Action расширён двумя обязательными полями: номер
соглашения и дата (передаются явно из L3 service, без Udata-таблицы).
factory (Эпик 1 / Story 1.7 финализация):
- Actions/1100.MarketplaceOfferTemplate.ts + Templates/1100.
MarketplaceOfferTemplate.ts (шаблон — только coop+vars, аналог
998.BlagorostProgramTemplate).
- Actions/1101.MarketplaceOffer.ts + Templates/1101.MarketplaceOffer.ts
(инстанс на конкретного пайщика — с common_user и номером соглашения).
- VarsSchema получил marketplace_program и marketplace_offer_template.
- index.ts и registry.ts factory регистрируют новые registry_id.
controller (Эпик 1 / Story 1.4 фоллоуап):
- DTO MarketplaceSignOnboardingOfferInputDTO — только signed document;
coopname/username берутся из JWT context'а guard'а (защита от подмены).
- MarketplaceOnboardingService.signOnboardingOffer — лукап
soviet::coagreements(coopname, type='marketplace'), вызов
walletBlockchainPort.signProgramAgreement (program_id из coagreement,
draft_id оттуда же, document — подписан фронтом).
- Mutation marketplaceSignOnboardingOffer в marketplace-onboarding.resolver.
ts с возвратом свежего MarketplaceOnboardingState (UI не нужен второй
запрос).
SDK:
- Mutations.Marketplace.MarketplaceSignOnboardingOffer wrapper (graphql-zeus
regen для нового input MarketplaceSignOnboardingOfferInput).
desktop:
- OnboardingMemberPickCpp api: signOnboardingOffer() — генерация документа
через DigitalDocument(registry_id=1101), подпись локальным WIF, вызов
новой mutation.
- OnboardingMemberPickCppPage.vue: onAccept теперь подписывает прямо со
стола (раньше редиректил в Registrator). По успеху — Notify «Оферта
подписана» + redirect /market/catalog.
- OnboardingCPPGate widget: новый пропс busy для loading-state кнопки
«Подписать оферту».
PLAN.md §9.14 — закрывающая запись по выбранному варианту A.
NB: schema.gql и SDK zeus/index.ts регенерированы автоматически — попадают
в коммит как часть SDK сборки. Текст оферты в cooptypes 1100/1101 остаётся
«рыбой» до утверждения юр.отделом; registry_id и подписи менять не
потребуется.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Пять 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>
Без решения пользователя по развилке A/B/C дальнейшие итерации
по магистрали II невозможны — все oставшиеся shots требуют успешного
order-create, а тот упирается в отсутствие подписи program_id=2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Что обновлено:
- shot 03-order-create-no-funds → 03-order-create-no-agreement (новый имя+PNG со свежим Notify).
- order-create.md admonition заменена: реальный блокер не «нет средств», а отсутствие записи в `wallet::users.programs[]` с program_id=2 (marketplace, draft_id=699). Зафиксированы две root cause-причины (factory adapter 1100 + L3 mutation marketplaceSignOnboardingOffer), две решённые подблокировки (cycle_type mapper + marketplace-deposit-fund скрипт).
- orders/01-my-orders-empty.png — обновлён PNG (тот же empty-state).
Почему. ekaterina пополнена 10000 RUB через wallet::createdeposit + gateway::completeincome (CHAIN_URL=8918 — критично для скрипта marketplace-deposit-fund.ts), но createorder валит assertion на o.mkt.assign (TRANSFER из w.wal.member в w.mkt.member): пайщик не имеет программного соглашения marketplace. Реальный L2-flow (core signup + factory render 1100) не работает в этом стенде, потому что (1) factory adapter 1100/1101 отсутствует в components/factory/src/Actions/, (2) L3 mutation для прямой подписи со стола обозначена как фоллоуап Эпика 1 в onboarding/README.md. Документация теперь явно описывает, что именно требуется доделать.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Создан components/boot/src/scripts/marketplace-deposit-fund.ts —
эмиссия в Main Wallet тестовых пайщиков через
wallet::createdeposit + gateway::completeincome (pattern из
seed-capital/08-investments.ts).
Запуск (ekaterina, sidorov) → fail:
«Пайщик не найден в кооперативе» (coops_access_helpers.hpp:57).
Root cause: текущий seed (add-plain-participant + ensureMarketplaceParticipant)
создаёт on-chain registrator account + mongo individual + pg user, но НЕ
делает joincoop через совет → запись в soviet::participants отсутствует →
createdeposit рекурсивно падает на membership check.
Архитектура кошельков (пояснил пользователь):
Main Wallet → членских взносов → программы (Стол заказов и др.)
Пополнение всегда в Main; конвертация автоматическая через
o.wal.conv / o.mkt.assign / o.mkt.block.
PLAN §9.12 фиксирует развилку (Task #138):
A) joincoop в installExtraData (быстрый seed)
B) UI-сценарий onboarding/join-coop (документация UX)
C) расширить add-plain-participant.ts (membership per default)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cycle_type Postgres-domain (snake_case) теперь конвертируется в eosio::name
(без `_`) на boundary chain submit:
marketplace-offer.types.ts:
+ MARKETPLACE_CYCLE_TYPE_CHAIN_NAME registry
+ toChainCycleType(cycle) helper (соответствует enum-правилу CLAUDE.md)
marketplace-order-create.service.ts:160:
cycle_type: toChainCycleType(offer.cycle_type)
Mapping: time_based→timebased, volume_based→volumebased,
open_subscription→opensubscr, individual→individual
(см. components/contracts/cpp/lib/domain/table_marketplace_orders.hpp:69-71).
После фикса pipeline проходит cycle_type-валидацию и доходит до
`o.wal.block`, где упирается в новый блокер: пустой L3-кошелёк тестовых
пайщиков (0,00 RUB). UI корректно показывает Notify
«Недостаточно средств для заказа: требуется 240.0000 RUB, доступно 0.0000 RUB»
через extractErrorMessage().
Обновлены:
- order-create.md admonition (старый блокер resolved, новый описан)
- shot переименован 03-order-create-onchain-error → 03-order-create-no-funds
- PLAN §9.11 фиксирует решённый и новый блокер
- Task #137 заведён для пополнения L3-кошельков в installExtraData
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- WrappedEditor (q-field+Editor) на странице транскрипции выглядел сломанным: floating-label «Заметка о звонке» падал поверх первой строки memo, кнопка «Сохранить» торчала сбоку без выравнивания, рамки редактора не было.
- Перешёл на голый Editor (Milkdown через src/shared/ui/Editor) в собственной карточке: border-radius:10px, рамка по тону --tr-border, padded=true. Кнопка «Сохранить» — в нижней панели карточки (border-top, flex-end). Hint «доступно председателю и членам совета» — отдельной строкой под карточкой.
- TranscriptionDetailPage: добавлен h2 «Заметка о звонке» как у секции «Текст», memo больше не висит без подписи.
Список транскрипций был зажат max-width: 720px в центре страницы и собран из самописных rows. Заменил на тот же паттерн, что использует CalendarPage / CalendarEventsTable — q-page(padding) + q-table с колонками: Звонок (название + превью memo), Начало, Длительность, Участники, Статус. Клик по строке открывает детальную страницу транскрипции.
Удалены типы '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 состояния.
- TranscriptionMemoEditor: q-input type=textarea заменён на WrappedEditor (Milkdown через src/shared/ui/WrappedEditor) — теперь memo отображается с markdown-рендерингом (заголовки/списки/жирный/ссылки), а не как plain текст. Кнопка «Сохранить» вынесена в отдельную панель под редактором (фокус-кольцо и поле растут естественно). Минимальная высота 180px, без скролла — растёт по содержимому.
- TranscriptionDetailPage: max-width 720 → 1040px, padding 16/20 → 16/32 — страница перестала быть сильно зауженной, гармонично для desktop.
- скилл manage-transcription-memo: добавлен раздел «Формат содержимого .memo.md» — первая строка строго одно предложение ≤150 символов без markdown (о чём был звонок), затем пустая строка и сжатая суть; этикет/повторы/разогрев убираются.
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%
ширины было визуально 'приплюснуто'.
Курсор transcriptionLastEndedExclusiveByProject фильтровал ВСЕ артефакты транскрипции — поэтому
для уже скачанных meeting.md sibling-файлы .memo.md не появлялись. Разделил циклы:
1. meeting.md — только endedAt > lowerBoundExclusive (тяжёлый GetTranscription с сегментами).
2. .memo.md — для всех COMPLETED транскрипций каждый pull (поле memo приходит уже в лёгком
GetTranscriptions, повторных запросов не делаем).
Проверено: blago pull → 4/4 sibling .memo.md появились пустыми в проекте 33-platforma-otchetov-dlya-fnsfss; затем blago transcription memo опубликовал 476 символов в крайней транскрипции 7116fb31-3b8c-4a63-9c0c-11da26aba075, повторный pull сохранил содержимое без конфликта.
orderer/order-create.mjs прогнан после магистрали II (Offer есть в каталоге),
но UI выводит stub Notify — реального сабмита marketplaceCreateOrder нет.
PLAN.md §9.9 фиксирует развилку: (A) доработать UI OrdererOrderCreate.vue
с реальной mutation + Membership flow или (B) TS-сидер через GraphQL.
(A) предпочтительно — даёт документируемую страницу.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Прогон цепочки через harness:
1. offerer/offer-create — Сидоров публикует «Картофель Адретта 25 кг»
2. chairman/offer-moderation — председатель одобряет → ACTIVE
3. orderer/catalog — Петрова видит offer в каталоге
4. offerer/my-offers — Сидоров видит свой offer в статусе ACTIVE
Также фикс meta пути в scenarios/marketplace/chairman/offer-moderation.mjs:
docPath/assetsDir указывали в new/marketplace/board/, что создавало дубль при
каждом install — теперь канон-путь new/marketplace/chairman/offer-moderation.
PNG перенесены в правильный каталог assets/.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- pull-communication: для каждой COMPLETED-транскрипции вызывает syncTranscriptionMemoFile, который ВСЕГДА обеспечивает файл meetings/<stem>.memo.md (пустой, если на сервере memo пуст). Файл сразу индексируется (entity_type=call_transcription_memo) — редактирование→blago transcription memo идёт без шагов «создать файл».
- Конфликты при отсутствии prev-индекса разруливаются явно: server пуст → принять локальный draft как baseline; оба непустые и разные → git-style merge-markers; совпало → проиндексировать как есть.
- update-transcription-memo: текст ошибки про отсутствующий sibling указывает на blago pull (создаст sibling сам).
- скилл manage-transcription-memo: процедура переписана под «sibling уже есть, просто открой и редактируй».
Реальный формат подписей кооперативного документа — это не «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 заголовок сокращён, чтобы не
обрывался при узкой колонке).
- новый entity_type call_transcription_memo в index-store (тип файла .memo.md, parsing/sync через стандартный syncEntityFile)
- pull-communication: для каждой COMPLETED-транскрипции с непустым tr.memo пишет sibling-файл meetings/<stem>.memo.md (hash = "<uuid>:memo"); при наличии локального draft, не индексированного в .blago/index.json, серверный memo не записывается — выводится warning
- update-transcription-memo: после успешной мутации сохраняет sibling и заносит запись в индекс (etag локального файла), чтобы следующий pull шёл штатно через syncEntityFile вместо warning'а
- скилл manage-transcription-memo: добавлены замечания про pull-синк, git-style маркеры конфликта и поведение неиндексированных черновиков
В документации не нужно перечислять то, чего в системе нет — это шум.
Описывать только используемые счета и операции.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- `blago transcription memo <pathOrId> [--file <path>] [--text <inline>]` — публикация краткого содержания транскрипции через chatcoopUpdateTranscriptionMemo
- pathOrId: путь к meetings/<stem>.md (id из .blago/index.json, entity_type=call_transcription) или UUID
- default-источник memo — sibling-файл meetings/<stem>.memo.md рядом с meeting (pull-only, в push не уходит)
- скилл ai/commands/manage-transcription-memo.md описывает workflow «прочитать meeting → собрать .memo.md → согласовать → опубликовать»
- backend: убран @MaxLength(4000) с UpdateCallTranscriptionMemoInputDTO.memo и описание лимита из @Field
- backend: chatcoopUpdateTranscriptionMemo больше не доступен роли user, только chairman/member (read-методы — без изменений)
- desktop: TranscriptionMemoEditor больше не задаёт maxlength и counter в q-input
Точечные правки на основе PRD «Стол заказов» MVP (~blago/production/1-prilozhenie-stol-zakazov):
- agenda-writeoff: операция o.mkt.wroff проходит через транзитный счёт 91
«Прочие расходы» в две проводки (Dr 91 / Cr 10, затем Dr 86 / Cr 91), а не
одной операцией списания с «накопителя». Транзит через 91 фиксирует
безвозвратное исчезновение имущества, не передачу пайщику.
- payouts-readonly: двухэтапная модель L12 (FR57). На signchair срабатывает
o.mkt.purch per-Order (фиксирует обязательство на счёте 86), создаётся
pending-запись. После банковского перевода кассир подтверждает, backend
дёргает o.mkt.payout (Дт 86 / Кт 51). Счёт 60 не используется.
- category-whitelist: категория «Услуги» помечена «вне MVP» — самостоятельная
категория поставки в MVP не реализуется (требует отдельной правовой
проработки). Заголовок раздела обновлён: «фиксированный набор категорий
имущества» вместо «10 категорий».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Магистраль I empty-state закрыта (9 страниц implemented, harness прогнан,
PNG installed, admonition «реализовано»). Добавлен §9.8 — конкретный план
сидера для Магистрали II: TS-скрипт seed-marketplace-flow.ts с GraphQL
chain из 6 mutations + on-chain APP-цепочка. Альтернатива — композитный
harness scenario через Playwright. Перечислены 13 шотов, которые нужно
переснять после прогона сидера.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Harness уже отстрелял 8 страниц, PNG установлены, проза описана — слово «ожидается
прогон harness» в admonition больше не актуально. Чистый статус «Реализовано» без
лишнего шума о промежуточном состоянии разработки.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Все скриншоты прогона harness'а (cedde9f277) теперь видны в MD через
 после admonition «Реализовано».
Существующая проза из PRD сохранена без изменений.
Страницы: orderer/consolidated, offerer/incoming-orders, offerer/offers,
chairman/category-whitelist, branch-chairman/branch-orders,
board/agenda-writeoff, board/payouts-readonly, onboarding/coop-accept-cpp,
onboarding/member-pick-cpp.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Поправки:
- offerer/incoming-orders.mjs: фикстура sidorov → ivanpetrov +
signAllAgreements (sidorov не подписал онбординг, диалоги перехватывали
клики).
- offerer/my-offers.mjs: ivanpetrov + signAllAgreements +
docPath/assetsDir выровнены на offers.md (Vue page = OffererMyOffers,
но MD-страница уже создана под именем offers.md с прозой из PRD).
- branch-chairman/branch-orders.mjs: chairkrg не имеет marketplace_pvz
membership на voskhod (роут /market-pvz/* требует roles: ['chairman']
кооператива). Используем loginAsChairman (ant) — председатель сам
открывает стол КУ для аудита; отдельный access-уровень «branch chairman»
подключится в Story 6.x+1.
Установлены PNG: incoming-orders (2), offers (2), branch-orders (2).
Все 9 из 9 marketplace-сценариев успешно сняты harness'ом против
desktop dev :3029 (docker-mount worktree → /app в mono-ai-4-desktop-1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Скриншоты:
- onboarding/coop-accept-cpp (2 шота: статус + диалог принятия)
- onboarding/member-pick-cpp (1 шот: gate или баннер)
- orderer/consolidated (1 шот: overview партий)
- chairman/category-whitelist (2 шота: overview + add-dialog)
- board/agenda-writeoff (2 шота: overview + on-agenda фильтр)
- board/payouts-readonly (1 шот: placeholder)
MD-прозы не перезаписаны (admonition «Реализовано» уже на месте, прозы
из PRD остаются валидны). Картинки подключатся в существующий текст MD
автоматически через relative paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Заменил warning admonition «Эпик X — в разработке» на info admonition
«Реализовано — ожидается прогон harness» с указанием коммита реализации
и команды harness'а для будущего прогона. Прозы сохранены — описания
из PRD остаются валидны как факт.
board/payouts-readonly остался в статусе «Placeholder — в подготовке»
(требует backend-расширения Payment/read:all).
Обновлены: orderer/consolidated, offerer/incoming-orders, offerer/offers,
onboarding/coop-accept-cpp, onboarding/member-pick-cpp,
chairman/category-whitelist, branch-chairman/branch-orders,
board/agenda-writeoff, board/payouts-readonly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Завершает набор harness-сценариев для всех 9 реализованных страниц:
- onboarding/member-pick-cpp.mjs (ivanpetrov, L3 gate)
- offerer/incoming-orders.mjs (sidorov, входящие заказы)
- offerer/my-offers.mjs (sidorov, мои Offer'ы)
- branch-chairman/branch-orders.mjs (chairkrg, сводный стол КУ)
- board/agenda-writeoff.mjs (chairman ant, повестка совета)
- board/payouts-readonly.mjs (chairman ant, placeholder)
Создана папка scenarios/marketplace/board/ (раньше отсутствовала).
Прогон harness'ом по-прежнему ЗАБЛОКИРОВАН — shared desktop dev не
видит worktree pages. До решения по подхвату 9 Vue-страниц в shared
desktop сценарии существуют как готовая инфраструктура.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Сценарии для прогона harness'ом по реализованным страницам:
- onboarding/coop-accept-cpp.mjs (chairman ant, L1 ЦПП)
- orderer/consolidated.mjs (ivanpetrov, партии заказчика)
- chairman/category-whitelist.mjs (chairman ant, whitelist категорий)
Прогон самим harness'ом пока ЗАБЛОКИРОВАН — shared desktop dev :3029
не видит worktree-страниц (src vs node_modules: src в worktree локален,
а node_modules симлинкованы). Чтобы снять скриншоты:
вариант A — sync 9 pages в shared desktop;
вариант B — поднять отдельный worktree desktop dev (медленно).
Остальные 6 сценариев (offerer/incoming-orders, offerer/my-offers,
branch-chairman/branch-orders, board/agenda-writeoff, board/payouts-readonly,
onboarding/member-pick-cpp) — создаются после прогона пилот-тройки.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Обновил PLAN.md по факту:
- §9.6 теперь не «дыры UI», а карта реализованных страниц с коммитами.
- §9.7 пересмотрен: аудит был пессимистичен. Backend для 8 из 9 страниц
уже был DONE — нужны были только SDK wrappers и Vue (8 страниц добавили
только UI, board/payouts-readonly — placeholder).
Урок для будущих аудитов: перед оценкой «нужно backend» делать grep по
marketplace-*.resolver.ts:@Query / @Mutation — список из 70+ резолверов
покрывает почти все MVP-use-case'ы.
Технический долг после 9 страниц: backend для board/payouts-readonly
(Payment/read:all policy), tree-выбор для category-whitelist, auto-detect
braname для branch-chairman, прогон harness'а по 9 страницам.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Эпик 1 / Story 1.4 + 1.11. Backend marketplaceOnboardingState уже DONE,
но SDK wrapper отсутствовал — добавлен Queries.Marketplace.MarketplaceOnboardingState.
Vue OnboardingMemberPickCpp: страница-точка-входа на подключение к Стол
заказов. Канон-виджет OnboardingCPPGate (UX-DR17) показывает пакет ЦПП
(оферта + Положение). Сама подпись делается через core Registrator-мастер
(wallet::signagree) — страница редиректит туда после accept.
Когда requires_gate=false — баннер «уже подключены» + переход к каталогу.
mp-role-orderer; роут /market/onboarding/member-cpp (все роли с auth).
Закрывает 9-ю из 9 нереализованных страниц Marketplace из PLAN.md §9.6.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Эпик 5 / Story 5.x. Совет нуждается в ленте всех выплат поставщикам;
текущий резолвер marketplaceListOutgoingPaymentsAsSupplier отдаёт только
выплаты текущего пайщика-supplier'а. Для совета нужна доп. policy
`Payment / read:all` в marketplace-access-matrix + новая query
`marketplaceListOutgoingPayments` с опц. supplier_account.
До этого — informational placeholder со ссылками на нужные файлы и
объяснением, как раздел заработает. До Phase 2 совет видит выплаты
через core cassir-стол (общий стол кассира кооператива).
mp-role-admin; роут /market/payouts (chairman + member совета).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Эпик 3 / Story 3.x. Backend available-category-admin.resolver.ts уже DONE
(@AuthRoles chairman). Добавлены 4 SDK wrappers:
- Queries.Marketplace.GetAvailableCategories
- Queries.Marketplace.GetAvailabilityStats
- Mutations.Marketplace.AddAvailableCategories
- Mutations.Marketplace.RemoveAvailableCategories
Vue ChairmanCategoryWhitelist: список разрешённых категорий (entire +
specific types), 4 stat-карточки (всего/категорий/типов/whitelist активен),
кнопка «Добавить» с диалогом ручного ввода ID через запятую, удаление
с подтверждением. Tree-выбор через marketplaceGetCategoryTree — следующий
шаг Story 3.x.
mp-role-admin; роут /market/category-whitelist (chairman).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Эпик 8 / Story 8.x. Совет видит проекты списания скоропорта в активных
статусах (ON_AGENDA / AUTHORIZED / EXECUTING / EXECUTED / REJECTED).
DRAFT-проекты скрыты — это рабочая зона администратора на /writeoffs.
Само голосование совета идёт через core soviet agenda (sov-flow);
здесь — обзор статусов и сумм потерь, фильтр по статусу, polling 30s.
Backend готов — reuse helper listWriteoffProposals из AdminWriteoffs/api
+ Queries.Marketplace.ListWriteoffProposals. Cast as never на statuses
из-за расхождения standalone vue-tsc и enum-merge (Zeus генерит enum
со values-как-литералами, vue-tsc вне -p не подхватывает merge).
mp-role-admin; роут /market/board-writeoff (chairman + member совета).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Эпик 6 / Story 6.x. Председатель КУ видит 3 ленты процессов своего
участка в одном экране с табами и счётчиками:
- Приёмки партий (Queries.Marketplace.ListAplReceptionsByBraname, Эпик 5)
- Выдачи заказов (Queries.Marketplace.ListIssuancesByBraname, Эпик 6)
- Гарантийные возвраты (Queries.Marketplace.ListReturnClaimsByBraname, Эпик 7)
Backend готов — обновление аудита §9.7 второй раз: «нужна агрегационная
query» неверно, 3 существующих query достаточно через Promise.all.
Read-only обзор; действия выполняются на /reception, /issuance, /returns.
Поле braname — ручной ввод (auto-detect через whoami — следующий шаг).
Polling 20s; mp-role-operator; роут /market-pvz/branch-orders (chairman).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Эпик 3 / Story 3.4 + Эпик 4 / Story 4.5. Backend marketplaceListMyOffers
и marketplaceListSupplierOrders уже DONE — было пропущено в аудите §9.7.
Добавлены только SDK wrappers Queries.Marketplace.ListMyOffers / ListSupplierOrders
и две Vue-страницы.
OffererIncomingOrders (/market/incoming-orders): заказы где supplier = пайщик,
канон OrderCard, фильтр по статусу через q-tabs, polling 15s.
OffererMyOffers (/market/my-offers): свои Offer'ы во всех 4 статусах
(PENDING_MODERATION / ACTIVE / REJECTED / WITHDRAWN), канон CatalogOfferCard,
client-side фильтр + поиск, polling 30s. Для REJECTED показывается
reject_reason под карточкой.
mp-role-offerer + marketplace-tokens на обеих.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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-* токены.
Эпик 4 / Story 4.4. Страница для пайщика-заказчика: его Order'ы группируются
по cycle_id (time_based/volume_based/open_subscription); индивидуальные —
отдельной группой. На каждой партии: этап (минимальный активный по STAGE_RANK),
суммарная стоимость, число заказов, ПВЗ. Внутри — канон-виджет OrderCard.
Источник данных — Queries.Marketplace.ListMyOrders (reuse MyOrders/api).
Polling 15s до Subscriptions Story 9.x. mp-role-orderer + marketplace-tokens.
Роут /market/consolidated (roles: chairman/member), иконка fa-layer-group.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Раньше compact-вариант рендерился без обрамления — avatar упирался
в левый край контейнера, визуально «проваливался» из общего стека
full-карточек (Screenshot_2026-05-21_21-51-09).
Compact теперь: padding 8/12px (тоньше чем full 16px), тот же border
+ surface + radius. В одну строку avatar + ФИО + AccountBadge + status,
но визуально это всё-таки карточка, не голая строка.
Body раньше был обычным block-контейнером, а .head и .value-row —
оба display: inline-flex. Без явного block-форматирования inline-flex
элементы становятся inline и рендерились в одну строку. Визуально:
Email⭐ivanov@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.
Зачем: пользователь обнаружил что 9 страниц Marketplace из PLAN.md §9.6 физически отсутствуют в Vue коде (бэкенд для Эпиков 1/3/4/5/6/8 DONE, фронтенд не реализован). Закрываю первую и самую компактную из 9 — Эпик 1 / Story 1.9-1.10 (L1 onboarding: приём кооперативом ЦПП «Стол заказов»).
Что сделано:
- SDK wrappers Queries.Marketplace.MarketplaceCppStatus + Mutations.Marketplace.MarketplaceAcceptCpp над уже существующими marketplaceCppStatus и marketplaceAcceptCpp GraphQL (Zeus схема в SDK уже содержит обе операции).
- Vue page components/desktop/src/pages/Marketplace/OnboardingCoopAcceptCpp/{api,ui,index.ts} (UX-DR конвенции: mp-role-admin, marketplace-tokens.scss).
- Роут /market/onboarding/coop-cpp в extensions/market/install.ts (roles: chairman/member, через agreementsBase).
Аудит остальных 8 страниц + приоритизация — в PLAN.md §9.7.
Ограничение: desktop в worktree через symlink тянет shared /home/admin/mono-ai-4/components/sdk, поэтому без пересборки shared sdk страница не подхватится в dev на :3029. Это будет решено отдельным PR (либо merge ветки, либо синхронизация SDK к shared).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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 в одну строку».
Зачем: пользователь, открывая ленту страниц marketplace в навигации, не должен решать, что заявленный функционал уже работает. Каждая из 9 страниц-заглушек (onboarding L1/L2, board/agenda-writeoff, board/payouts-readonly, branch-chairman/apl-reception-close, branch-chairman/branch-orders, chairman/category-whitelist, offerer/offers, offerer/incoming-orders, orderer/consolidated) получила `!!!warning "Эпик X — в разработке"` admonition с указанием конкретного Эпика и текущего обходного пути на MVP-стенде.
PLAN.md §9.4 переписан: Магистраль I empty-state фактически исчерпана UI-кодом, приоритет смещается на Магистраль II (сидер marketplace-flow в installExtraData). Добавлена секция §9.6 «Дыры UI» с таблицей 9 нереализованных страниц и ожидаемых Эпиков.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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 селекторов.
Контекст: после одобрения председателем «Берёзовый сок» в прошлой
сессии moderation page стенд имеет один APPROVED offer. Перенимаем
этот факт для документации:
1. harness.mjs / cleanViteOverlays — добавлен опциональный
preserveNotifications=true. По умолчанию shot() продолжает чистить
.q-notification (как раньше), но сценарии для UI-заглушек могут
попросить оставить тост в кадре.
2. orderer/catalog.mjs — пересминаем как 01-catalog-with-offer.
Снимок показывает CatalogOfferCard (UX-DR10) со статус-чипом
«Опубликовано», фильтр-чипы категорий «Все 1 / Овощи и фрукты 1».
Старый 01-catalog-empty.png удалён из docs/assets, ссылка в
catalog.md обновлена.
3. orderer/order-create.mjs — новый сценарий: клик «Заказать» в
каталоге показывает Quasar Notify с фактическим текстом
«Создание заказа на Offer X — будет доступно после Эпика 4»
(Story 4.1 ещё не реализована). Снимок взят с
preserveNotifications=true. В order-create.md добавлен !!!warning
«Story 4.1 в разработке» с этим снимком в начале страницы.
Результат: документация честно отражает текущее состояние MVP —
там, где UI ещё заглушка, читатель видит заглушку и причину.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Vue-страница OffererPaymentHistoryPage существовала в src/pages/Marketplace
после Story 5.9, но не была подключена ни в роутинг, ни в документацию.
Закрыло пробел Эпика 5 — поставщик не мог посмотреть, какие платёжные
запросы кооператив сформировал по его закрытым актам приёмки.
Добавлено:
- Роут /market/payments (роль `[]`, доступ всем пайщикам).
- Сценарий offerer/payments.mjs со снимком empty state.
- MD-страница new/marketplace/offerer/payments.md с описанием статусов
INITIATED/CONFIRMED/FAILED, банковского референса и поведения «Обновить».
- Пункт «История выплат» в mkdocs.yml под Стол поставщика.
- §9.2 PLAN.md обновлён: operator 5/5, offerer 3/5+1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Vue-страницы OperatorReception/OperatorInventoryLabeling/OffererPendingAplReceptions/
OffererSupplyPreparation существовали в src/pages/Marketplace, но не были
подключены в extensions/market/install.ts (мёртвый код после Эпиков 5-6).
Регистрируем 4 роута:
- /market/supply-prep — поставщик подтверждает готовность отгрузки
- /market/apl-receptions — поставщик подписывает signapl1
- /market-pvz/reception — председатель КУ создаёт АПП на партию
- /market-pvz/labeling — оператор клеит EAN-13/CODE128
Каждый закрывает страницу плана PLAN.md (operator/apl-reception-create,
operator/inventory-label, offerer/apl-reception-sign, offerer/shipment-prep)
empty-state снимками для документации.
Также: §9 в scenarios/marketplace/PLAN.md обновлён — живой статус Фазы 1
(19→23/30 сценариев), приоритеты для /loop-цикла, известные блокеры.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Старое значение --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>
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>
ChairmanModerationPage.vue (mp-role-admin) с CatalogOfferCard,
route marketplace-moderation, типизированные SDK-обёртки
(ListPendingOffers / ApproveOffer / ListModerationLog) — backend
уже работал, не было только UI. harness scenario + автоматическое
прохождение onboarding-каскада соглашений в loginAsChairman.
@livekit/rtc-node (Rust + rustls + rustls-native-certs) reads root CAs
only from the system trust store. node:22-slim ships without
ca-certificates, so without /etc/ssl/certs/ca-certificates.crt every TLS
handshake from native bindings fails with
"invalid peer certificate: UnknownIssuer" — even for a valid LE chain.
Node.js itself is unaffected (own embedded CA bundle).
Incident 2026-05-21: secretary in dicoop/coopback:v2026.5.21-2 could not
connect to wss://chatcooprtc.coopenomics.world (controller logs full of
SecretaryAgentService UnknownIssuer; nginx on api-prod never saw the
handshake — broken before HTTP upgrade). Hot-fix on
voskhod-coopback-blue (docker cp ca-certificates.crt + restart)
restored secretary connectivity; this commit makes the fix permanent
in the base image so every consumer of dicoop/mono-base
(coopback / desktop / cooparser / notifications / boot) inherits it
on the next release tag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-mature payment row: при wallet.interactor.ts::createWithdraw платёж
сразу появлялся в gateway PG со status=PENDING — кассир видел заявку
как готовую к выплате ДО того как совет её утвердил.
Минимальный фикс без переписывания create/sync-цепочки:
- PaymentStatusEnum += AWAITING_AUTHORIZATION (с лейблом «Ожидает
решения совета»). Initial status в gateway.interactor.createWithdraw
переключён с PENDING на AWAITING_AUTHORIZATION.
- Новый WithdrawAuthorizationListener в gateway.module:
- on-chain `wallet::authwthd` (совет авторизовал)
→ AWAITING_AUTHORIZATION → PENDING (кассир увидит и сможет подтвердить).
- on-chain `wallet::declinewthd` (совет/Gateway отказал)
→ AWAITING_AUTHORIZATION → CANCELLED.
UI кассира (desktop) должен в отдельном PR фильтровать AWAITING_AUTHORIZATION
из списка платежей (текущие компоненты, скорее всего, и так не показывают
неизвестный статус). PAYMENT_STATUS_LABELS подхватится автоматически.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Перенёс INSERT extension market из ad-hoc функции в installExtraData (где
он был хаком сразу после фикса) в initExtensionsInPostgres() — там же где
capital и reports. Это правильное архитектурное место: postgres-init.ts
владеет seed'ом extensions при первичной инициализации PG, и market теперь
получает ту же обработку, что capital.
schema_version=0 в INSERT означает: при следующем старте coopback
ExtensionSchemaMigrationService прогонит marketplace-bootstrap миграции
v1→v6 по порядку. v4 (upsertBaseline) сидирует 9 baseline-категорий.
Заодно убрал дубликат ensureMarketplaceExtensionEnabled() из infra.ts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Завершение возврата паевого (o.wal.wthcpl) переключено с TRANSFER
(SHARE_FUND_PAY → WITHDRAWALS_SINK) на новый WalletOp::BURN_BLOCKED:
заблокированная при createwthd сумма сжигается на кошельке пайщика,
получателя на цепи нет — деньги уходят из системы банковским переводом.
Чинит assertion 'walletop TRANSFER: недостаточно L3-средств' на
completewthd: после REQUEST_WITHDRAW (BLOCK) сумма лежит в L3.blocked,
а TRANSFER проверял L3.available.
Изменения:
- ledger2: WalletOp::BURN_BLOCKED=6 + case в walletop.cpp (списание из
L2/L3 blocked). OPERATION_REGISTRY: o.wal.wthcpl с BURN_BLOCKED, без
wallet_to. burn_pattern_correct обобщён на оба BURN-варианта.
- cooptypes: WalletOp += 'BURN_BLOCKED'; o.wal.wthcpl wallet_to=null.
- wallets.hpp: WITHDRAWALS_SINK (w.wal.wthdrw) помечен DEPRECATED —
остаётся в реестре для исторических L2-балансов.
- standard p.wal.wthdrw: приведён к коду — одностадийный flow
(pending → authorized → completed), без approvewthd.
- wallet::approvewthd удалён как dead code: action не в whitelist
callback'ов, из createwthd создаётся повестка сразу с callback=authwthd.
Удалены: .cpp, declaration в wallet.hpp, include в wallet.cpp, struct
и action в abi_json/wallet.json.
- sov.authpkg.standard.yaml: упоминание approvewthd → authwthd.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ensureBranchPaymentMethod() сохраняет dummy bank_transfer paymentMethod
для каждой КУ в Mongo. Без него branch.interactor.getBranch() падает
TypeError в строке 55 (new BankPaymentMethodDTO(null) destructure-ит
username из null), и весь getBranches возвращает 500 → desktop UI
«Кооперативные участки» показывает «Нет данных».
После фикса страница показывает 3 КУ с председателями и реквизитами
банка (ПАО Сбербанк / Тинькофф / ВТБ). MARKETPLACE_BRANCHES расширен
полем bank_name; payment method сохраняется через generator.save
('paymentMethod', ...) после ensureMarketplaceBranch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ensureMarketplaceExtensionEnabled() делает INSERT в `extensions` с
schema_version=0, чтобы при следующем старте coopback bootstrap-миграции
v1→v6 marketplace-extension прогнались по порядку. v4 (marketplace-
bootstrap-v4) делает upsertBaseline() для marketplace_category — заливает
9 baseline-категорий (8 продовольственных + «Прочее»).
Без записи MarketplacePlugin.initialize() бросает «Конфиг не найден» и
весь marketplace-стэк выключен: marketplaceListCategories возвращает [],
форма «Категория *» залипает на валидации, marketplaceCreateOffer
недоступна. В проде это делает chairman через installExtension; для
dev-стенда сидируем напрямую через PG (идемпотентно — ON CONFLICT DO NOTHING).
Подтверждено: после активации marketplaceCreateOffer отрабатывает за
29мс, offer попадает в marketplace_offer со статусом PENDING_MODERATION.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- factory/PDFService.generatePDFBuffer: писать tmp в os.tmpdir(), не в __dirname/tmp.
dist/ внутри docker bind-mount RO → mkdirSync падал EROFS на каждом generateDocument.
Блокировало онбординг (4 соглашения) + marketplace submit + любой signAgreement.
Видно в логах: generateDocument теперь успешно за 3.8с для registry_id 1-4.
- harness/loginAs: вынес общую часть в ensureSigninReady() c 3 попытками reload
и cleanViteOverlays. После рестарта desktop dev контейнера первый goto оставлял
пустой spinner (vite-plugin-checker overlay перехватывает клики), reload фиксит.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Также:
- harness.mjs dismissOnboardingDialogs ловит «Формируем документ…» состояние SignAgreementDialog по фразе в q-bar/body (ModalBase рендерит title в span, не в .q-toolbar__title).
- chairman/branches.mjs адаптирован под просмотр готовых КУ из installExtraData (без UI-сабмита, КУ уже создаются on-chain в boot:extra).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
После Эпика 2 / компонента 48 `soviet::sndagreement` отказывается на
`program_id > 0`, программные соглашения переехали в контракт wallet
(`wallet::signagree`, ADR-008). Соответственно boot test helper'ы
(`signWalletAgreement`/`signCapitalAgreement`/`signGenerationContract`)
и две точки `init/*` (`participant.addUser`, `cooperative.createCooperative`),
которые ещё дёргали `soviet::sndagreement` через `signAgreement`/
`blockchain.sendAgreement` с `agreement_type ∈ {wallet, blagorost, generator}`,
падали в beforeAll тестов на ассерте контракта.
Делаем:
- Новый общий helper `tests/wallet/signProgramAgreement.ts` —
обёртка над `wallet::signagree` с auth=coopname@active. Проверяет, что
в `wallet::users[username].programs[]` появилась запись с нужным
`program_id`. Возвращает `{wallet, program, txId}` — той же формы,
что прежние helper'ы (downstream-тесты остаются без изменений).
- Три исторических helper'а становятся тонкими обёртками над ним:
`signWalletAgreement` → (program_id=1, draft_id=1),
`signCapitalAgreement` → (program_id=4, draft_id=1000),
`signGenerationContract` → (program_id=3, draft_id=0).
- `tests/capital/consts.ts` — добавлены `walletDraftId`/`sourceDraftId`/
`capitalDraftId`, соответствующие мапе `program_map` в
`lib/core/programs.hpp` (источник правды).
- `init/participant.ts` (`addUser`) и `init/cooperative.ts`
(`createCooperative`) — `blockchain.sendAgreement(...agreement_type='wallet')`
заменён на `signProgramAgreement(...program_id=1, draft_id=1)`.
`tests/soviet/signAgreement.ts` оставлен как есть — он по-прежнему нужен
для не-программных типов соглашений (`signature`/`user`/`privacy`,
program_id=0), которые остались за `soviet::sndagreement`.
Не входит:
- Замена `getUserProgramWallet` (читает legacy `soviet::progwallets`)
на чтение `wallet::users` в downstream-тестах — отдельный технический
долг, не блокирует beforeAll.
На voskhod walletop `convertsegm` падал «недостаточно L3-средств у
пайщика» в проектах, где CRPS-перераспределение увеличивало долю
contributor'а. Причина: `w.cap.gen` был USER_SHARED, а CRPS в
`approvecmmt` не делал per-user компенсирующих TRANSFER между сегментами:
инвариант `Σ COMMIT_RID == Σ ACCEPT_RID` соблюдался только на проекте,
не на сегменте, и L3-проверка walletop ломала convertsegm у пайщиков,
чья доля выросла через CRPS.
Фикс — переключение `w.cap.gen` с USER_SHARED на COOPERATIVE (ADR-002):
генератор становится единым кооперативным пулом без L3, walletop
проверяет только L2-баланс пула. `convertsegm` любого пайщика берёт из
общего котла ровно `segment.available_for_program` без проверки
персонального остатка.
Изменения:
- wallets.hpp: GENERATOR_FUND kind USER_SHARED → COOPERATIVE.
- signact2.cpp: коммент-инвариант — закрытие 08 на программе, не на
сегменте (L3-разрез по пайщику снят).
- ledger2::migrate(): переписан на cleanup осиротевших userwallets
[voskhod, w.cap.gen, *] прямым erase. Старая legacy → ledger2
миграция убрана (meta.migrated=true в проде), сигнатура сжата до
пустых скобок. migrate теперь — универсальная точка расширения
для разовых исправлений состояния ledger2, по аналогии с
capital::migrate.
migrate3 и capital::migrate сознательно не трогаются: migrate3 —
низкоуровневая per-record миграция L3 со своим назначением (не для
очистки чужой таблицы); capital::migrate остаётся пустой точкой
расширения капитала.
L2 `wallets2[w.cap.gen]` = Σ старых userwallets — синхронен на момент
перехода (проверено по фактическим данным voskhod).
Не входит в этот PR: балансовая корректировка для уже застрявших
пайщиков на voskhod — отдельной миграцией-скриптом после деплоя.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why:
- backend mono-ai-4 :3028 был перезагружен с нуля (boot:extra с правильным
MONGO_URI?directConnection=true и POSTGRES для per-instance портов),
cooperative voskhod + 5 членов совета загружены в БД через cooparser initial-load
- старые скриншоты в PR были сняты до wipe — UI мог показывать stale state
Также:
- components/desktop/quasar.config.cjs: viteConf.server.fs.strict=false для assets
из bind-mount /home/admin/mono-ai-N (worktree compose override)
- components/docs-harness/bin/shoot.mjs:
- ensureOneFixture передаёт CHAIN_URL и MONGO_URI?directConnection=true в env
add-plain-participant (без CHAIN_URL скрипт падает на ECONNREFUSED)
- runScenario передаёт BASE_URL в env под лежащий снизу harness.mjs
(раньше default :2999 — это mono-ai-1 desktop, не свой)
Файлы первой партии docs-harness имели title/generated_by/scenario, но без tags.
Без tags страница не попадает в фильтры «Пайщик / Председатель / Член совета»
на главной mkdocs. Привёл к единой конвенции:
- chairman/dashboard.md — tags: Председатель
- chairman/design-system.md — tags: Председатель, Член совета (UI доступен chairman/member)
- chairman/market-tour.md — tags: Председатель
- offerer/offer-create.md — tags: Пайщик
- orderer/marketplace-tour.md — tags: Пайщик
Submit-кнопка называется «Опубликовать на модерацию», не «Создать».
Quasar рендерит label как div.q-field__label, не <label>, поэтому
locator('.q-field').filter({ has: locator('.q-field__label', ...) }) —
корректный путь к контейнеру q-select. Кадр 03 теперь снимается.
Каталог в кадре 04 остаётся пустым: backend :2998 в текущей среде не
имеет marketplace-резолверов (marketplaceListCategories отсутствует
в GraphQL-схеме), fetchCategories возвращает []. Сценарий готов: когда
controller с marketplace-extension будет поднят, выбор категории и
публикация offer пройдут автоматически.
Существующая проза описывает функционал каталога; добавил один кадр
с реальным состоянием на свежем стенде (фильтр «Все 0», плейсхолдер
«Ничего не найдено», свежая пайщица Екатерина без баланса).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Story 9.1: /market-pvz/warehouse, поштучный реестр единиц имущества
на складе одного КУ. Фильтр Код КУ/Состояние/Заказчик + кнопка
«Обновить». Таблица «Штрих-код / Товар / Заказчик / Ед. / Состояние /
Возраст / Промаркировано» с 4-стадийным жизненным циклом единицы
(LABELED/ISSUED/RETURNED/WRITTEN_OFF). Backend ACL: read:own-KU.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Story 7.2-7.4: /market-pvz/returns. Председатель КУ загружает ленту
заявлений своего ПВЗ. Три секции жизненного цикла: «Ждут удалённого
рассмотрения» (teal), «Ожидают очного визита» (orange), «Архив»
(grey). Compensating forward через транзит 91: пара o.mkt.return +
o.mkt.return2 атомарно возвращает средства и имущество.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
/market-pvz/list (workspace market-pvz). Снимок: пустой список с
жёлтым баннером «Карта недоступна: не задан YANDEX_MAPS_API_KEY» —
ключ Yandex.Maps не входит в стандартный локальный .env. ПВЗ
регистрируется ончейне через o.mkt.regbranch после создания КУ.
Канон-виджет KUMapWithList (UX-DR16). Доступ — chairman.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Story 9.2: /market/warehouse-summary, read-only обзор для chairman/member.
Снимок: две вкладки (Сводный склад / Поток заказов и поставок), 4 KPI
(Активных позиций / КУ с движением / Товарных позиций в обороте /
Списано накопительно), таблица SKU×ПВЗ с поиском по SKU, плейсхолдер
«Нет данных». Списания/выдачи здесь не редактируются — только триггерами
контракта marketplace.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Story 8.7: admin-стол /market/writeoffs. Снимок раскрывает регламент
1106→1105 (Заявление председателя + Протокол совета), кнопку
«+ Новый черновик», карточку «В работе совета», пустые ленты проектов
и архива у свежего стенда. Списание per-item через атомарную пару
o.mkt.wroff + o.mkt.wroff2; ролевой доступ chairman/member.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Story 6.7: отдельная страница для статуса READY_TO_RECEIVE, открывающаяся
из push-уведомления marketplace-order-ready. Снимок: пустая таблица
«Заказ / Пункт выдачи / Количество / Сумма заказа / Открыто к выдаче»
с плейсхолдером «Нет заказов, готовых к получению» для свежей пайщицы.
Подключил страницу в mkdocs.yml в навигации Стола заказчика.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Лента my-orders пайщицы Екатерины: 8 табов-фильтров по статусу
жизненного цикла заказа (Все/Активные/Ждут поставщика/Приняты/
Готовы к выдаче/Получены/Отменены заказчиком/Отменены поставщиком)
и пустое состояние «нет заказов» для свежей пайщицы. Селекта статусов
нет — фильтрация табовая, всё видно на одном кадре.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Документирует фактическое поведение свежей пайщицы Екатерины при первом
входе на Стол заказов: модальный стек 4 общих соглашений (ЦПП Кошелёк +
3 базовых) держит любую навигацию в /market/*; отдельной гейт-оферты
расширения сегодня нет; после подписания каталог открывается пустой
в контексте «Стола пайщика». Проза переписана с гипотетического
описания на наблюдаемые факты со скриншотов.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Зафиксированы 2 блокера фронта, мешавшие снять marketplace-документацию,
плюс 23 скриншота и 2 инструкции (`dashboard.md`, `market-tour.md`).
## Фиксы desktop
1. `ProcessesPage.vue` — убраны TS non-null assertion `!.` из pug-template
v-for. Они проникали в скомпилированный render-функцию и приводили к
runtime `SyntaxError: missing ) after argument list` на import-time всех
extensions, что блокировало init-app целиком.
2. `Environment.ts` — раскомментирован fallback на `/config.default.js`,
когда SSR middleware не запущен и `/config.js` отдаёт SPA-fallback HTML.
Без этого фронт получал пустой BACKEND_URL и зависал на loadSystemInfo.
3. `config.default.js` — обновлён под реальный локальный стек
(BACKEND_URL=127.0.0.1:2998, CHAIN_ID соответствует blockchain-data).
4. `watch-branch-overlay/index.ts` — добавлен harness escape hatch
`localStorage.harness:noBranchOverlay` для снятия скриншотов разделов
без процедуры выбора КУ. В prod localStorage пуст, поведение не меняется.
## Документация
- `new/marketplace/chairman/dashboard.md` (11 шотов) — обзорная инструкция
по разделам Стола председателя: онбординг, запросы одобрений, магазин
приложений, члены совета, ПВЗ, регистрационные взносы, ключ кооператива,
провайдер платежей, контакты.
- `new/marketplace/chairman/market-tour.md` (12 шотов) — экскурсия по всем
маршрутам Стола заказов (`/market/*` и `/market-pvz/*`): каталог, создание
предложения, мои заказы, готовые к выдаче, возвраты, списания скоропорта,
склад, реестр экосистемы, ПВЗ-разделы.
- `mkdocs.yml` — добавлены 2 пункта в nav.
## Harness
- `dashboard-overview.mjs` — переписан под кликовый обход меню (без goto, чтоб
не триггерить branch-overlay в navigation guard).
- `market-tour.mjs` (новый) — 12-шаговая экскурсия по marketplace-маршрутам
с harness-bypass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- init-wallet: info.coopname может быть undefined из-за race между login и
loadSystemInfo. Когда login завершается быстрее, init-wallet вызывает
loadUserWallet({coopname: undefined}) → GraphQL 400 → catch → reload →
фронт никогда не редиректит после login. URL-fallback извлекает coopname
из window.location.hash/pathname как safety net.
- harness scenarios/marketplace/chairman/dashboard-overview.mjs: обзорный
сценарий по 5 marketplace URL-ам (/marketplace, /marketplace/catalog,
/marketplace/orders, /chairman, /chairman/approval-requests). Без
waitForURL — фронт сейчас не делает redirect после login.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Environment.ts: check content-type before eval(/config.js) — в dev-режиме Vite catch-all отдаёт index.html на отсутствующий /config.js, и eval(htmlString) бросает SyntaxError из синхронного XHR, который всплывает как «Quasar boot error» в обход try/catch и ломает последующую init-app. Проверка content-type включает eval только если ответ действительно JavaScript.
- boot/src/scripts/seed-cooperative-base.ts: новый idempotent скрипт для восстановления базовых сущностей кооператива в Mongo (vars, monos, vaults). Полезен когда chain (blockchain-data) сохранился, а Mongo volume сбросился — boot:extra падает на adduser duplicate, а этот скрипт сидит ровно то что не на chain.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
- 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>
Воздух над заголовком стола внутри WorkspaceSwitcher — заголовок не упирается
в верхнюю границу caption «ПК «ВОСХОД»».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- .rail__top: padding 18px 20px → 16px 8px (унификация боковых отступов)
- WorkspaceSwitcher: убраны :disabled-атрибут и hover:not(:disabled) — у пайщика, которому доступен только один стол, кнопка выглядит как обычно (без not-allowed-cursor, без приглушённого цвета). Меню q-menu просто не открывается через v-if='workspaces.length > 1'. Тихое игнорирование клика вместо явного запрета.
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), даёт больше места под текст
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
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
Резолверы marketplace с capability :own-KU теперь принимают и доверенных
лиц председателя кооперативного участка (branch.trusted[]), а не только
самого председателя (branch.trustee). По комментариям ревью к PR #10 / #15.
Источник правды — таблица branches контракта branch (поля trustee + trusted[]).
Helper MarketplaceBranchOwnershipService инжектится через @Global()
BlockchainModule.BRANCH_BLOCKCHAIN_PORT.
Затронутые места:
* marketplace-issuance.resolver:marketplaceListIssuancesByBraname — добавлен
member + ownership-check (раньше отсутствовал любой ownership).
* marketplace-inventory.resolver:marketplaceListInventory — role-fork
admin/board (весь склад) vs operator (свой КУ с trustee-учётом).
* marketplace-return-claim.resolver — 4 mutations (approveReturnVisit,
rejectReturnRemote, acceptReturnAtVisit, rejectReturnAtVisit) +
listReturnClaimsByBraname + marketplaceReturnClaim (single): ownership
по data.braname / claim.delivery_braname с учётом trustees.
* marketplace-apl-reception.resolver:marketplaceListAplReceptionsByBraname
— добавлен ownership-check (раньше отсутствовал).
Phase 2 follow-ups (не в scope этого PR):
* Mutation marketplaceLabelInventory / marketplaceLabelShipmentInventory —
проверка operator имеет право label'ить inventory по order.delivery_braname
должна сделаться в service-уровне.
* Кеш branch lookup'ов с TTL (сейчас каждый запрос — chain RPC).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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 селектор
- .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-токены
- 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 не должен растягиваться во всю ширину карточки
HIGH:
- submitOnChainSignSupp / submitOnChainSignChair использовали
«last-non-empty» с fallback `signsupp-<reception_id>` /
`signchair-<reception_id>` если ВСЕ tx_hash в группе оказались
пусты. Внутри for-of цикла tx_hash чек шёл с `if (txHash)
lastTxHash = txHash` — пропускал пустой ответ молча.
- Теперь: на любой пустой ответ от цепи внутри loop — fail-fast
`ConflictException` с указанием конкретного Order'а. Часть группы
могла быть уже подписана выше — продолжать и финализировать APL
с фантомным tx_hash в БД категорически нельзя; ретрай безопасен
(контракт идемпотентен по order_hash).
Тесты: jest apl-reception — 6/6 passed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CRITICAL:
- `MarketplaceOfferService.hasActiveOrders` был TODO-stub (`return false`)
с комментарием «Order'ов нет — withdraw разрешён всегда. Story 4.x
перепишет на OrderRepository.countActiveByOffer». Эпик 4 уже merged
в marketplace2 (PR #5) — Order'ы существуют. Stub НЕ был обновлён →
supplier мог withdraw'нуть Offer с активными заказами, у пайщиков
средства оставались в blocked, заказы зависали.
- Подключён `MARKETPLACE_ORDER_REPOSITORY`. Метод `hasActiveOrders`
через `orderRepo.list` с фильтром по 6 не-терминальным статусам
(ACTIVE / ACCEPTED_PENDING_SUPPLIER[_INDIVIDUAL] / ACCEPTED /
ACCEPTED_TO_COOP / READY_TO_RECEIVE). Пагинация 1×1 — нужен только
factOfExistence через totalCount > 0.
Тесты:
- makeOrderRepo() helper в unit-spec, batch-обновление 40 instances
`new MarketplaceOfferService(repo, cats, makeOrderRepo())`.
- jest marketplace-offer-service — 40/40 passed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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()
По ревью-комментариям к PR #4 (Эпик 3 vitrina):
1. categoryOfferCounts.ts query: было inline-объявление полей в
`Selector('Query')(...)`. Создан селектор
`marketplaceCategoryOfferCountSelector` с _validate-проверкой
на `MakeAllFieldsRequired<ValueTypes['MarketplaceCategoryOfferCount']>`,
query использует его.
2. listCatalog.ts query: было inline `{ items, totalCount, totalPages,
currentPage }`. Создан селектор `marketplaceOfferPaginationSelector`
(по канону `paginatedAgreementsSelector`, `paginatedMarketplace
WriteoffProposalsSelector`) на тип `MarketplaceOfferPaginationResult`
с _validate. Query берёт его целиком.
3. MarketplaceCatalog/types.ts: убраны ручные типы `MarketplaceOfferView`,
`MarketplaceOfferPage`, `MarketplaceCategoryView`,
`MarketplaceCategoryOfferCount` — теперь все четыре выведены из
`Queries.Marketplace.{ListCatalog,ListCategories,CategoryOfferCounts}.
IOutput`. Канон как в других страницах desktop'а.
4. api/index.ts: убраны `as MarketplaceOfferPage`/`as
MarketplaceCategoryView[]`/`as MarketplaceCategoryOfferCount[]` cast'ы
и `?? []` fallback'и — destructure от Queries даёт уже типизированный
результат через IOutput.
5. selectors/marketplace/index.ts: добавлены оба новых selector файла.
SDK перебилжен (unbuild succeeded, 438 kB). controller/tsc + vue-tsc по
изменённым файлам без ошибок.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
HIGH:
- TakeoverDialog: добавлены role/aria-modal/aria-labelledby/aria-describedby
на q-card (Story 10.2 NFR-A1). Раньше screen reader'ы не озвучивали
заголовок takeover-диалога — несоответствие AC «aria-label/
aria-describedby на custom-компонентах где Quasar базовых
недостаточно».
MEDIUM:
- BarcodeScanner: timer cleanup при unmount через onBeforeUnmount.
Иначе при закрытии TakeoverDialog раньше 1.5 s scanning-таймера
scanned/error эмитится на размонтированный компонент → memory leak
+ lost-event race.
Story coverage:
- Story 10.1 «дизайн-токены»: marketplace-tokens.scss проверен —
spacing/touch-targets (44/48 POS-режим)/per-role layout/dark mode
реализованы корректно. Без правок.
- Story 10.2 «13 custom-компонентов»: все 13 на месте
(12 в widgets/Marketplace/ + KUMapWithList в widgets/KUMapWithList/).
A11y базовая обеспечена через QDialog/QInput штатные aria;
TakeoverDialog получил недостающие atribute'ы.
- Story 10.3 «performance baseline»: NFR-P1/P2/P3 — manual measurements
в MVP (без CI gate), нет автоматизации; код-аудит не выявил blockers.
Known limitations (Phase 2):
- Storybook stories — AC Story 10.2 требует, но папка stories/ пустая.
- Unit-тесты widgets/Marketplace/* отсутствуют (AC Story 10.2 требует
Jest + Vue Test Utils).
- BarcodeScanner — mock-режим (camera-API / USB не реализованы; AC
UX-DR26 Phase 2). Также используется в Эпике 7 OnSiteDecisionDialog.
- Полный WCAG 2.1 AA audit + axe-core CI — Phase 2 (NFR-A1 MVP
покрыт базово).
- Lighthouse / bundle-size CI gate — Phase 2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- og-image.png — взят brand-постер ~/blago/production/shared/poster-logo-horizontal.png,
отресайзен до 1000px по ширине и центрирован на полотне 1200×630
с белыми краями по вертикали. Шрифт/композиция родные бренда.
- description / og:description / twitter:description: «Цифровой Кооператив —
система управления хозяйством. Регистрация пайщиков, заказы на поставку
и приобретение имущества, собрания совета, общие собрания пайщиков,
взаимные расчёты на блокчейне, автоматический документооборот и бухбаланс
на основе простой электронной подписи.»
CRITICAL:
- service: tx_hash propwroff/createagenda/execwroff раньше нигде не
сохранялся — audit-trail отсутствовал. Теперь extractTxHash и пишется
в decision_log при submitToCouncil (propwroff_tx_hash, createagenda_tx_hash)
и при executeAuthorizedProposal (execwroff_tx_hash per-item).
- service: пустой tx_hash от цепи → fail-fast ConflictException
(submitToCouncil + executeAuthorizedProposal). Раньше silent fall-through.
- service: pre-gate в submitToCouncil — если proposal с тем же hash уже
отправлен в совет, повторный submit отбивается (защита от двойного
клика → дублирующего propwroff + createagenda).
- service: inventory.applyStatusTransition WRITTEN_OFF теперь обязателен —
раньше catch → warn без re-throw; позиция могла остаться
RETURNED_TO_WAREHOUSE при успешном on-chain wroff'е и попасть в
следующий cron-цикл → повторное списание. Теперь re-throw, proposal
остаётся в EXECUTING для retry.
- service: signer fallback на coopname удалён (coopname — аккаунт
кооператива, не пайщик; контракт execwroff требует
branch.is_user_authorized(signer) — coopname этого не пройдёт).
- cron: amount=0 placeholder отбивался validateAndNormalizeItems
(`amount <= 0`) — крон при auto_proposal_enabled=true ВСЕГДА падал.
Теперь крон фильтрует кандидатов без unit_cost; если все без цены —
отправляет напоминание председателю вместо broken DRAFT.
MEDIUM:
- resolver: throw new Error → BadRequestException для «DRAFT-only»
валидации (унификация с остальными resolver'ами).
Known limitations (Phase 2):
- Story 8.1 AC требует ЧЕТЫРЕ типа кандидатов
(RETURNED_TO_WAREHOUSE / EXCESS_RETURNED_TO_WAREHOUSE / без юр.
оформления / expiry_date). Сейчас крон покрывает только expiry_date.
Расширение требует добавить поля age_days/returned_at в inventory.
- Story 8.3 AC «единая Antelope-транзакция NFR-R2» — by design
контракта per-item execwroff (комментарий cpp прямо говорит
«снимает ограничение на максимальный размер протокола»). AC ↔
контракт конфликт — требует переоформления AC.
- Number()/toFixed() float-арифметика на суммах — на больших суммах
возможна потеря точности; нужен переход на BigNumber.
- cycle_started_at.toISOString() hash-mismatch при round-trip через PG
с разной precision timestamptz.
- onCouncilAuthorized synchronously вызывает execute — long-running
в delta-listener'е, риск duplicate execute при retry. Нужен
decoupling через background worker.
Test plan:
- [x] jest marketplace-writeoff — 31 passed.
- [ ] e2e: двойной клик «Подписать и отправить» — второй submit
отбивается ConflictException.
- [ ] e2e: цепь возвращает {} вместо tx — ConflictException, статус
draft остаётся DRAFT.
- [ ] e2e: inventory.applyStatusTransition бросает — proposal в
EXECUTING, retry возможен.
- [ ] e2e: cron при auto=true и пустом unit_cost — reminder вместо
DRAFT'а с amount=0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- og-image.png перерисован: «Цифровой Кооператив» / «Система управления
хозяйством» / «на платформе БЛАГО — кооперативной экономики для жизни» /
«Регистрация пайщиков · заказы на поставку и приобретение имущества ·
расчёты на блокчейне».
- description / og:description / twitter:description выровнены под этот
же копирайт. Слово «взаимоотношения с пайщиками» убрано как слишком
узкое — управление хозяйством включает в себя ещё заказы, реестры
и документы.
CRITICAL:
- resolver: добавлена ownership-проверка `:own`/`:own-KU` для
marketplaceReturnClaim и marketplaceListReturnClaimsByBraname —
matrix capability guard'ом не закрывает data-уровень (matrix явно
делегирует ownership resolver'у).
- service: extractTxHash → fail-fast ConflictException вместо записи
фейкового tx_hash `<action>-<claim.id>` в БД при пустом ответе цепи
(5 мест: submretrn/aprretrem/rejretrem/accretrn/rejretrn). Аналог
фикса PR #7 для signiss1/signiss2.
- service: reason_text лимит 500 → 2000 (соответствие AC Story 7.1).
- service: warranty_until === null → fail-closed (ConflictException
«гарантия не предусмотрена»). Раньше null читался как «без проверки»,
возврат принимался даже на заказы без гарантии.
- frontend SubmitReturnClaimDialog: payload-документ генерируется один
раз вместе с preview (с reason_text + defect_category), подписывается
именно показанный snapshot — устраняет hash-mismatch preview ≠ signed.
HIGH:
- service: assertBranameMatchesClaim — backend проверяет совпадение
input.braname с claim.delivery_braname перед on-chain action.
Раньше председатель КУ-X мог одобрить заявление с delivery=КУ-Y
(контракт проверял только is_user_authorized signer для braname,
не cross-link с return_request).
- service: удалён костыль barcode-валидации
`.includes(order_id.slice(0,8))` — UUID Order'а никогда не пересекается
с EAN-13 inventory; проверка ложно отбивала валидные сканы.
Полноценная сверка с marketplace_inventory — Эпик 5/9.
- service: validatePhotoPayloads — размер фото (10 МБ) валидируется
в service до bucket.put, не полагаясь только на @UseBucket maxBytes.
- service: cleanupBucketPhotos — orphan-фото удаляются из bucket'а
при провале on-chain submit'а (submretrn/accretrn/rejretrn).
- adapter listByDeliveryBraname: default без status возвращает ВСЕ
заявления (включая архив). Раньше default был ACTIVE_STATUSES — это
делало архивную секцию в operator-столе всегда пустой.
Frontend:
- maxlength 500 → 2000 для reason_text в q-input.
Known limitations (Phase 2):
- chairman_account lookup через `coop_ku.chairman_account` (сейчас
fallback `chairmanAccount = delivery_braname` в notifications +
resolver — ownership `:own-KU` приближение).
- Загрузка фото base64-payload одной mutation (AC требует per-file
upload в bucket → image_registry_id). Реальный лимит body-parser
упрётся в ~10MB.
- C++ submretrn не сохраняет delivery_braname в return_request →
cross-branch decision разрешён by design контракта (backend закрывает
ту же дыру service-level проверкой).
- BarcodeScanner в OnSiteDecisionDialog — mock-режим (UX-DR26 Phase 2).
- BarcodeScanner backend-сверка с marketplace_inventory — Эпик 5/9.
- Уведомление председателю шлётся на delivery_braname account
(TODO в notification.service.ts) — Phase 2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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 (новый).
Pug-template передаёт выражения в Vue compiler как строки; конструкции
вида (entry as RailItem).route не превращаются в JS и падают
SyntaxError в браузере: «Unexpected identifier 'as'».
Касты унесены в <script>: добавлены computed flatItems и
normalizedGroups, шаблон работает с уже типизированными массивами без
inline-TS.
- LeftDrawerMenu: импорт useCmdkStore → useCmdkMenuStore (правильное имя).
Без этого ломался ESM build всего drawer'а: «no exported member
useCmdkStore» — RailUserCard не монтировался, поэтому и chevron свёртки
«не работал», и Найти/Пополнить не реагировали.
- AppDrawer.vue, AppHeader.vue, RailUserCard.vue переведены на pug
(lang="pug"). В проекте везде pug — никакого исторического HTML в
canon-компонентах быть не должно.
- 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 (как остальной проект)
- 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, чтобы фон страницы соответствовал шапке и рейлу
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).
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-разметку.
Переписан 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.
Корневая причина «оторванной» галочки в 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 класс.
Quasar по умолчанию даёт ~12–16px padding на .q-checkbox__label, в
compact-форме сохранения ключа это смотрелось «оторвано». Уменьшаем до 4px
через :deep override на классе .rk-form__confirm.
Три кадра в одной секции: LostKey (ввод email), ResetKeyForm check-mail
(уведомление «письмо отправлено»), ResetKeyForm save-key (демонстрация
сохранения ключа с моковым сгенерированным аккаунтом). Submit на третьем
кадре регенерирует мок-ключ для повторной проверки.
Заменяет ошибочную секцию ChangeKey из предыдущей итерации.
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.
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 целиком.
Подключение ChangeKeyForm + ChangeKeySuccessDialog к dev-витрине.
Submit запускает заглушку useChangeKey().changeKey() (≈600 мс), по успеху
открывает диалог с новым WIF. Generate генерирует валидный K1 ключ через
SDK и заполняет оба новых поля. Confirmed сбрасывает форму.
Диалог успеха смены ключа: новый 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.
Форма смены ключа пайщика на новых компонентах дизайн-системы.
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.
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>
Override (padding-top:0 + align-self:center на dense) не отрабатывал —
Quasar в dense держит prefix/suffix по baseline вводимого значения,
и пользователю это устраивает. Убираем неработающий override, чтобы
не мешался в дальнейших правках.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
Промахнулся `git add -A` — захватил worktree-symlink node_modules
(он pnpm-shared с основным репо). .gitignore покрывает node_modules/
но симлинк-файл — отдельный путь.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Контекст: после проброса --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>
- 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>
Между нижним инпутом и 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>
Регрессия мерцания границы и приплюснутости — следствие борьбы наших
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>
- 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>
- 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>
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>
Регрессия после 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>
Корень регрессии: жёсткий 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>
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>
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>
Архитектурный 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>
Истинный источник «прыгалки» 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>
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>
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>
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>
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>
* fix(controller): IS_UNIONED zod-парсер принимает string из .env
z.boolean().default(true) валится для переменной из .env, потому что
process.env всегда отдаёт строку: zod не приводит "true"/"false" к
boolean, в результате validateEnv падает с «IS_UNIONED: параметр не
установлен» и coopback не стартует, если в .env стоит IS_UNIONED=false
(стандартный dev-обход messenger-гейта, см. flow подключения партнёра).
Заменено на string().default('true').transform(v => v === 'true') —
сохранение прежнего default=true и поддержка string-форм из env.
* feat(epic-0): partner onboarding harness — signin → sign agreements → connect
Что добавлено:
- desktop/quasar.config.cjs: vite server.allowedHosts для voskhod-dev/
partner-dev/api-dev (Vite 5.4+ блокирует cross-origin Host без явного
списка — иначе SSR/HMR-сервер возвращает 403 «Blocked request»).
- docs-harness/lib/harness.mjs: helper signOnboardingAgreements —
реальная подпись каскада SignAgreementDialog (wallet/signature/
privacy/user), каждый клик отправляет sendAgreement → wallet::signagree
on-chain. В отличие от dismissOnboardingDialogs делает on-chain эффект
(см. inc 2026-05-18: stale vault SERVER_SECRET → bad decrypt).
- scenarios/onboarding/01..07: новый partner-flow Эпика 0 — partner1
заходит в Восход, подписывает 4 типовых соглашения, идёт на «Подключение»,
ant одобряет, наблюдаем установку partner-dev.
- scenarios/registration/01..04: архив старого registration-doc как
отдельная история (см. mem feedback_provider_mono_pr_flow).
- scripts/debug-*: вспомогательные скрипты для портал-структуры и
chairman onboarding'а.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(docs-harness/08): chairman install wizard на partner-dev
Сценарий 08 проходит установочный wizard /:coopname/install целиком:
шаг 1 (RequestKeyForm: WIF partner1 из state/cooperatives/partner1.json),
шаг 2 (SetInitForm: readonly orgdata из is_server_init=true, «Далее»),
шаг 3 (SetSovietForm: один председатель — Иванов И.И.),
шаг 4 (SetVariablesForm: ОПФ+ во всех падежах, устав, конф.email).
Финальный submit «Завершить установку» сейчас падает on-chain в
soviet::create — `assertion: Один из аккаунтов не найден в реестре
пайщиков`. Причина: в install.interactor.ts adduser и createBoard
шлются двумя отдельными tx; partner1 nodeos не producer, между tx есть
лаг p2p-репликации, createBoard приходит до того как participants[N]
обновился. Шот 06-error-state снимается при таймауте; шот
06-completed появится после фикса race в install.interactor (отдельный
коммит).
Сценарий запускается:
BASE_URL=https://partner-dev.coopenomics.world COOPNAME=partner1 \
node run.mjs onboarding/08-chairman-install-on-partner-dev
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(controller/install): bundle adduser+createBoard в одну tx
Cause. install.interactor.ts шлёт adduser×N и createBoard двумя отдельными
tx через BlockchainService. На production-нодах с producer-схемой это
работает (lag реплицирования минимальный), но на dev-loop'е partner-
coopback соединён со своим nodeos, который p2p-репликой подтягивает
блоки от producer'а — после accept'а adduser в local state ещё нет
soviet::participants[username] к моменту push'а createBoard. Контракт
soviet::createboard падает «Один из аккаунтов не найден в реестре
пайщиков».
Fix. Объединил все adduser-action'ы и createBoard-action в одну tx
через новый метод BlockchainPort.installSoviet(). Обе action'ы теперь
атомарны в одном блоке — soviet::addpartcpnt (inline action от
adduser) обновляет participants и createBoard видит запись сразу же
в том же блоке.
Поток в install.interactor.ts перестроен в два шага:
1. Цикл по soviet: createUser в БД + setupNotificationSubscriber +
сбор addUserActions[] и members[] (без on-chain активности).
2. installSoviet(addUserActions, createBoardData) — одна tx.
Catch при ошибке on-chain (как и раньше) откатывает users из БД.
Зачем. Закрывает блокер сценария 08-chairman-install-on-partner-dev:
без этого финальный экран wizard'а «Установка завершена» недостижим
на dev-loop'е (Эпик 0 не закрывается).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(docs-harness/09): invite-token → WIF → signin chairman
Закрывает Эпик 0: после сценария 08 (chairman install wizard) на
partner1 в Postgres появляется invite-токен председателя, но Novu не
доставит его на @example.com адрес. Сценарий 09 идёт за токеном
напрямую в БД через `ssh partner1 → docker exec postgres → psql` и
прогоняет до финального signin под новым ключом.
Шаги:
1. fetchLatestInviteToken — SELECT token FROM tokens WHERE
type='invite' AND blacklisted=false AND expires > NOW() LIMIT 1.
2. Открываем `${BASE_URL}/${COOPNAME}/auth/invite?token=<token>` —
widget Invite.vue клиентски генерирует новый WIF (generateAccount).
3. Извлекаем WIF из q-input, сохраняем в
state/cooperatives/partner1-chairman.json для последующих сценариев.
4. Чекбокс «Я сохранил ключ» → «Установить ключ» → resetKey шлёт
on-chain ChangeKey, фронт редиректит на /auth/signin.
5. Финальный signin под chairman.partner1@example.com + новый WIF;
ждём перехода на /chairman или /participant.
Запускается:
BASE_URL=https://partner-dev.coopenomics.world COOPNAME=partner1 \
node run.mjs onboarding/09-chairman-key-and-login
Требует SSH-доступа к partner1 (PARTNER_SSH=user1@91.218.246.46 по
умолчанию) и предварительно прогнанный сценарий 08 (после merge
fix(controller/install) — иначе токен не появится).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(controller/install): откатить installSoviet bundle на sleep 2s
Bundle adduser×N + createBoard в одну tx работает, но требует расширения
BlockchainPort и больше read'а; для не-producer-нод (dev-loop) достаточно
короткой паузы между adduser и createBoard, чтобы p2p-реплика подтянула
блок с participants. 2с гарантированно перекрывают и prod (~50ms), и
dev-loop (1-3с).
Отменён 3f2ecc42cf (installSoviet в blockchain.port + blockchain.service +
install.interactor), добавлен `await new Promise(setTimeout, 2000)` между
циклом adduser и createBoard.
Это однострочный фикс race-condition; bundle вернётся когда понадобится
многошаговая атомарность.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(desktop+harness): SPA mode dev + q-checkbox селектор
- desktop dev по умолчанию в SPA — SSR грузит сервер на 70%+ CPU и ломает
reverse-proxy HTTP/2 multiplex'ом параллельных модулей /src/*. Старый
режим доступен как `dev:ssr`. docker-compose зовёт `dev` — без правок.
- harness 09: Quasar q-checkbox рендерит label в div, не <label>; кликаем
по `.q-checkbox:has-text(...)`.
* feat(boot:extra): засев partner1 как coop-пайщика в воскход + announce=domain
В installExtraData (reboot:extra) теперь регистрируется partner1 как
готовый coop-пайщик воскхода со status=active, чтобы dev-стенд после
reboot:extra сразу позволял запустить полный E2E прогон Эпика 0
(provider order_instance → Hostkey rent → playbook → coopback healthy
→ provider initSystem → chairman wizard).
Flow засева:
newaccount → reguser(type=organization) → regcoop → stcoopstatus(active)
+ Mongo: organization + paymentMethod (bank_account)
+ Postgres: users.partner1{type:organization, role:user, status:joined}
В CooperativeClass — `announce` теперь хранит доменное имя
('voskhod-dev.coopenomics.world'), не текст. Provider читает announce
как domain для DomainDelegationPollingService (regex→is_valid,
health-check L7-Proxy 8880→is_delegated). См. memory
reference_provider_announce_domain.
Без этих правок:
- provider не подхватывает partner1 в свою DB после reboot:extra
- harness 08 после initSystem видит «Unsupported account type:
undefined» — partner1 organization_data отсутствуют в воскход'е
- announce='Тестовый кооператив' не парсится как domain → is_valid=false
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(boot:extra): partner1 reuse WIF из docs-harness фикстуры
В installExtraData boot:extra на каждом запуске генерировал случайный
WIF для partner1 через generateKeypair(undefined). harness 08
(chairman wizard /partner1/install) брал WIF из docs-harness/state/
cooperatives/partner1.json (засеян однократно в репозиторий) — после
reboot:extra on-chain active key уже другой, startInstall mutation на
partner-coopback'е не принимает ключ, wizard зависает на шаге 1 без
видимой ошибки. ACTIVE+initialized провайдером проходит, но
installSystem (создание совета + рассылка инвайт-токенов) не
наступает → harness 09 не может получить WIF chairman'а.
Фикс: если фикстура есть, читаем wif (опц. publicKey, иначе деривим
ecc.privateToPublic) и передаём как seededKeys в generateKeypair —
получается детерминированный partner1 active key, синхронный с
harness'ом. Иначе fallback на старое поведение (рандомный ключ).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: coopops <coopops@coopenomics.world>
После переезда контрактов в cpp/ (коммит 2b6b56a, март 2025) паттерны
в components/contracts/.gitignore ссылались на старые пути
(soviet/soviet.wasm и т.п.) и не матчились. В итоге собранные
.wasm/.abi уходили в репозиторий.
— переписаны паттерны на cpp/**/*.{wasm,abi}
— тестовые фикстуры в cpp/tests/test_contracts/** оставлены трекаться
— git rm --cached для soviet.{wasm,abi} и starter.{wasm,abi}
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>
Перенёс из общей auto-memory: stack, worktree policy (mono-ai-1 vs mono-ai-4), PR-flow, локальные тесты, GraphQL/desktop каноны, платформенные паттерны (роли, agreements, трёхуровневый онбординг), SDK login, vault & SERVER_SECRET, Capital фиксы, Стол Заказов MVP. Часть реорганизации памяти агента по проектам; в mono-ai-2..5 файл подцепляется локальными симлинками без отдельного коммита (попадёт в feature-ветки при следующем merge от dev).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Благорост — капитализация РИД (не «накопления на жильё»), Генератор —
генерация РИД (не «доходы от программы»). Кошелёк остаётся «свободный
остаток». Черновые ярлыки риском попадают в production через копипасту.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Канонический .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>
* chore(release): publish
* chore(release): publish
* ci: атомарный release.yaml, убрать workflow_run-связку (#366)
build-contracts + build-containers через workflow_run упёрлись в:
(а) default-branch caveat (новая логика не активна, пока не в main),
(б) `${{ github.event.workflow_run.head_sha }}` иногда пуст в YAML-
выражениях — описание см. в шаге Resolve tag, инцидент v2026.5.14
не дёрнул PRODUCTION_WEBHOOK_URL.
Замена — один `release.yaml` на push тэга `v*`: резолвит ветку через
`git branch --contains`, собирает контракты → пушит
`dicoop/contracts:<branch>`, собирает базу + сервисные образы →
пушит `dicoop/<svc>:<tag>`, шлёт webhook. Гонок нет by construction.
`build-contracts.yaml` остаётся только на push веток для CI-
обновления `dicoop/contracts:dev|testnet|main` без релизного тэга.
Триггер `tags: ['*']` и логика резолва ветки через --contains
оттуда удалены — это теперь забота release.yaml.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* chore(release): publish
* chore(release): publish
* ci: tag-only триггеры для build-contracts и docs (#367)
build-contracts.yaml — только workflow_dispatch (ручная пересборка
`dicoop/contracts:<branch>` для отладки на dev-ноде). Тэги обрабатывает
release.yaml атомарно (контракты + контейнеры + webhook), отдельная
сборка по push'у в ветку только давала вторую параллельную сборку.
publish-docs.yaml и build-contracts-docs.yaml — на push:tags v* с
gate-job'ом, пропускающим только продакшн-тэги (без -alpha/-beta/-rc/
-test) на main. Раньше docs пересобирались на каждый push в
main/testnet/dev/reports/marketplace2 — впустую.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(capital/commit): nullable description/meta в CommitOutputDTO + не дефолтить satisfaction в 5
Если syncCommit не дождался delta из блокчейна, interactor возвращает DB-only
entity, где description/meta остаются undefined — non-nullable GraphQL field
ломал ответ мутации capitalCreateCommit. Сделал оба поля nullable.
UI CreateCommitButton.vue: satisfaction_stars=0 по умолчанию, label "не указано"
пока пользователь не выбрал; блок contribution_feedback в payload только если
stars >= 1 или review_text непустой.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix commit process
* chore(release): publish
* fix(file-storage): MinIO стартует только при заданном MINIO_ENDPOINT
Раньше MINIO_ENDPOINT имел default http://minio:9000, и на проде без
minio-контейнера контроллер падал на bootstrap в HeadBucket с
getaddrinfo ENOTFOUND minio — Nest application не поднимался вообще.
Теперь MINIO_ENDPOINT optional без default; адаптер хранит enabled-флаг
по наличию endpoint и при отсутствии — onApplicationBootstrap логирует
warning и возвращается без сетевых вызовов. Любая попытка getBucket /
fetchObjectForReadProxy кидает InterFileStorageBackendUnavailableError
с понятным сообщением.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(release): publish
* chore(release): publish
* fix(capital/time-tracking): личные доли estimate + revert при decline (#387)
* chore(release): publish
* chore(release): publish
* ci: атомарный release.yaml, убрать workflow_run-связку (#366)
build-contracts + build-containers через workflow_run упёрлись в:
(а) default-branch caveat (новая логика не активна, пока не в main),
(б) `${{ github.event.workflow_run.head_sha }}` иногда пуст в YAML-
выражениях — описание см. в шаге Resolve tag, инцидент v2026.5.14
не дёрнул PRODUCTION_WEBHOOK_URL.
Замена — один `release.yaml` на push тэга `v*`: резолвит ветку через
`git branch --contains`, собирает контракты → пушит
`dicoop/contracts:<branch>`, собирает базу + сервисные образы →
пушит `dicoop/<svc>:<tag>`, шлёт webhook. Гонок нет by construction.
`build-contracts.yaml` остаётся только на push веток для CI-
обновления `dicoop/contracts:dev|testnet|main` без релизного тэга.
Триггер `tags: ['*']` и логика резолва ветки через --contains
оттуда удалены — это теперь забота release.yaml.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* chore(release): publish
* chore(release): publish
* ci: tag-only триггеры для build-contracts и docs (#367)
build-contracts.yaml — только workflow_dispatch (ручная пересборка
`dicoop/contracts:<branch>` для отладки на dev-ноде). Тэги обрабатывает
release.yaml атомарно (контракты + контейнеры + webhook), отдельная
сборка по push'у в ветку только давала вторую параллельную сборку.
publish-docs.yaml и build-contracts-docs.yaml — на push:tags v* с
gate-job'ом, пропускающим только продакшн-тэги (без -alpha/-beta/-rc/
-test) на main. Раньше docs пересобирались на каждый push в
main/testnet/dev/reports/marketplace2 — впустую.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(capital/time-tracking): личные доли estimate, partial-split, revert при decline
Три бага в распределении билетов времени, вскрытые на прод-инциденте voskhod
(проект CC7-1 «Концепция», estimate=15 ч, 3 creators):
БАГ #1 — recalcDoneEstimatesForContributorProject раздавал «общий остаток пула»
(estimate − committed_total) / N всем creators поровну. Закоммитивший свою долю
получал её ещё раз, остальные — урезанную (15/3=5 → после committed 5 у одного
становилось 10/3=3.33 у каждого, включая того кто уже закоммитил).
Фикс: личная доля = estimate/N − собственный committed estimate. Введён общий
helper redistributeIssueEstimateEntries, который используют и applyExplicit-
EstimateToTimeEntries (force=true), и recalcDoneEstimates (force=false с
no-op оптимизацией если раскладка уже совпадает с планом).
БАГ #2 — commitTime при partial split (entry.hours > requested) создавал новую
committed-запись без entry_type и estimate_snapshot. По default'у БД сохраняла
её как entry_type='hourly', что ломало последующий recalc (он фильтрует только
entry_type='estimate'). Фикс: явно копировать entry_type и estimate_snapshot
из оригинального entry.
БАГ #3 — declineCommit / handleDeclineCommit меняли только commit.status в БД,
но не возвращали time-entries в is_committed=false. После отказа мастера часы
оставались в total_committed_hours и не возвращались в available_hours. Фикс:
новый метод revertEntriesForDeclinedCommit в TimeTrackingInteractor + методы
findCommittedByCommitHash / revertCommittedEntriesByCommitHash в TimeEntry-
Repository. После revert делается force=true redistribute для затронутых DONE
задач, чтобы доли вернулись в норму.
Покрытие тестами: 17 unit-тестов в time-tracking.interactor.spec.ts с
регрессионными сценариями под каждый из трёх багов плюс integration-сценарий
полного lifecycle CC7-1 (estimate → коммит → decline → revert).
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix commit process
* chore(release): publish
* chore(release): publish
* feat(controller): is_server_init флаг в initSystem для provider-overwrite (#391)
* feat(docs-harness): onboarding 01..06 — визуальная цепочка регистрация → активация
Шесть сценариев visual docs-harness, покрывающих полный путь подключения
нового кооператива через провайдера Восход:
01-register-coop — регистрация кооператива-клиента
02-sign-and-submit — подпись заявления о вступлении + PayInitial
03-operator-approve — chairman принимает заявку в реестре одобрений
04-sign-connection-agreement — Партнёр-1 видит ConnectionAgreementStepper
(на текущем стенде получаем заглушку
/signup, пока пайщик не принят)
05-activate-from-registry — оператор открывает карточку инстанса
в provider-frontend, выбирает preset
06-wait-instance-active — pending → ACTIVE (overrideInstance
для имитации финального статуса в шоте)
Обвязка:
• lib/registrator-signup.mjs — переиспользуемый helper подписания.
• lib/harness.mjs — расширенный dismissOnboardingDialogs (Положение ЦПП
и связанные модалки chairman'а).
• state/cooperatives/{,.gitkeep} — папка для фикстур; partner1.json
(с приватным wif) игнорируется (.gitignore обновлён).
ОГРАНИЧЕНИЕ: в 06 финальный ACTIVE — это playwright-override JSON, а не
реальный POST /instances/activate с боевым Ansible до testnet300.coopenomics.world.
Реальный E2E (аренда VM на Hostkey + поднятие кооператива на домене)
будет следующим шагом Эпика 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(controller): is_server_init flag в initSystem для разблокировки provider-overwrite
Поле data.is_server_init: boolean (optional) в InitDTO + домейн-интерфейсе.
Если true (вызов от провайдера через server-secret) — coopback ставит
init_by_server=true безусловно, даже если до этого пользователь успел
заполнить визард первым (user-init). Это разблокирует ситуацию, когда
провайдер не успел вызвать initSystem до того как chairman открыл
/install — следующий callInitSystemMutation от провайдера перезапишет
organization_data и пометит её readonly для визарда.
Без флага сохраняется прежняя логика: первая инициализация — серверная,
повторная наследует флаг.
Инцидент 2026-05-18 на partner1: при первой установке provider вообще
не успел/не сходил в callInitSystemMutation, визард пользователя
проинициализировал систему как user-init (init_by_server=false), визард
2-го захода не предзаполнил форму. С этим фиксом следующий вызов
provider'а поднимет флаг и фикстура встанет на место.
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(capital/convert): унифицировать шаблон 1080, убрать 1081/1082 (#392)
* refactor(capital/convert): унифицировать 1080 как универсальное заявление о конвертации, убрать 1081/1082
Шаблон 1080 (GenerationConvertStatement) уже технически универсален — содержит
обе суммы (main_wallet_amount/blagorost_wallet_amount) и условные блоки в
context. Шаблоны 1081 (GenerationToProjectConvertStatement) и 1082
(GenerationToCapitalizationConvertStatement) были недоделанными заглушками
без полей и нигде не подключены в UI.
Изменения:
- cooptypes: 1080 переименован GenerationToMainWalletConvertStatement →
GenerationConvertStatement; title/description нейтральные. 1081/1082 удалены.
- factory: Template + Action 1080 переименованы; в Action добавлено
super.formatAsset(...) для обеих сумм (как в Action 1020). 1081/1082 удалены.
- controller: DTO переименован; appendix_hash убран из generate-input и
перенесён в signed-meta-input; добавлен enrich appendix_hash через
AppendixRepository.findConfirmedByUsernameAndProjectHash в
DistributionManagementInteractor.prepareGenerationConvertStatementData
(по образцу InvestsManagementInteractor для 1020). Резолвер мутации
переименован в capitalGenerateGenerationConvertStatement; убраны два
резолвера 1081/1082. mutation-log-mapper обновлён.
- sdk: мутация переименована, две удалены, zeus regenerated.
- desktop: Distribution-фичи 1081/1082 удалены, 1080-фича переименована.
ConvertSegment теперь шлёт project_hash + обе суммы (formatToEosioAsset) +
to_wallet/to_blagorost; appendix_hash подтягивается на бекенде.
- controller schema.gql regenerated.
registry_id=1080 не меняется — on-chain контракт convertsegm не сверяет
registry_id, миграций БД/контракта не требуется.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(capital/convert): применить ревью — title «трансляция паевого взноса из программы Генерация»
По комментарию ревью в PR #392 (строка 37): принять доменный термин
«трансляция паевого взноса» (перенос между программами) вместо
«конвертация»; description согласован в том же стиле.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(capital): подтянуть 3 ссылки на GenerationConvertStatement → MoneyInvestStatement (#394)
PR #392 переименовал Cooperative.Registry.GenerationConvertStatement в
GenerationMoneyInvestStatement в @coopenomics/document, но в controller
осталось 3 несинхронизированные ссылки:
- distribution-management.service.ts:56 — registry_id метода generation
- distribution-management.interactor.ts:48,66 — тип Action в return и as-cast
- generation-convert-statement-document.dto.ts:12 — type action в DTO
Coopback падал на ts-node compile (TS2551/2724), что блокировало старт
всего dev-stack после reboot dev-chain 2026-05-18.
Co-authored-by: coopops <coopos@coopenomics.world>
* fix(controller): IS_UNIONED zod-парсер принимает string из .env (#395)
z.boolean().default(true) валится для переменной из .env, потому что
process.env всегда отдаёт строку: zod не приводит "true"/"false" к
boolean, в результате validateEnv падает с «IS_UNIONED: параметр не
установлен» и coopback не стартует, если в .env стоит IS_UNIONED=false
(стандартный dev-обход messenger-гейта, см. flow подключения партнёра).
Заменено на string().default('true').transform(v => v === 'true') —
сохранение прежнего default=true и поддержка string-форм из env.
Co-authored-by: coopops <coopos@coopenomics.world>
* fix(capital): revert ошибочной замены GenerationConvertStatement → MoneyInvestStatement (#394) (#396)
PR #394 «починил» TS-ошибки coopback заменой типа `Cooperative.Registry.GenerationConvertStatement`
на `GenerationMoneyInvestStatement` в 3 файлах controller'а. Это семантически неверно:
- 1080 GenerationConvertStatement — заявление о трансляции паевого взноса
(поля: project_hash, main_wallet_amount, blagorost_wallet_amount, to_wallet, to_blagorost, appendix_hash)
- 1020 GenerationMoneyInvestStatement — заявление о денежном паевом взносе по программе Генерация
(совсем другой набор полей)
DTO BaseGenerationConvertStatementMetaDocumentInputDTO декларирует поля 1080, но `implements ExcludeCommonProps<action>`
где action = тип 1020 → TS2352 на as-cast в interactor, потому что Action'ы не пересекаются по полям.
Реальная причина исходных ошибок coopback после #392 — несвежий dist `@coopenomics/cooptypes`
на dev-узле (старое имя символа). Лечится пересборкой пакета, не переименованием ссылок.
Изменено (откат #394):
- generation-convert-statement-document.dto.ts:12 — `action = ...GenerationConvertStatement.Action`
- distribution-management.service.ts:56 — `registry_id: ...GenerationConvertStatement.registry_id`
- distribution-management.interactor.ts:48,66 — `Promise<...GenerationConvertStatement.Action>`
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(capital/convertsegm): регистрировать convert_statement в реестре документов как completed (#403)
Финальная фаза процесса p.cap.rid (convertsegm) принимала document2 convert_statement (шаблон 1080)
параметром, но:
1. **Не проверяла подпись** — `verify_document_or_fail` отсутствовала, поэтому on-chain
принял бы любую сконструированную document2 без валидной user-подписи.
У signact1/signact2 (соседние фазы того же процесса) verify есть — здесь забыли.
2. **Не регистрировала документ в реестре** — `newlink`/`newsubmitted`/`newresolved`
не вызывался, заявление о трансляции 1080 «терялось»: off-chain controller (process_instance)
не видел финальный документ привязанным к result_hash, процесс p.cap.rid не помечался completed.
У pushrslt (create_approval) и signact2 (newlink с SIGN_ACT2_RESULT) линковка есть.
Канон есть в soviet/src/system/converttoaxn.cpp:54 и soviet/src/agreement/sndagreement.cpp:104:
паттерн `Soviet::make_complete_document(calling_contract, coopname, username, action, package_hash, document)`
шлёт newsubmitted + newresolved одной парой, package = анкер процесса (здесь result_hash).
Изменено:
- names.hpp: новая константа Names::Capital::CONVERT_SEGMENT = "convertsegm"_n (12 символов)
- convertsegm.cpp:
- verify_document_or_fail(convert_statement, {username}) сразу после require_auth
- Soviet::make_complete_document(...) ДО delete_result (чтобы линковка прошла, пока result_hash ещё анкер)
Контракт capital собирается без ошибок (cdt-cpp testnet mode), warnings — старые ricardian.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(epic-0): onboarding harness 01-08 + install bundle adduser+createBoard в одну tx (#398)
* fix(controller): IS_UNIONED zod-парсер принимает string из .env
z.boolean().default(true) валится для переменной из .env, потому что
process.env всегда отдаёт строку: zod не приводит "true"/"false" к
boolean, в результате validateEnv падает с «IS_UNIONED: параметр не
установлен» и coopback не стартует, если в .env стоит IS_UNIONED=false
(стандартный dev-обход messenger-гейта, см. flow подключения партнёра).
Заменено на string().default('true').transform(v => v === 'true') —
сохранение прежнего default=true и поддержка string-форм из env.
* feat(epic-0): partner onboarding harness — signin → sign agreements → connect
Что добавлено:
- desktop/quasar.config.cjs: vite server.allowedHosts для voskhod-dev/
partner-dev/api-dev (Vite 5.4+ блокирует cross-origin Host без явного
списка — иначе SSR/HMR-сервер возвращает 403 «Blocked request»).
- docs-harness/lib/harness.mjs: helper signOnboardingAgreements —
реальная подпись каскада SignAgreementDialog (wallet/signature/
privacy/user), каждый клик отправляет sendAgreement → wallet::signagree
on-chain. В отличие от dismissOnboardingDialogs делает on-chain эффект
(см. inc 2026-05-18: stale vault SERVER_SECRET → bad decrypt).
- scenarios/onboarding/01..07: новый partner-flow Эпика 0 — partner1
заходит в Восход, подписывает 4 типовых соглашения, идёт на «Подключение»,
ant одобряет, наблюдаем установку partner-dev.
- scenarios/registration/01..04: архив старого registration-doc как
отдельная история (см. mem feedback_provider_mono_pr_flow).
- scripts/debug-*: вспомогательные скрипты для портал-структуры и
chairman onboarding'а.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(docs-harness/08): chairman install wizard на partner-dev
Сценарий 08 проходит установочный wizard /:coopname/install целиком:
шаг 1 (RequestKeyForm: WIF partner1 из state/cooperatives/partner1.json),
шаг 2 (SetInitForm: readonly orgdata из is_server_init=true, «Далее»),
шаг 3 (SetSovietForm: один председатель — Иванов И.И.),
шаг 4 (SetVariablesForm: ОПФ+ во всех падежах, устав, конф.email).
Финальный submit «Завершить установку» сейчас падает on-chain в
soviet::create — `assertion: Один из аккаунтов не найден в реестре
пайщиков`. Причина: в install.interactor.ts adduser и createBoard
шлются двумя отдельными tx; partner1 nodeos не producer, между tx есть
лаг p2p-репликации, createBoard приходит до того как participants[N]
обновился. Шот 06-error-state снимается при таймауте; шот
06-completed появится после фикса race в install.interactor (отдельный
коммит).
Сценарий запускается:
BASE_URL=https://partner-dev.coopenomics.world COOPNAME=partner1 \
node run.mjs onboarding/08-chairman-install-on-partner-dev
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(controller/install): bundle adduser+createBoard в одну tx
Cause. install.interactor.ts шлёт adduser×N и createBoard двумя отдельными
tx через BlockchainService. На production-нодах с producer-схемой это
работает (lag реплицирования минимальный), но на dev-loop'е partner-
coopback соединён со своим nodeos, который p2p-репликой подтягивает
блоки от producer'а — после accept'а adduser в local state ещё нет
soviet::participants[username] к моменту push'а createBoard. Контракт
soviet::createboard падает «Один из аккаунтов не найден в реестре
пайщиков».
Fix. Объединил все adduser-action'ы и createBoard-action в одну tx
через новый метод BlockchainPort.installSoviet(). Обе action'ы теперь
атомарны в одном блоке — soviet::addpartcpnt (inline action от
adduser) обновляет participants и createBoard видит запись сразу же
в том же блоке.
Поток в install.interactor.ts перестроен в два шага:
1. Цикл по soviet: createUser в БД + setupNotificationSubscriber +
сбор addUserActions[] и members[] (без on-chain активности).
2. installSoviet(addUserActions, createBoardData) — одна tx.
Catch при ошибке on-chain (как и раньше) откатывает users из БД.
Зачем. Закрывает блокер сценария 08-chairman-install-on-partner-dev:
без этого финальный экран wizard'а «Установка завершена» недостижим
на dev-loop'е (Эпик 0 не закрывается).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(docs-harness/09): invite-token → WIF → signin chairman
Закрывает Эпик 0: после сценария 08 (chairman install wizard) на
partner1 в Postgres появляется invite-токен председателя, но Novu не
доставит его на @example.com адрес. Сценарий 09 идёт за токеном
напрямую в БД через `ssh partner1 → docker exec postgres → psql` и
прогоняет до финального signin под новым ключом.
Шаги:
1. fetchLatestInviteToken — SELECT token FROM tokens WHERE
type='invite' AND blacklisted=false AND expires > NOW() LIMIT 1.
2. Открываем `${BASE_URL}/${COOPNAME}/auth/invite?token=<token>` —
widget Invite.vue клиентски генерирует новый WIF (generateAccount).
3. Извлекаем WIF из q-input, сохраняем в
state/cooperatives/partner1-chairman.json для последующих сценариев.
4. Чекбокс «Я сохранил ключ» → «Установить ключ» → resetKey шлёт
on-chain ChangeKey, фронт редиректит на /auth/signin.
5. Финальный signin под chairman.partner1@example.com + новый WIF;
ждём перехода на /chairman или /participant.
Запускается:
BASE_URL=https://partner-dev.coopenomics.world COOPNAME=partner1 \
node run.mjs onboarding/09-chairman-key-and-login
Требует SSH-доступа к partner1 (PARTNER_SSH=user1@91.218.246.46 по
умолчанию) и предварительно прогнанный сценарий 08 (после merge
fix(controller/install) — иначе токен не появится).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(controller/install): откатить installSoviet bundle на sleep 2s
Bundle adduser×N + createBoard в одну tx работает, но требует расширения
BlockchainPort и больше read'а; для не-producer-нод (dev-loop) достаточно
короткой паузы между adduser и createBoard, чтобы p2p-реплика подтянула
блок с participants. 2с гарантированно перекрывают и prod (~50ms), и
dev-loop (1-3с).
Отменён 3f2ecc42cf (installSoviet в blockchain.port + blockchain.service +
install.interactor), добавлен `await new Promise(setTimeout, 2000)` между
циклом adduser и createBoard.
Это однострочный фикс race-condition; bundle вернётся когда понадобится
многошаговая атомарность.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: coopops <coopops@coopenomics.world>
* fix(asset-formatting): truncate (toward-zero) до 2 знаков вместо math-round (#404)
UI показывал баланс 99999.9999 RUB как 100 000,00 — пользователь видел
больше, чем реально на кошельке, и попытка инвестировать «весь баланс»
падала в walletop TRANSFER с insufficient funds (precision=4 на цепи
vs precision=2 в UI).
Принцип: «никогда не показывать сумму больше реальной». Truncate
выполняется на уровне строки, без преобразования в Number, чтобы
избежать FP-погрешности (0.29 * 100 = 28.999... в IEEE 754).
Изменения:
- desktop: новая утилита floorDecimalString — единый источник истины.
- desktop: formatAsset2Digits / formatToAsset / addAssets используют floor.
- desktop: убран double-formatting amount.toFixed(4) перед formatAsset2Digits
в ParticipantWalletsPage.
- factory: Factory.formatAsset / Factory.formatShare через приватный
truncateDecimal — тот же принцип в документах (заявления, акты).
Не затронуто (отдельный фолоу-ап): 10 файлов с inline .toFixed(4)
перед отправкой в контракт — рекомендую заменить на formatToAsset().
Co-authored-by: coopops <coopos@coopenomics.world>
* feat(capital): требовать допуск для просмотра артефактов проекта/компонента (#407)
Любой авторизованный пользователь раньше мог запросить capitalStories /
capitalStory и получить требования всех проектов кооператива. Закрываем эту
лазейку на бекенде и показываем аккуратную заглушку на фронте.
Backend (controller):
- GenerationService.getStories: фильтрует список разрешённых project_hash
через PermissionsService. Допуск к корневому проекту каскадно открывает
все его компоненты; допуск к одному компоненту — только этот компонент.
Запрос без project_hash/issue_hash доступен только chairman/member.
- GenerationService.getStoryByHash: проверяет допуск через project_hash
(или issue.project_hash для story с issue_hash) и возвращает null без
доступа.
- getCapitalStories / getCapitalStory резолверы теперь получают currentUser.
- ProjectPermissionsOutputDTO: добавлены has_parent_clearance и
can_view_artifacts — для отображения заглушек на фронте.
Frontend (desktop):
- ProjectRequirementsPage / ComponentRequirementsPage: показывают
ArtifactsAccessPlaceholder когда can_view_artifacts=false, с кнопкой
получения допуска (или PendingClearanceButton при поданном запросе).
- Новый shared компонент ArtifactsAccessPlaceholder в стиле остальных
заглушек расширения capital.
SDK:
- projectSelector обновлён под новые поля; schema.gql / zeus
регенерированы.
Co-authored-by: coopops <coopos@coopenomics.world>
* fix(capital/convertsegm): линковать 1080 к пакету через newlink, не make_complete_document (#409)
PR #403 использовал Soviet::make_complete_document — это вытащило заявление о трансляции (1080)
на верхний уровень реестра как самостоятельный документ. В разделе «Документы» 1080 оказался
отдельной строкой, под ним болтались протокол+акт из соседнего пакета (агрегатор подтягивал их
по тому же result_hash без главного документа).
Правильно — линковать 1080 к существующему пакету процесса p.cap.rid, где ведущим документом
является заявление о внесении РИД (1040, statement из pushrslt). Канон уже есть в signact1/signact2:
Action::send<newlink_interface>(_soviet, "newlink"_n, _capital, coopname, username,
Names::Capital::SIGN_ACT*_RESULT, result_hash, act);
Здесь по тому же паттерну, action = Names::Capital::CONVERT_SEGMENT (константа уже в dev из #403,
её не трогаем), package = result_hash. Off-chain controller добавит 1080 в группу пакета 1040,
а не на верхний уровень.
verify_document_or_fail оставлен — это безопасность, не связана с проблемой реестра.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* migration complete
* [598-14][@ant] fix(desktop): vue-tsc на marketplace2 после merge'ов
— OperatorInventoryLabelingPage: BarcodeStrategy/Format через Zeus.MarketplaceBarcode*
— OperatorOwnWarehousePage: pug-tернарник в одну строку (vue-tsc не парсит мультиline | text-block)
— AdminWriteoffsPage: statuses[] через Zeus.MarketplaceWriteoffProposalStatus, trigger === CRON
— SubmitToCouncilDialog: useSessionStore вместо удалённого useStore
— ProcessesPage: ProcessDocument → { hash, source.{code,table,...} } + sortOrder в PaginationInput
* [598-14][@ant] fix(controller): DI marketplace модулей чтобы controller стартовал на marketplace2
— application/marketplace-application.module: импортирует MarketplaceInfrastructureModule (резолверы инжектят MARKETPLACE_*_REPOSITORY) и AccountInfrastructureModule (MarketplaceNotificationService требует ACCOUNT_DATA_PORT). Импорт ExtensionDomainModule удалён — порождал циркуляр AppModule → ExtensionDomainModule → ExtensionsModule → MarketplacePluginModule → MarketplaceExtensionApplicationModule, из-за чего Nest падал с "module at index [6] is undefined" сразу после merge Эпика 8.
— marketplace-writeoff-cron.service: ExtensionDomainService инжектится через @Optional() с null-fallback (cron сам читает env при отсутствии extension config). Это убирает необходимость в проблемном импорте ExtensionDomainModule.
— infrastructure/marketplace-infrastructure.module: импорт VaultDomainModule, потому что MarketplaceCanonicalBlockchainAdapter инжектит VAULT_DOMAIN_SERVICE.
Также добавлен MINIO_ENDPOINT в controller/.env (Эпик 7 / Story 7.1 stol-zakazov:images bucket теперь корректно поднимается на dev).
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: coopops <coopops@coopenomics.world>
Co-authored-by: ant <ant@noreply.local>
Анимация 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>
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>
По фидбеку:
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>
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>
PR #403 использовал Soviet::make_complete_document — это вытащило заявление о трансляции (1080)
на верхний уровень реестра как самостоятельный документ. В разделе «Документы» 1080 оказался
отдельной строкой, под ним болтались протокол+акт из соседнего пакета (агрегатор подтягивал их
по тому же result_hash без главного документа).
Правильно — линковать 1080 к существующему пакету процесса p.cap.rid, где ведущим документом
является заявление о внесении РИД (1040, statement из pushrslt). Канон уже есть в signact1/signact2:
Action::send<newlink_interface>(_soviet, "newlink"_n, _capital, coopname, username,
Names::Capital::SIGN_ACT*_RESULT, result_hash, act);
Здесь по тому же паттерну, action = Names::Capital::CONVERT_SEGMENT (константа уже в dev из #403,
её не трогаем), package = result_hash. Off-chain controller добавит 1080 в группу пакета 1040,
а не на верхний уровень.
verify_document_or_fail оставлен — это безопасность, не связана с проблемой реестра.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* [598-14][@ant] feat: дозакрытие техдолгов Эпика 11 — FR45 AC5 jest-тесты + 598-21 frontend batch-flow
— FR45 AC5 (598-15): jest unit-тесты compensating-rollback signSupp/signChair
с mock chainPort failure → status не сдвинут + idempotent retry.
6 тестов в marketplace-apl-reception.service.spec.ts (76 сек локально).
— 598-21 frontend batch-flow в OperatorInventoryLabelingPage:
тогл «Маркировка партии целиком / Поштучно по заказу», выбор default
стратегии и формата, селектор партии из marketplaceListShipments,
идемпотентность через skipped_order_ids в toast, A4 print grid в
@media print, page-break-inside: avoid на этикетку.
— SDK добавлена mutation marketplaceLabelShipmentInventory + селектор
результата (labeled_order_ids / skipped_order_ids); zeus regen из
controller schema.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [598-14][@ant] fix: добавить _validate typeguard на marketplaceLabelShipmentInventoryResultSelector
Селектор должен проходить compile-time проверку MakeAllFieldsRequired<ValueTypes[...]>
как все остальные селекторы в проекте; добивает review-комментарий PR #408.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: ant <ant@noreply.local>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Любой авторизованный пользователь раньше мог запросить capitalStories /
capitalStory и получить требования всех проектов кооператива. Закрываем эту
лазейку на бекенде и показываем аккуратную заглушку на фронте.
Backend (controller):
- GenerationService.getStories: фильтрует список разрешённых project_hash
через PermissionsService. Допуск к корневому проекту каскадно открывает
все его компоненты; допуск к одному компоненту — только этот компонент.
Запрос без project_hash/issue_hash доступен только chairman/member.
- GenerationService.getStoryByHash: проверяет допуск через project_hash
(или issue.project_hash для story с issue_hash) и возвращает null без
доступа.
- getCapitalStories / getCapitalStory резолверы теперь получают currentUser.
- ProjectPermissionsOutputDTO: добавлены has_parent_clearance и
can_view_artifacts — для отображения заглушек на фронте.
Frontend (desktop):
- ProjectRequirementsPage / ComponentRequirementsPage: показывают
ArtifactsAccessPlaceholder когда can_view_artifacts=false, с кнопкой
получения допуска (или PendingClearanceButton при поданном запросе).
- Новый shared компонент ArtifactsAccessPlaceholder в стиле остальных
заглушек расширения capital.
SDK:
- projectSelector обновлён под новые поля; schema.gql / zeus
регенерированы.
Co-authored-by: coopops <coopos@coopenomics.world>
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>
Скрипт извлечения из 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>
Заносим эталонную дизайн-систему в проект — только базовые элементы для
дальнейшей компоновки. 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>
* Эпик 11: «Реестр процессов» на столе бухгалтера (598-14)
UI-надстройка над уже работающим Phase A/B ProcessRegistry в core controller:
страница /reports/processes показывает листинг процессов ledger2 с фильтрами
по типу процесса (LEDGER2_PROCESS_REGISTRY) и пайщику, при раскрытии строки
рисует events + documents процесса (через getProcess), отдаёт слот
processInfoFactory для бизнес-описания (marketplace handlers уже регистрируют
SUPPLY/RETURN/WRITEOFF в extensions/market/app/extensions.ts) и кнопку
deep-link «Открыть в реестре операций» (filter process_hash).
Используется existing SDK query `Queries.Processes.ListProcesses` —
расширены entities/Process api+store+types методом `listProcesses` и
type alias'ами IProcessListInput/IProcessListResult/IProcessSummary.
Стол бухгалтера приобретает четвёртый базовый реестр (операции, проводки,
**процессы**, кошельки, счета) — закрывает аудит-видимость marketplace
flow «Стола заказов» в рамках Эпика 11.
* Эпик 11 техдолг 598-21: batch-маркировка партии за один вызов
Mutation `marketplaceLabelShipmentInventory(shipment_id, default_strategy?,
per_order_overrides?)` массово маркирует все Order'ы партии поставки —
оператор перестаёт проходить N карточек по одной.
Service `MarketplaceInventoryLabelService.labelShipment`:
1. Shipment.findById → берёт coopname/cycle_id/braname.
2. orderRepo.findByCycleId(coopname, cycle_id) → filter по
delivery_braname партии = все Order'ы партии.
3. Для каждого Order'а зовёт уже работающий execute() — единый
источник правил маркировки (стратегия из Offer.barcode_strategy
по 598-22, per-Order override для смешанных Offer'ов в партии).
4. Идемпотентность: ConflictException от execute() при повторной
маркировке → skipped_order_ids в результате (повторный заход на
страницу не валится fatal'ом).
Возврат включает labeled_order_ids + skipped_order_ids + сами наклейки
(`MarketplaceInventoryItem[]`) — UI оператора может одним вызовом
получить grid этикеток для печати и понимать что было сделано.
`@media print` grid layout и расширение `OperatorInventoryLabelingPage`
под выбор default-стратегии на всю партию — следующий шаг, mutation
теперь доступна.
* Эпик 11 Stories 11.2 + 11.3: off-chain инварианты ledger2 marketplace + coverage трассировки 13 op-кодов
Backend-side scaffold контрактных проверок Stories 11.2 (Journal/wjournal
трассировка) + 11.3 (CI-инварианты ledger2). Реализация — pure-функциями
без NestJS/PG: работают на синтетических массивах Ledger2OperationDTO,
гонятся unit-тестом без реального chain'а (отвечает требованию off-chain
агрегации: никаких сумм по всем кошелькам пайщиков в смарт-контракте).
Состав:
- `marketplace-ledger2-invariants.ts` — 6 pure-агрегаторов (I1 payout
balance, I2 Δ86, I3 acc.10, I4 acc.91 transit = 0, I5 blocked в
w.mkt.member, I6 нет orphan o.mkt.block). Та же логика что UI стола
бухгалтера (`/reports/operations`, `/reports/postings`).
- `marketplace-ledger2-invariants.spec.ts` — 86 тестов: happy path +
violation path для каждого I1..I6 + property-based на 50 случайных
последовательностях (mix happy/cancel/return/writeoff).
- `marketplace-process-trace-coverage.spec.ts` — 10 групп: coverage по
всем 13 marketplace operation_code (12 o.mkt.* + o.wal.conv) в
cooptypes + OPERATION_CODE_TO_PROCESS_TYPE + PROCESS_HASH_LOCATOR;
Phase-A якорь связности apply+walletop+debit+credit под одним
process_hash; full-flow scenarios для supply/return/wroff.
Контрактные тесты в Antelope simulator (mono-ai-5) остаются за другим
агентом; здесь — backend-side mapping контроль и off-chain агрегация
инвариантов, доступные для CI и admin-вью.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: ant <ant@noreply.local>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
UI показывал баланс 99999.9999 RUB как 100 000,00 — пользователь видел
больше, чем реально на кошельке, и попытка инвестировать «весь баланс»
падала в walletop TRANSFER с insufficient funds (precision=4 на цепи
vs precision=2 в UI).
Принцип: «никогда не показывать сумму больше реальной». Truncate
выполняется на уровне строки, без преобразования в Number, чтобы
избежать FP-погрешности (0.29 * 100 = 28.999... в IEEE 754).
Изменения:
- desktop: новая утилита floorDecimalString — единый источник истины.
- desktop: formatAsset2Digits / formatToAsset / addAssets используют floor.
- desktop: убран double-formatting amount.toFixed(4) перед formatAsset2Digits
в ParticipantWalletsPage.
- factory: Factory.formatAsset / Factory.formatShare через приватный
truncateDecimal — тот же принцип в документах (заявления, акты).
Не затронуто (отдельный фолоу-ап): 10 файлов с inline .toFixed(4)
перед отправкой в контракт — рекомендую заменить на formatToAsset().
Co-authored-by: coopops <coopos@coopenomics.world>
* fix(controller): IS_UNIONED zod-парсер принимает string из .env
z.boolean().default(true) валится для переменной из .env, потому что
process.env всегда отдаёт строку: zod не приводит "true"/"false" к
boolean, в результате validateEnv падает с «IS_UNIONED: параметр не
установлен» и coopback не стартует, если в .env стоит IS_UNIONED=false
(стандартный dev-обход messenger-гейта, см. flow подключения партнёра).
Заменено на string().default('true').transform(v => v === 'true') —
сохранение прежнего default=true и поддержка string-форм из env.
* feat(epic-0): partner onboarding harness — signin → sign agreements → connect
Что добавлено:
- desktop/quasar.config.cjs: vite server.allowedHosts для voskhod-dev/
partner-dev/api-dev (Vite 5.4+ блокирует cross-origin Host без явного
списка — иначе SSR/HMR-сервер возвращает 403 «Blocked request»).
- docs-harness/lib/harness.mjs: helper signOnboardingAgreements —
реальная подпись каскада SignAgreementDialog (wallet/signature/
privacy/user), каждый клик отправляет sendAgreement → wallet::signagree
on-chain. В отличие от dismissOnboardingDialogs делает on-chain эффект
(см. inc 2026-05-18: stale vault SERVER_SECRET → bad decrypt).
- scenarios/onboarding/01..07: новый partner-flow Эпика 0 — partner1
заходит в Восход, подписывает 4 типовых соглашения, идёт на «Подключение»,
ant одобряет, наблюдаем установку partner-dev.
- scenarios/registration/01..04: архив старого registration-doc как
отдельная история (см. mem feedback_provider_mono_pr_flow).
- scripts/debug-*: вспомогательные скрипты для портал-структуры и
chairman onboarding'а.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(docs-harness/08): chairman install wizard на partner-dev
Сценарий 08 проходит установочный wizard /:coopname/install целиком:
шаг 1 (RequestKeyForm: WIF partner1 из state/cooperatives/partner1.json),
шаг 2 (SetInitForm: readonly orgdata из is_server_init=true, «Далее»),
шаг 3 (SetSovietForm: один председатель — Иванов И.И.),
шаг 4 (SetVariablesForm: ОПФ+ во всех падежах, устав, конф.email).
Финальный submit «Завершить установку» сейчас падает on-chain в
soviet::create — `assertion: Один из аккаунтов не найден в реестре
пайщиков`. Причина: в install.interactor.ts adduser и createBoard
шлются двумя отдельными tx; partner1 nodeos не producer, между tx есть
лаг p2p-репликации, createBoard приходит до того как participants[N]
обновился. Шот 06-error-state снимается при таймауте; шот
06-completed появится после фикса race в install.interactor (отдельный
коммит).
Сценарий запускается:
BASE_URL=https://partner-dev.coopenomics.world COOPNAME=partner1 \
node run.mjs onboarding/08-chairman-install-on-partner-dev
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(controller/install): bundle adduser+createBoard в одну tx
Cause. install.interactor.ts шлёт adduser×N и createBoard двумя отдельными
tx через BlockchainService. На production-нодах с producer-схемой это
работает (lag реплицирования минимальный), но на dev-loop'е partner-
coopback соединён со своим nodeos, который p2p-репликой подтягивает
блоки от producer'а — после accept'а adduser в local state ещё нет
soviet::participants[username] к моменту push'а createBoard. Контракт
soviet::createboard падает «Один из аккаунтов не найден в реестре
пайщиков».
Fix. Объединил все adduser-action'ы и createBoard-action в одну tx
через новый метод BlockchainPort.installSoviet(). Обе action'ы теперь
атомарны в одном блоке — soviet::addpartcpnt (inline action от
adduser) обновляет participants и createBoard видит запись сразу же
в том же блоке.
Поток в install.interactor.ts перестроен в два шага:
1. Цикл по soviet: createUser в БД + setupNotificationSubscriber +
сбор addUserActions[] и members[] (без on-chain активности).
2. installSoviet(addUserActions, createBoardData) — одна tx.
Catch при ошибке on-chain (как и раньше) откатывает users из БД.
Зачем. Закрывает блокер сценария 08-chairman-install-on-partner-dev:
без этого финальный экран wizard'а «Установка завершена» недостижим
на dev-loop'е (Эпик 0 не закрывается).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(docs-harness/09): invite-token → WIF → signin chairman
Закрывает Эпик 0: после сценария 08 (chairman install wizard) на
partner1 в Postgres появляется invite-токен председателя, но Novu не
доставит его на @example.com адрес. Сценарий 09 идёт за токеном
напрямую в БД через `ssh partner1 → docker exec postgres → psql` и
прогоняет до финального signin под новым ключом.
Шаги:
1. fetchLatestInviteToken — SELECT token FROM tokens WHERE
type='invite' AND blacklisted=false AND expires > NOW() LIMIT 1.
2. Открываем `${BASE_URL}/${COOPNAME}/auth/invite?token=<token>` —
widget Invite.vue клиентски генерирует новый WIF (generateAccount).
3. Извлекаем WIF из q-input, сохраняем в
state/cooperatives/partner1-chairman.json для последующих сценариев.
4. Чекбокс «Я сохранил ключ» → «Установить ключ» → resetKey шлёт
on-chain ChangeKey, фронт редиректит на /auth/signin.
5. Финальный signin под chairman.partner1@example.com + новый WIF;
ждём перехода на /chairman или /participant.
Запускается:
BASE_URL=https://partner-dev.coopenomics.world COOPNAME=partner1 \
node run.mjs onboarding/09-chairman-key-and-login
Требует SSH-доступа к partner1 (PARTNER_SSH=user1@91.218.246.46 по
умолчанию) и предварительно прогнанный сценарий 08 (после merge
fix(controller/install) — иначе токен не появится).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(controller/install): откатить installSoviet bundle на sleep 2s
Bundle adduser×N + createBoard в одну tx работает, но требует расширения
BlockchainPort и больше read'а; для не-producer-нод (dev-loop) достаточно
короткой паузы между adduser и createBoard, чтобы p2p-реплика подтянула
блок с participants. 2с гарантированно перекрывают и prod (~50ms), и
dev-loop (1-3с).
Отменён 3f2ecc42cf (installSoviet в blockchain.port + blockchain.service +
install.interactor), добавлен `await new Promise(setTimeout, 2000)` между
циклом adduser и createBoard.
Это однострочный фикс race-condition; bundle вернётся когда понадобится
многошаговая атомарность.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: coopops <coopops@coopenomics.world>
Финальная фаза процесса p.cap.rid (convertsegm) принимала document2 convert_statement (шаблон 1080)
параметром, но:
1. **Не проверяла подпись** — `verify_document_or_fail` отсутствовала, поэтому on-chain
принял бы любую сконструированную document2 без валидной user-подписи.
У signact1/signact2 (соседние фазы того же процесса) verify есть — здесь забыли.
2. **Не регистрировала документ в реестре** — `newlink`/`newsubmitted`/`newresolved`
не вызывался, заявление о трансляции 1080 «терялось»: off-chain controller (process_instance)
не видел финальный документ привязанным к result_hash, процесс p.cap.rid не помечался completed.
У pushrslt (create_approval) и signact2 (newlink с SIGN_ACT2_RESULT) линковка есть.
Канон есть в soviet/src/system/converttoaxn.cpp:54 и soviet/src/agreement/sndagreement.cpp:104:
паттерн `Soviet::make_complete_document(calling_contract, coopname, username, action, package_hash, document)`
шлёт newsubmitted + newresolved одной парой, package = анкер процесса (здесь result_hash).
Изменено:
- names.hpp: новая константа Names::Capital::CONVERT_SEGMENT = "convertsegm"_n (12 символов)
- convertsegm.cpp:
- verify_document_or_fail(convert_statement, {username}) сразу после require_auth
- Soviet::make_complete_document(...) ДО delete_result (чтобы линковка прошла, пока result_hash ещё анкер)
Контракт capital собирается без ошибок (cdt-cpp testnet mode), warnings — старые ricardian.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* [598-12][@ant] chore: process-hash-locator Phase B готов к активации marketplace
Эпик 9 / Story 9.3: обновлён комментарий к ключам p.mkt.supply / p.mkt.return /
p.mkt.wroff. Phase A через OPERATION_CODE_TO_PROCESS_TYPE уже связывает все
o.mkt.* с тремя process_type из cooptypes; Phase B ждёт появления entity-
таблицы marketplace::requests с полем request_hash (Locked Decision L10).
Заглушка [] остаётся корректной — Phase B наполняется одной правкой при
готовности C++.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-12][@ant] feat: operator-стол «Склад моего КУ» — Story 9.1
Лента marketplace_inventory отфильтрована по braname текущего КУ;
per-row статусы (LABELED / ISSUED / RETURNED / WRITTEN_OFF) через
mp-status-chip, фильтры по статусу/orderer, summary count by status
(activeOnly + writtenOff). Сортировка по labeled_at DESC по умолчанию.
Backend-доступ ограничивается guard'ом Warehouse: ['read:own-KU'] —
оператор видит только свой участок. Real-time подписки AR37 SDK
платформы подключаются отдельным шагом эпика (на MVP — refresh кнопкой).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-12][@ant] feat: admin-стол «Сводный склад» через WarehouseSummaryGrid — Story 9.2
Две вкладки admin-стола (UX-DR16 + DR31, mp-role-admin):
- «Сводный склад» — агрегация marketplace_inventory по ku × sku через
canon-виджет WarehouseSummaryGrid (incoming = LABELED + RETURNED, outgoing
= ISSUED, balance — транзитный остаток без ISSUED);
- «Поток заказов и поставок» — sums маркировок / выдач / возвратов /
списаний + топ-10 SKU по обороту. Графики динамики (orders_per_day,
supplies_per_day, writeoffs_per_month) подключаются по AR37 SDK-
подпискам платформы отдельным шагом.
Backend-доступ Warehouse: ['read:all'] (chairman + member).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-12][@ant] feat: SDK обёртки processes + страница «История операций» — Story 9.5
SDK:
- selectors/processes/processViewSelector + processSummarySelector
(полное покрытие ProcessView / ProcessSummary / Pagination)
- queries/processes/getProcess + listProcesses
- queries/index.ts: export * as Processes
Desktop pages/Marketplace/OperationsHistory:
- лента всех процессов с фильтрами по coopname / processType / username;
- разворот через стандартный core processRegistry.getProcess(process_hash)
показывает actions, delta_history и связанные documents;
- кнопка «Скачать JSON» для аудиторской выгрузки конкретного процесса
(AR36, L13 pull-модель).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-12][@ant] feat: UI-stub «Экосистема кооперативов» — Story 9.4
Раздел admin-стола под межкооперативную торговлю Phase 2 (NFR-Sc2 / AR18).
На MVP — заглушка с информацией о текущем кооперативе и явным сообщением
что список других controller'ов с marketplace появится после подключения
платформенного ecosystem_registry. Backend-регистрация controller через
core-API в отдельном Эпике 9 + техдолге platform (см. issue эпика).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-12][@ant] feat: маршруты market workspace под Эпик 9
Регистрирует 4 новые страницы Эпика 9 в desktop/extensions/market/install:
- market-pvz/warehouse — Story 9.1 «Склад моего КУ» (operator-стол КУ);
- market/warehouse-summary — Story 9.2 «Сводный склад» (admin-стол);
- market/history — Story 9.5 «История операций» (admin-стол);
- market/ecosystem — Story 9.4 «Экосистема» (admin-стол, MVP-stub).
Доступы соответствуют access-matrix marketplace: warehouse-стол КУ —
только chairman; сводный склад / история / экосистема — chairman +
member (board_readonly).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-12][@ant] chore: регенерация schema.gql / zeus / SDK Zeus + fix DTO writeoff
Source-of-truth GraphQL-схема была устаревшая после Эпика 8 — DTO
`MarketplaceWriteoffProposalItemDTO` использовал `?: string | null` без
explicit-`@Field(() => String)`, и Nest GraphQL падал при
`generate-schema` ⇒ Zeus в SDK не получил writeoff queries и Processes
из Story 9.5.
- DTO: пять nullable string-полей writeoff'а получили `@Field(() => String, { nullable: true })`.
- schema.gql регенерирован (`pnpm run generate-schema`).
- zeus обновлён в controller/zeus и sdk/src/zeus (`pnpm run generate-client`).
- В Zeus теперь видны: ProcessView / ProcessSummary / ProcessesFilter,
MarketplaceWriteoffProposal / PaginatedMarketplaceWriteoffProposals и т.п.
После этого `cd components/sdk && pnpm run build` собирается без ошибок;
desktop потребители (AdminWriteoffs API + новая страница OperationsHistory)
получают типизированный доступ к Zeus.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-12][@ant] fix: enum-значения статусов + убрать as const в bucket аккумуляторе
После регенерации Zeus `MarketplaceInventoryStatus` стал nominal enum
вместо string-literal union — статусы передаются через
`Zeus.MarketplaceInventoryStatus.LABELED|ISSUED|RETURNED|WRITTEN_OFF`,
а не голыми строками.
В `AdminWarehouseSummaryPage.warehouseRows` убран лишний `as const` на
дефолтном bucket'е — он делал поля `in/out/balance` readonly и ломал
накопление через `b.in += qty`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-12][@ant] feat: Phase B locator p.mkt.* — реальные ссылки на marketplace::orders/retrequests/wroffprops
Story 9.3. Контракт marketplace опубликован в Эпике 11; все три marketplace-процесса
(supply / return / writeoff) уже имеют свои entity-таблицы с одноимённым
checksum256-полем `hash`. Pre-existing заглушка `[]` с упоминанием Locked Decision L10
снята: теперь Phase B end-to-end отдаёт текущий снэпшот процесса из blockchain_deltas.
- p.mkt.supply -> marketplace::orders.hash
- p.mkt.return -> marketplace::retrequests.hash
- p.mkt.wroff -> marketplace::wroffprops.hash
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-12][@ant] refactor: убрать дубль «Истории операций marketplace» — стол бухгалтера единая точка
Старая страница `pages/Marketplace/OperationsHistory` повторяла стол бухгалтера
(`extensions/reports`) — actions / delta_history / documents видны там в общем
реестре операций. По ревью к PR #400 удаляем полностью; вместо неё marketplace
будет инжектировать бизнес-описание процесса в стол бухгалтера через фабрику
(см. следующие коммиты).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-12][@ant] feat: фабрика processInfoFactory и три marketplace info-widget'а — слот в стол бухгалтера
Аналог DecisionFactory, но для бизнес-описания процесса в реестре операций
стола бухгалтера. Каждое расширение регистрирует свой компонент по `process_type`;
стол бухгалтера рендерит его в развёрнутой строке после проводок и движений
по кошелькам. Дубля «истории» больше нет.
- shared/lib/process-info-factory + types: реестр + getInfoComponent
- widgets/Marketplace/ProcessSupplyInfoWidget — заказчик, поставщик, КУ,
состояние, единицы; deep-link «открыть заказ на столе ПВЗ»
- widgets/Marketplace/ProcessReturnInfoWidget — заказчик, состояние,
исходный заказ, причина; deep-link «открыть заявление на столе ПВЗ»
- widgets/Marketplace/ProcessWriteoffInfoWidget — источник проекта (cron/manual),
состояние, цикл, сумма; deep-link «открыть проект на столе списания»
- extensions/market/app/extensions.ts — регистрация по `LEDGER2_PROCESS_REGISTRY.name`
(SUPPLY/RETURN/WRITEOFF), без string-литералов в коде
- extensions/reports/pages/OperationsPage — слот «Содержание процесса» в развороте
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-12][@ant] style: страницы Эпика 9 на <template lang="pug"> + русские лейблы — снять техжаргон из UI
Привожу страницы к канону стола заказов: PUG-шаблоны, бизнес-словарь
в подписях/фильтрах (Состояние / Кооперативный участок / Заказчик),
никаких `p.mkt.*` или raw-имён enum-значений в видимой части. Внутри —
строгая типизация через `Zeus.MarketplaceInventoryStatus`, switch на
status-полях больше не сравнивается со строками.
- OperatorOwnWarehouse (Story 9.1) — PUG, enum-fix, человеческие лейблы
- AdminWarehouseSummary (Story 9.2) — PUG, enum-fix, человеческие лейблы
- EcosystemRegistry (Story 9.4) — PUG
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-12][@ant] refactor: вынести запрос процесса в entities/Process + чистая типизация в info-widget'ах
По канону Ledger2: api/store/types. Widget'ы больше не вызывают client.Query
и не делают cast'ов вида `as Record<string, unknown>` — снэпшот приходит
строго типизированным `IProcessSnapshot` из SDK через `useProcessStore`.
- entities/Process/types — IProcessView / IProcessSnapshot / IProcessDelta
выведены из Queries.Processes.GetProcess.IOutput
- entities/Process/api — getProcess + pickLatestSnapshot (единственная
точка приведения JSON-скаляра)
- entities/Process/model — Pinia store с loadProcess / loadLatestSnapshot
- widgets/Marketplace/Process{Supply,Return,Writeoff}InfoWidget — используют
processStore, типы из entities/Process
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-12][@ant] style: убрать пользовательский техжаргон со сводного склада и экосистемы
- AdminWarehouseSummary: удалить сноску про SDK-подписки и формулу остатка —
пользователю кооператива это не нужно
- EcosystemRegistry: оставить короткую фразу «включится позже», без режима
просмотра и упоминаний платформенного реестра
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-12][@ant] docs: зафиксировать паттерн фабрик инжекции в стол совета и стол бухгалтера
Новый документ `components/desktop/docs/extension-factories.md`:
- общий принцип «общие столы единственная точка просмотра, расширения
инжектируют бизнес-описание»
- DecisionFactory: контракт, пример регистрации (capital → стол совета)
- ProcessInfoFactory: контракт, пример регистрации (market → стол
бухгалтера) с использованием LEDGER2_PROCESS_REGISTRY вместо
строковых литералов
- чек-лист для нового расширения (виджет/PUG → app/extensions.ts →
install.ts → реестр в cooptypes)
- что делать НЕЛЬЗЯ: повторять у себя реестры операций/проводок/повестки
и пихать техжаргон в infoComponent
В README desktop — ссылка из раздела «Система расширений».
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: ant <ant@noreply.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-10][@ant] feat: backend Эпика 7 «Гарантийный возврат» (Stories 7.1-7.4) — завести MarketplaceReturnClaimService с 5 mutations (submretrn/aprretrem/rejretrem/accretrn/rejretrn) и хранилище фото в bucket stol-zakazov:images
— Domain: ReturnClaim entity + types (5 статусов pendrev/approvvisit/accepted/rejremote/rejatku с маппингом на ReturnStatus::* контракта), decision_log append-only журнал решений председателя, on_site_inspection-снапшот очного осмотра, ledger_snapshot снапшот compensating-forward пары после accretrn. Photo type хранит bucket_key + sha256 content_hash + mime_type — хеш используется как анкер on-chain (параметр photos[] submretrn).
— Repository: create/findById/findByRequestHash/findActiveByOrderId (idempotency check) /listByOrderer/listByDeliveryBraname/applyDecision (append к decision_log + опциональная фиксация on_site_inspection / ledger_snapshot). Partial-unique индекс (coopname, order_id) ограниченный активными статусами — пайщик не может одновременно открыть две заявки на один заказ.
— Canonical blockchain port + adapter: 5 новых методов submRetrn/aprRetRem/rejRetRem/accRetrn/rejRetrn — обёртки над marketplace::submretrn/aprretrem/rejretrem/accretrn/rejretrn (контракты уже реализованы в cpp/marketplace/src/p.mkt.return/). accretrn — композитная транзакция через транзит 91: o.mkt.return + o.mkt.return2 (compensating forward к o.mkt.consum, AR9/AR14 L3).
— MarketplaceReturnClaimService: submitReturnClaim (Story 7.1: валидация warranty_until>now, reason_text 1-500, photos 1-10, request_hash детерминирован от order_hash+orderer+actual_quantity, фото загружаются в bucket до on-chain submit, on-chain photos[] = sha256-хеши), approveReturnVisit/rejectReturnRemote (Story 7.2 FR31: PENDING_CHAIRMAN_REVIEW → APPROVED_FOR_VISIT/REJECTED_REMOTELY, comment обязателен), acceptReturnAtVisit/rejectReturnAtVisit (Story 7.3-7.4 FR32-FR33: APPROVED_FOR_VISIT → ACCEPTED_AT_VISIT/REJECTED_AT_VISIT, при accept атомарно compensating forward o.mkt.return+o.mkt.return2 восстанавливает w.mkt.member.available заказчика и возвращает имущество на склад КУ, barcode-сверка с Order). getReturnClaimSignablePayload — preview через factory registry_id=800 (ReturnByAssetStatement, переиспользован для членской модели по стандарту p.mkt.return.standard.yaml).
— MarketplaceReturnClaimImagesService: @UseBucket('stol-zakazov:images') — конвенция «один сервис на один bucket» из README file-storage. maxBytes 10MB, allowedMime jpeg/png/webp, metadataSchema требует ownerAccount+orderId+claimId. Ключ объекта детерминирован: returns/<claim_id>/<role>/<index>.<ext>; backend хеширует bytes (sha256-hex) — этот же хеш идёт on-chain в submretrn.photos[].
— Resolver + DTO: 5 mutations + 3 queries (marketplaceListMyReturnClaims, marketplaceListReturnClaimsByBraname, marketplaceReturnClaimSignablePayload, marketplaceReturnClaim) + Photo/Decision/Inspection/Ledger ObjectType. GraphQL enum через registerEnumType (MarketplaceReturnClaimStatus / MarketplaceReturnClaimDefectCategory / MarketplaceReturnClaimExpectedResolution). Подписанное заявление приходит как SignedDigitalDocumentInputDTO meta=MarketplaceReturnClaimSignedStatementInputDTO; toDocument() конвертит в Cooperative.Registry.ReturnByAssetStatement.Action для submretrn.statement.
— Signed-document DTO: переиспользуем шаблон 800 «Заявление на возврат паевого взноса имуществом» (по стандарту p.mkt.return — структурно совместим с членской моделью; поля request.hash/title/units/unit_cost/total_cost заполняются из Order). request.hash = backend-computed request_hash → двусторонняя сверка backend ↔ on-chain.
— Access-matrix: orderer ['create:own', 'read:own'] на ReturnClaim, operator ['read:own-KU', 'decide:remote', 'decide:on-site']. resolver-уровень проверка ownership (orderer_account == member.username, braname == председатель чьего КУ — отложено до core coop_ku.chairman_account, в MVP передаётся параметром).
— Регистрация в marketplace-infrastructure.module.ts (TypeORM entity + repository) и marketplace-application.module.ts (FileStorageInfrastructureModule.forFeature + service + resolver).
Why: реализация Эпика 7 «Гарантийный возврат имущества» MVP «Стол Заказов» — пайщик подаёт заявление с фото-доказательством, председатель КУ удалённо рассматривает и при необходимости приглашает на очный осмотр, при принятии возврата атомарно через транзит 91 восстанавливаются средства на программном кошельке заказчика и имущество возвращается на склад участка (compensating forward — отдельное событие в журнале, не revert исходного o.mkt.consum). Возврат поставщику и работа с поставщиком по претензиям — Phase 2 (out of MVP).
* [598-10][@ant] feat: notifications Эпика 7 — 3 Novu workflow и слушатели per-contract event-bus (submitted/decided/finalized) для гарантийного возврата
— @coopenomics/notifications: 3 workflow: marketplace-return-claim-submitted (председателю КУ на новое заявление; email + in-app + push с deep-link на operator-стол), marketplace-return-claim-decided (заказчику на промежуточные решения председателя — invite на очный визит), marketplace-return-claim-finalized (заказчику на финальный исход с восстановленной суммой если accept_at_visit).
— controller: 3 события в marketplace-notification.events.ts — MARKETPLACE_RETURN_CLAIM_SUBMITTED_EVENT/DECIDED/FINALIZED + типы payload. Сервис эмитит их после save в PG (INV-12).
— marketplace-notification.service.ts: 3 listener (handleReturnClaimSubmitted/Decided/Finalized) тянут subscriber_id/email из AccountDataPort, формируют payload с display-name и deep-link через config.frontend_url, шлют через NovuWorkflowPort. Ошибки доставки logged как warn — основной flow возврата не блокируется.
Why: уведомления (push/email/in-app) — обязательный канал коммуникации по UX-DR38; заявление на гарантийный возврат должно дойти до председателя КУ без поляризации UI (председатель не всегда сидит в operator-столе), решение председателя — до заказчика без необходимости вручную проверять статус заявления.
* [598-10][@ant] feat: SDK Эпика 7 — Zeus selectors, 5 mutations и 3 queries для гарантийного возврата + перегенерированная schema.gql
— Selectors: marketplaceReturnClaimSelector (полный набор полей заявления вместе с photos / decision_log / on_site_inspection / ledger_snapshot), marketplaceReturnClaimResultSelector (claim + tx_hash) и поднаборы (photo / decision-entry / inspection / ledger). Валидаторы MakeAllFieldsRequired гарантируют полное покрытие type.
— Mutations: CreateReturnClaim / ApproveReturnVisit / RejectReturnRemote / AcceptReturnAtVisit / RejectReturnAtVisit. Каждая принимает соответствующий Input DTO и возвращает marketplaceReturnClaimResultSelector.
— Queries: ListMyReturnClaims (без args), ListReturnClaimsByBraname (data.delivery_braname), ReturnClaimSignablePayload (data.order_id + actual_quantity? — preview registry_id=800 для подписи пайщиком).
— schema.gql пересчитана; zeus/const.ts + zeus/index.ts перегенерированы. Все ID-поля заявления через @Field(() => String) (Zeus резолвит scalar ID как unknown без глобального scalar-resolver — поэтому Order и ReturnClaim единообразно используют String).
Why: типобезопасный SDK-слой между backend GraphQL и desktop UI; правило «никаких raw GraphQL-строк в desktop» (feedback_graphql_no_raw_strings_desktop) требует прохода через Zeus client.
* [598-10][@ant] feat: desktop UI Эпика 7 — orderer и operator страницы возврата на канон-widgets (TakeoverDialog + BarcodeScanner)
— OrdererReturnClaims (страница /:coopname/market/returns): список заявлений пайщика разделён на «активные» (PENDING_CHAIRMAN_REVIEW + APPROVED_FOR_VISIT) и «архив» (финальные статусы). Подача нового заявления через SubmitReturnClaimDialog — full-screen TakeoverDialog с q-stepper 3 шага: описание (reason_text + defect_category + actual_quantity?) → фото (q-file 1-10 шт jpeg/png/webp до 10МБ → base64) → подпись заявления (preview registry_id=800 + Classes.Document.signDocument приватным ключом из useGlobalStore). Просмотр через ReturnClaimDetailsDialog — фото-thumbnails со ссылками на signed-URL bucket'а, q-timeline decision_log председателя, снапшот compensating-forward при ACCEPTED_AT_VISIT.
— OperatorReturnClaims (страница /:coopname/market-pvz/returns): три ленты — pending (PENDING_CHAIRMAN_REVIEW) с inline-thumbnails фото и кнопкой «Принять решение» → RemoteDecisionDialog, approved (APPROVED_FOR_VISIT) с кнопкой «Очный осмотр» → OnSiteDecisionDialog, archive. RemoteDecisionDialog — q-option-group approve/reject + обязательный комментарий 1-500 симв. OnSiteDecisionDialog — q-stepper: 1) BarcodeScanner сканирует штрих-код имущества для сверки с Order; 2) inspection_result 1-2000 симв; 3) опциональные inspection_photos до 10 шт; 4) accept (compensating forward предупреждение) либо reject (имущество остаётся у заказчика).
— API: типобезопасные обёртки в pages/<page>/api/index.ts через destructuring результата `{ [Queries.X.name]: result } = await client.Query(...)`. Типы выведены из Queries/Mutations.X.IOutput без cast'ов. Фото-загрузка через FileReader→base64 в browser, backend кладёт в bucket stol-zakazov:images.
— DateTime utility: `formatDateTime(value: unknown)` для GraphQL DateTime scalar — Zeus резолвит как unknown без глобального scalar-resolver, простой helper парсит и форматирует через toLocaleString('ru-RU').
— install.ts: 2 новых маршрута зарегистрированы — workspace 'market' (orderer-стол) → '/:coopname/market/returns' (roles []); workspace 'market-pvz' (operator-стол) → '/:coopname/market-pvz/returns' (roles ['chairman']). Иконки fa-rotate-left / fa-clipboard-check; agreements: agreementsBase.
Why: продолжение поведенческой части Эпика 7 — пайщик и председатель кооперативного участка получают функциональные UI для подачи и рассмотрения заявлений на гарантийный возврат, со всеми атрибутами (фото, подпись заявления, барскод-сверка, compensating forward) согласно FR29-FR33. Соблюдён канон дизайн-системы Эпика 10: используются только widgets/Marketplace/* (TakeoverDialog + BarcodeScanner), inline-вёрстка кастомных диалогов запрещена; токены через marketplace-tokens.scss (var(--mp-space-md), mp-role-* классы на корнях страниц).
* [598-10][@ant] feat: добавить registry_id=1104 MarketplaceReturnStatement в cooptypes — отделить документ Эпика 7 «Стол заказов» от системы клиринга
Marketplace по системе членских взносов ведёт свой контур документов рядом
с 1102 «Акт приёма-передачи» и 1103 «ТТН». До этого Эпик 7 ошибочно
переиспользовал registry_id=800 ReturnByAssetStatement из старой системы
клиринга «по структурному совпадению» — это смешало два процесса в одном
signed-document пайплайне. Завожу полноценный собственный документ:
заявление пайщика о гарантийном возврате имущества с marketplace-полями
(order_id / order_hash / fact_cost / actual_quantity / reason_text /
defect_category / braname). Шаблон 800 остаётся как есть на случай
будущего клиринга.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [598-10][@ant] feat: проложить factory-цепочку для registry_id=1104 — Action + Template + регистрация в Registry/index
Action 1104.MarketplaceReturnStatement.Factory повторяет паттерн 1102
(getCooperative/getVars/getUser/getRequest/getProgram/getOrganization),
но кладёт в Model marketplace-поля (fact_cost / actual_quantity /
reason_text / defect_category) и не требует transmitter/decision —
заявление подписывает только пайщик.
Также: убираю tsconfig.tsbuildinfo из индекса (build-артефакт, не должен
жить в git).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [598-10][@ant] feat: переключить controller + SDK на registry_id=1104 для Эпика 7 — заявление о гарантийном возврате имущества
- marketplace-return-statement-document.dto.ts — новый Signed-DTO под
Cooperative.Registry.MarketplaceReturnStatement.Action; старый
marketplace-return-claim-document.dto.ts (ссылавшийся на 800
ReturnByAssetStatement) удалён.
- MarketplaceReturnClaimService.generateStatementDocument теперь строит
Action.MarketplaceReturnStatement с marketplace-полями (order_id /
order_hash / fact_cost / reason_text / defect_category / braname),
а не reuse'ит request{hash,units,unit_cost,total_cost} из старого 800.
- getReturnClaimSignablePayload принимает reason_text — превью теперь
показывает реальный текст обращения пайщика.
- Регенерированы schema.gql + zeus client + SDK zeus (вход
MarketplaceReturnStatementSignedInput с marketplace-меткой meta).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [598-10][@ant] feat: desktop SubmitReturnClaimDialog — превью под registry_id=1104 с пробросом reason_text
- API getReturnClaimSignablePayload теперь принимает reason_text и
defect_category — превью показывает реальный текст обращения пайщика.
- Убрал `result as MarketplaceGeneratedDocumentView` cast (Zeus уже
выдаёт нужный тип после регенерации schema/SDK).
- Комментарии и подсказки UI обновлены: 1104 MarketplaceReturnStatement
вместо устаревшего 800 ReturnByAssetStatement.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [598-10][@ant] refactor: десктоп-API Эпика 7 на строгие типы из SDK — variables передаём целиком data
Канон @coopenomics/sdk: каждый api-метод принимает `data: Mutations.<X>.<Y>.IInput['data']`
(или `Queries.<X>.<Y>.IInput['data']`) и передаёт его в variables объектом
целиком — `{ variables: { data } }`. Без раскладки полей вручную и без
самодельных интерфейсов аргументов.
Затронуто:
- OrdererReturnClaims/api: getReturnClaimSignablePayload + createReturnClaim
- OperatorReturnClaims/api: listReturnClaimsByBraname, approveReturnVisit,
rejectReturnRemote, acceptReturnAtVisit, rejectReturnAtVisit
- SubmitReturnClaimDialog: defectCategory типизирован как enum
Zeus.MarketplaceReturnClaimDefectCategory (раньше падал на string→enum,
но был замаскирован ручной раскладкой полей в variables)
- OnSiteDecisionDialog: ReturnClaimPhotoUploadInput выведен из IAcceptReturnAtVisitInput
- OperatorReturnClaimsPage: listReturnClaimsByBraname({ delivery_braname })
Зачем — ревью PR #397: при ручной раскладке полей в variables.data
изменение схемы в SDK не подсвечивает call-site как ошибку TS;
регрессии маскируются. Канон даёт строгую сверку при изменении бэкенда.
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
PR #394 «починил» TS-ошибки coopback заменой типа `Cooperative.Registry.GenerationConvertStatement`
на `GenerationMoneyInvestStatement` в 3 файлах controller'а. Это семантически неверно:
- 1080 GenerationConvertStatement — заявление о трансляции паевого взноса
(поля: project_hash, main_wallet_amount, blagorost_wallet_amount, to_wallet, to_blagorost, appendix_hash)
- 1020 GenerationMoneyInvestStatement — заявление о денежном паевом взносе по программе Генерация
(совсем другой набор полей)
DTO BaseGenerationConvertStatementMetaDocumentInputDTO декларирует поля 1080, но `implements ExcludeCommonProps<action>`
где action = тип 1020 → TS2352 на as-cast в interactor, потому что Action'ы не пересекаются по полям.
Реальная причина исходных ошибок coopback после #392 — несвежий dist `@coopenomics/cooptypes`
на dev-узле (старое имя символа). Лечится пересборкой пакета, не переименованием ссылок.
Изменено (откат #394):
- generation-convert-statement-document.dto.ts:12 — `action = ...GenerationConvertStatement.Action`
- distribution-management.service.ts:56 — `registry_id: ...GenerationConvertStatement.registry_id`
- distribution-management.interactor.ts:48,66 — `Promise<...GenerationConvertStatement.Action>`
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
z.boolean().default(true) валится для переменной из .env, потому что
process.env всегда отдаёт строку: zod не приводит "true"/"false" к
boolean, в результате validateEnv падает с «IS_UNIONED: параметр не
установлен» и coopback не стартует, если в .env стоит IS_UNIONED=false
(стандартный dev-обход messenger-гейта, см. flow подключения партнёра).
Заменено на string().default('true').transform(v => v === 'true') —
сохранение прежнего default=true и поддержка string-форм из env.
Co-authored-by: coopops <coopos@coopenomics.world>
PR #392 переименовал Cooperative.Registry.GenerationConvertStatement в
GenerationMoneyInvestStatement в @coopenomics/document, но в controller
осталось 3 несинхронизированные ссылки:
- distribution-management.service.ts:56 — registry_id метода generation
- distribution-management.interactor.ts:48,66 — тип Action в return и as-cast
- generation-convert-statement-document.dto.ts:12 — type action в DTO
Coopback падал на ts-node compile (TS2551/2724), что блокировало старт
всего dev-stack после reboot dev-chain 2026-05-18.
Co-authored-by: coopops <coopos@coopenomics.world>
* [598-9][@ant] feat: backend Эпика 6 «Выдача пайщику с двойной подписью» (Stories 6.1-6.4) — расширить Order полями выдачи, завести MarketplaceIssuanceService с двумя mutations (signiss1 + signiss2) и push «заказ готов к получению»
— Order entity + typeorm + mapper: добавлены поля выдачи (current_warehouse_braname, issuance_fact, chairman_signed_at/chairman_account/signiss1_tx_hash, orderer_signed_at/delivery_signer_account/signiss2_tx_hash), derived-геттеры awaits_chairman_issue_open/awaits_orderer_issue_final/is_received. На Order живут metadata подписей; жирные Document2 хранятся в on-chain row marketplace::orders.
— Repository: applyIssuanceOpened (статус ACCEPTED_TO_COOP → READY_TO_RECEIVE, проставление chairman_signed_at + signiss1_tx_hash + current_warehouse_braname = delivery_braname), applyIssuanceFinalized (READY_TO_RECEIVE → RECEIVED + issuance_fact + warranty_until), listForIssuanceByBraname (operator), listReadyToReceiveByOrderer (orderer).
— Chain port + adapter: signIss1, signIss2 — обёртки над marketplace::signiss1/signiss2 (контракт уже реализован в cpp/marketplace/src/p.mkt.supply/).
— MarketplaceIssuanceService: openIssuance (председатель КУ открывает выдачу), finalizeIssuance (заказчик закрывает финальной подписью; actual_quantity влияет на корректирующие операции в C++ signiss2 — три ветки FR23), getOpenIssuanceSignablePayload / getFinalizeIssuanceSignablePayload (preview документов через factory registry_id=1102).
— GraphQL DTO + resolver: MarketplaceIssueActSignedDocumentInputDTO, MarketplaceOpenIssuanceInputDTO, MarketplaceFinalizeIssuanceInputDTO, MarketplaceIssuanceResultDTO; mutations marketplaceOpenIssuance / marketplaceFinalizeIssuance; queries marketplaceListIssuancesByBraname / marketplaceListMyReadyToReceive + preview payloads.
— Access matrix: operator получает Issuance:{create,sign:first,read:own-KU}; orderer получает Issuance:{sign:final,read:own}.
— Push notification (Story 6.4 / FR22): новый workflow @coopenomics/notifications/marketplace-order-ready (email + in-app + push) + listener в MarketplaceNotificationService, эмит после applyIssuanceOpened (INV-12 — после save в PG).
— MarketplaceOrderDTO расширен публичными полями выдачи (issuance_fact с enum diff_state, chairman/orderer signatures metadata) — UI карточки заказа в RECEIVED видит фактическое количество и diff_state.
Issue: 598-9 / Эпик 6 MVP Стола Заказов
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-9][@ant] feat: SDK Эпика 6 — обёртки openIssuance/finalizeIssuance + queries listing/preview, обновлены схема и zeus-клиент (Story 6.5)
— schema.gql + zeus regen после расширения marketplace-process-controller: MarketplaceOpenIssuance/FinalizeIssuance/IssuanceResult/IssueActPayload + 4 query/mutation для столa выдачи.
— Selectors: orderSelector расширен полями выдачи (current_warehouse_braname, issuance_fact с diff_state, chairman/orderer signatures metadata) + новый marketplaceOrderIssuanceFactSnapshotSelector + marketplaceIssuanceResultSelector — UI карточки заказа в RECEIVED увидит фактическое количество.
— Mutations: openIssuance.ts, finalizeIssuance.ts — обе через единый marketplaceIssuanceResultSelector.
— Queries: listIssuancesByBraname.ts (operator-стол), listMyReadyToReceive.ts (orderer-стол), issueActChairmanSignablePayload.ts / issueActOrdererSignablePayload.ts (preview документа для подписи).
— Side-fix техдолгов рассинхронизации cooptypes dist ↔ controller, мешавших generate-schema:
* marketplace-apl-reception.service.ts — добавлено обязательное reception_id в Action 1102.
* marketplace-shipment-create.service.ts — Action 1103.MarketplaceTransportNote/PrivateData приведены к any (поля экспедитора уже off-chain в privatePayload через doc_data_hash).
* generator.service.ts — saveDocData/getDocData проброшены через cast (методы из @coopenomics/factory есть в runtime, в типах ещё не публиковались).
Issue: 598-9 / Эпик 6 MVP Стола Заказов
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-9][@ant] feat: каркас столов выдачи в desktop — OperatorIssuance + OrdererReadyToReceive (Stories 6.6-6.7)
Минимальные каркасные страницы по образцу OperatorReception для Эпика 6 — лента заказов на ПВЗ + точки входа в подписи. Полный flow подписи (BarcodeScanner со штрих-кодом заказа, CorrectionTable для сверки факт vs заказ, TakeoverDialog для двух подписей) — следующий UI PR; backend уже принимает signed_document и исполняет три ветки сверки в C++ signiss2 атомарно.
— OperatorIssuance: список заказов на КУ выдачи (ACCEPTED_TO_COOP + READY_TO_RECEIVE) + точки входа «Открыть выдачу» (signiss1) / «Завершить выдачу» (signiss2). API через @coopenomics/sdk — listIssuancesByBraname / openIssuance / finalizeIssuance + getChairman/OrdererSignablePayload.
— OrdererReadyToReceive: лента заказов пайщика в READY_TO_RECEIVE — визуальное продолжение push-уведомления marketplace-order-ready (FR22). API — listMyReadyToReceive через @coopenomics/sdk.
— Никаких raw GraphQL-строк — только типизированные обёртки @coopenomics/sdk, как требует components/desktop/extensions/market/CLAUDE.md.
Issue: 598-9 / Эпик 6 MVP Стола Заказов
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-9][@ant] feat: полный UI flow подписи Эпика 6 через канон-widgets + регистрация в роутинге
Закрываем техдолг Эпика 6, оставленный из предыдущего коммита. Оба диалога — это реальная подпись приватным ключом через @coopenomics/sdk Classes.Document, не моки.
— IssueActOpenDialog (Story 6.1 / FR21): full-screen TakeoverDialog с предварительным актом выдачи (registry_id=1102). Председатель кооперативного участка подписывает первой подписью приватным ключом из useGlobalStore (signatureId=1), backend верифицирует и отправляет on-chain signiss1; статус заказа READY_TO_RECEIVE + push заказчику.
— IssueActFinalizeDialog (Story 6.3 / FR23-FR25): full-screen takeover с тремя шагами через q-stepper:
* **Шаг 1 — штрих-код.** BarcodeScanner (DR11) сверяет штрих-код заказа.
* **Шаг 2 — сверка факта.** CorrectionTable (DR13) показывает «план vs факт», оператор корректирует actual_quantity. UI вычисляет fact_cost и diff_state (equal/less/more) и подсказывает, какие корректирующие операции исполнит контракт (o.mkt.unblk при less, o.wal.conv + o.mkt.assign + o.mkt.block при more).
* **Шаг 3 — двойная подпись.** Заказчик вводит свой WIF (signatureId=1, signer = orderer_account), оператор подписывает вторым ключом из сессии (signatureId=2, delivery_signer = current member). Backend верифицирует обе подписи и отправляет on-chain signiss2 — C++ исполняет три ветки сверки в одной композитной транзакции; при недостатке средств на доплату (more) транзакция падает с человеческим сообщением (L6 guard / FR25), backend пробрасывает его клиенту.
— OperatorIssuancePage переключён на эти диалоги — теперь кнопки «Открыть выдачу» и «Завершить выдачу» делают реальный сценарий, а не NotifyAlert-заглушку.
Регистрация в extensions/market/install.ts:
— `/<coopname>/market-pvz/issuance` — operator-стол выдачи (roles: chairman, иконка handshake).
— `/<coopname>/market/ready-to-receive` — orderer лента «Готово к получению» (roles: все, иконка box-check).
Issue: 598-9 / Эпик 6 MVP Стола Заказов
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-9][@ant] refactor: убрать any/unknown по ревью — пересобрать cooptypes/factory + типизировать desktop api через Queries.X.IOutput и Types.Document
Реакция на ревью PR #390: ревьюер требует строгую типизацию, any/unknown запрещены вне очень редких случаев.
Корневая причина прошлых хаков — рассинхронизация cooptypes/factory dist ↔ source в monorepo. Source 1102 уже без reception_id и 1103 уже с PrivateData, но dist в node_modules был собран из старой версии. Решение: пересборка cooptypes и factory из текущего source — dist приходит в соответствие с типами, controller компилируется без cast'ов.
— marketplace-shipment-create.service.ts: `const action: any` → `Cooperative.Registry.MarketplaceTransportNote.Action`, `privatePayload: any` → `PrivateData`. Все expeditor-поля живут off-chain в privatePayload, как и задумано в архитектуре 1103.
— marketplace-apl-reception.service.ts: убран лишний `reception_id` (его нет в свежей dist Action 1102).
— marketplace-issue-act-document.dto.ts: убрано поле `reception_id` из подписной формы АПП-выдачи — тоже не требуется.
— generator.service.ts: убран `(this.generator as any).saveDocData/getDocData` — методы есть в новой factory dist.
— catch (err: any) → catch (err) с `err instanceof Error ? err.message : String(err)` в marketplace-issuance.service.ts.
Desktop:
— OperatorIssuance/api/index.ts полностью переписан: вместо локальных View-интерфейсов и `as unknown as` — производные типы от SDK: `Queries.Marketplace.ListMyReadyToReceive.IOutput['marketplaceListMyReadyToReceive'][number]` для Order'а в контексте выдачи, `Types.Document.IGeneratedDocument` для preview-документа, `Types.Document.ISignedDocumentInput` для signed_document, `Mutations.Marketplace.OpenIssuance.IOutput['marketplaceOpenIssuance']` для результата. Все client.Query / client.Mutation возвращают строго типизированный `result` через destructuring `const { [Mutations.X.name]: result }` — паттерн из features/User/LoginUser/api/index.ts.
— OrdererReadyToReceive/api/index.ts: то же — без `as unknown as`.
— IssueActOpenDialog/FinalizeDialog: убраны все `generated as unknown as Parameters<...>` и `signed as unknown as SignedDocumentInput` — `docSigner.signDocument(generated, ...)` принимает результат `getXxxSignablePayload` напрямую через `Types.Document.IGeneratedDocument`. catch (err: any) → catch (err) с проверкой `instanceof Error`.
Issue: 598-9 / Эпик 6 MVP Стола Заказов
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(capital/convert): унифицировать 1080 как универсальное заявление о конвертации, убрать 1081/1082
Шаблон 1080 (GenerationConvertStatement) уже технически универсален — содержит
обе суммы (main_wallet_amount/blagorost_wallet_amount) и условные блоки в
context. Шаблоны 1081 (GenerationToProjectConvertStatement) и 1082
(GenerationToCapitalizationConvertStatement) были недоделанными заглушками
без полей и нигде не подключены в UI.
Изменения:
- cooptypes: 1080 переименован GenerationToMainWalletConvertStatement →
GenerationConvertStatement; title/description нейтральные. 1081/1082 удалены.
- factory: Template + Action 1080 переименованы; в Action добавлено
super.formatAsset(...) для обеих сумм (как в Action 1020). 1081/1082 удалены.
- controller: DTO переименован; appendix_hash убран из generate-input и
перенесён в signed-meta-input; добавлен enrich appendix_hash через
AppendixRepository.findConfirmedByUsernameAndProjectHash в
DistributionManagementInteractor.prepareGenerationConvertStatementData
(по образцу InvestsManagementInteractor для 1020). Резолвер мутации
переименован в capitalGenerateGenerationConvertStatement; убраны два
резолвера 1081/1082. mutation-log-mapper обновлён.
- sdk: мутация переименована, две удалены, zeus regenerated.
- desktop: Distribution-фичи 1081/1082 удалены, 1080-фича переименована.
ConvertSegment теперь шлёт project_hash + обе суммы (formatToEosioAsset) +
to_wallet/to_blagorost; appendix_hash подтягивается на бекенде.
- controller schema.gql regenerated.
registry_id=1080 не меняется — on-chain контракт convertsegm не сверяет
registry_id, миграций БД/контракта не требуется.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(capital/convert): применить ревью — title «трансляция паевого взноса из программы Генерация»
По комментарию ревью в PR #392 (строка 37): принять доменный термин
«трансляция паевого взноса» (перенос между программами) вместо
«конвертация»; description согласован в том же стиле.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(docs-harness): onboarding 01..06 — визуальная цепочка регистрация → активация
Шесть сценариев visual docs-harness, покрывающих полный путь подключения
нового кооператива через провайдера Восход:
01-register-coop — регистрация кооператива-клиента
02-sign-and-submit — подпись заявления о вступлении + PayInitial
03-operator-approve — chairman принимает заявку в реестре одобрений
04-sign-connection-agreement — Партнёр-1 видит ConnectionAgreementStepper
(на текущем стенде получаем заглушку
/signup, пока пайщик не принят)
05-activate-from-registry — оператор открывает карточку инстанса
в provider-frontend, выбирает preset
06-wait-instance-active — pending → ACTIVE (overrideInstance
для имитации финального статуса в шоте)
Обвязка:
• lib/registrator-signup.mjs — переиспользуемый helper подписания.
• lib/harness.mjs — расширенный dismissOnboardingDialogs (Положение ЦПП
и связанные модалки chairman'а).
• state/cooperatives/{,.gitkeep} — папка для фикстур; partner1.json
(с приватным wif) игнорируется (.gitignore обновлён).
ОГРАНИЧЕНИЕ: в 06 финальный ACTIVE — это playwright-override JSON, а не
реальный POST /instances/activate с боевым Ansible до testnet300.coopenomics.world.
Реальный E2E (аренда VM на Hostkey + поднятие кооператива на домене)
будет следующим шагом Эпика 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(controller): is_server_init flag в initSystem для разблокировки provider-overwrite
Поле data.is_server_init: boolean (optional) в InitDTO + домейн-интерфейсе.
Если true (вызов от провайдера через server-secret) — coopback ставит
init_by_server=true безусловно, даже если до этого пользователь успел
заполнить визард первым (user-init). Это разблокирует ситуацию, когда
провайдер не успел вызвать initSystem до того как chairman открыл
/install — следующий callInitSystemMutation от провайдера перезапишет
organization_data и пометит её readonly для визарда.
Без флага сохраняется прежняя логика: первая инициализация — серверная,
повторная наследует флаг.
Инцидент 2026-05-18 на partner1: при первой установке provider вообще
не успел/не сходил в callInitSystemMutation, визард пользователя
проинициализировал систему как user-init (init_by_server=false), визард
2-го захода не предзаполнил форму. С этим фиксом следующий вызов
provider'а поднимет флаг и фикстура встанет на место.
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-8][@ant] feat: добавить формирование партий поставки из акцептованной заявки с жёстким акцептом состава — закрыть Stories 5.1 и 5.2 Эпика 5
Backend (controller/extensions/marketplace) — старт Эпика 5 «Поставка и
приёмка имущества на КУ». Story 5.1 — pre-shipment группировка Order'ов
консолидированной заявки по КУ-получателю с выбором варианта доставки.
Story 5.2 — жёсткий акцепт состава поставки backend'ом перед формированием
партий.
Domain (новое):
- MarketplaceShipmentDomainEntity + types (delivery_variant A/B, status
DRAFT/SUPPLY_PREPARED/RECEPTION_IN_PROGRESS/ACCEPTED_TO_COOP/CANCELLED,
TTNData с полями экспедитора, ТС, погрузки, доставки). Backend-only PG
без on-chain зеркала: фиксация поставки идёт через АПП-приёмки и
signsupp/signchair (Stories 5.3/5.4/5.6).
- MarketplaceSupplyValidationLogDomainEntity + типы Outcome/Reason —
append-only audit-журнал валидаций состава.
Infrastructure (новое):
- MarketplaceShipmentEntity TypeORM с unique-индексом (coopname,
cycle_id, ku_id) + partial unique-индексом по ttn_number + hot-path
индексами по offerer/ku/status.
- MarketplaceSupplyValidationLogEntity TypeORM.
- Адаптеры репозиториев + мапперы Row→Domain под обе сущности.
Application (новое):
- MarketplaceShipmentCreateService — guard ACCEPTED + ownership заявки,
жёсткая валидация (Story 5.2): покрытие всех КУ заявки группами,
непересечение групп, статус Order'ов ACCEPTED. Атомарное создание
Shipment'ов per группа + перевод Order'ов в SUPPLY_PREPARED. Для
Варианта Б — генерация уникального ttn_number и stub-URL ТТН
(реальная подпись через document factory подключается Story 5.4
follow-up).
- MarketplaceShipmentResolver — мутация marketplaceCreateShipment под
Shipment:create:own access-matrix, query marketplaceListShipments
+ marketplaceGetShipment для стола подготовки поставки.
Wiring:
- marketplace-infrastructure.module.ts регистрирует обе TypeORM-сущности,
адаптеры и мапперы, экспортирует репозиторные токены.
- marketplace-application.module.ts регистрирует create-service и
resolver.
Не входит в Stories 5.1/5.2 (отложено по scope в follow-up Stories
эпика):
- Apollo-codegen интеграция нового стола «Подготовка поставки» в desktop
offerer-flow — обвязка через @coopenomics/sdk после generate-schema +
generate-client (паттерн PR #386 Эпика 4).
- Реальная подпись ТТН через AR33 document factory — закрывается
Story 5.4 (асинхронная подпись поставщика).
- GraphQL Subscription marketplaceShipmentUpdated для live-tracking
партий — follow-up по аналогии с Order subscriptions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-8][@ant] feat: добавить маркировку имущества штрих-кодом Code128/EAN-13 на приёмке КУ — закрыть Story 5.5 Эпика 5
Backend (controller/extensions/marketplace) — оператор КУ при приёмке
маркирует имущество внутренним штрих-кодом (стандарт маркетплейсов;
QR-коды в MVP запрещены).
Domain (новое):
- MarketplaceInventoryDomainEntity с конструктор-валидацией формата
EAN-13 (13 цифр + checksum) и обязательного barcode_value.
- MarketplaceBarcodeFormat (CODE128/EAN13), MarketplaceBarcodeStrategy
(PER_ORDER/PER_UNIT/PER_PACKAGE), MarketplaceInventoryStatus
(LABELED/ISSUED/RETURNED/WRITTEN_OFF — последние три задел для Эпиков
6/7/8).
Infrastructure (новое):
- MarketplaceInventoryEntity TypeORM с unique-индексом по
(coopname, barcode_value) — поиск сканером при выдаче — плюс hot-path
индексами по order/ku/shipment + status.
- Адаптер репозитория + маппер Row→Domain.
Application (новое):
- MarketplaceInventoryLabelService — guard перехода статуса Order
(SUPPLY_PREPARED/READY_TO_RECEIVE/ACCEPTED_TO_COOP), guard повторной
маркировки (countByOrder>0 → 409), привязка к Shipment через
cycle_id+ku_id, planLabels по стратегии (PER_ORDER → 1 этикетка;
PER_UNIT → N=quantity; PER_PACKAGE → ceil(quantity/pack_size)),
генерация уникального barcode_value с retry-loop против коллизий,
корректный checksum-digit EAN-13.
- MarketplaceInventoryResolver — мутация marketplaceLabelInventory под
новый access-matrix permission Inventory:label (роль operator), query
marketplaceListInventory под Warehouse:read:own-KU для admin-стола.
- Access matrix дополнен `operator → Inventory: ['label']`.
Wiring:
- MarketplaceInventoryEntity подключён в connection «marketplace»,
адаптер и маппер зарегистрированы в infrastructure-module, сервис
и резолвер — в application-module.
Не входит в Story 5.5 (отложено по scope):
- Apollo-codegen интеграция BarcodeScanner/BarcodeDisplay в operator-стол
— обвязка через @coopenomics/sdk после generate-schema +
generate-client.
- Batch-маркировка одной страницей для всей партии (UX-уровень,
обвязка вокруг marketplaceLabelInventory).
- barcode_strategy как поле Offer — в MVP стратегия передаётся в
mutation. Поле на Offer'е — техдолг витрины (Stories 3.x follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-8][@ant] feat: добавить акт приёмки партии с двухподписной state machine для Вариантов А и Б — закрыть Stories 5.3 и 5.4 Эпика 5
Backend (controller/extensions/marketplace) — АПП приёмки на КУ:
оператор формирует акт, поставщик подписывает первой подписью
(лично — Вариант А или асинхронно через push — Вариант Б), председатель
закрывающей. Backend state machine (PG) + расширение canonical-port
методами signSupp / signChair.
Domain (новое):
- MarketplaceAplReceptionDomainEntity + types: вариант A/B, статусы
PENDING_SUPPLIER_SIGN → PENDING_CHAIRMAN_RECEPTION_SIGN →
ACCEPTED_TO_COOP (плюс CANCELLED резерв), fact_quantity_per_order
json для корректировки Варианта Б, expeditor_data снапшот из
Shipment.ttn_data.
- canonical-port расширен signSupp / signChair (Marketplace.ISignSupp /
ISignChair из cooptypes).
Infrastructure (новое):
- MarketplaceAplReceptionEntity TypeORM с unique-индексом по
(coopname, shipment_id) — одна активная АПП на партию + partial
unique по ttn_number + hot-path индексами по ku/offerer.
- Адаптер репозитория + маппер Row→Domain.
- MarketplaceCanonicalBlockchainAdapter дополнен submit'ами signsupp
и signchair (подпись кооператива через VaultDomainService).
Application (новое):
- MarketplaceAplReceptionService — state machine:
* create(shipment_id, fact_quantity?) — guard статуса партии
SUPPLY_PREPARED + ownership + одна-АПП-на-партию через unique-
constraint; для Варианта Б копирует TTN-данные снапшотом;
fact_quantity по умолчанию = order.quantity; total_amount =
Σ fact_quantity * price_per_unit. Shipment →
RECEPTION_IN_PROGRESS.
* signAsSupplier(reception_id) — переход
PENDING_SUPPLIER_SIGN → PENDING_CHAIRMAN_RECEPTION_SIGN;
placeholder tx_hash до подключения on-chain signsupp с реальной
подписью документа Document2.
* signAsChairman(reception_id, chairman_account) — переход
PENDING_CHAIRMAN_RECEPTION_SIGN → ACCEPTED_TO_COOP; Order'ы
группы переходят в ACCEPTED_TO_COOP (FR19a — выдача
разблокируется только здесь); Shipment → ACCEPTED_TO_COOP.
- MarketplaceAplReceptionResolver:
* marketplaceCreateAplReception под Receiving:create.
* marketplaceSignAplReceptionAsSupplier под Receiving:sign:first
(теперь принадлежит роли offerer — корректировка матрицы).
* marketplaceSignAplReceptionAsChairman под Receiving:sign:closing
(новый permission на роли operator).
* marketplaceListAplReceptionsByKu — список текущих АПП КУ для
operator-стола.
* marketplaceListAplReceptionsAsSupplier — список ждущих подписи
поставщика для offerer-стола.
- Access-matrix:
* offerer + Receiving: ['sign:first'] — первая подпись поставщика
в обоих вариантах.
* operator: Receiving: ['create', 'sign:closing'] — формирование
АПП и закрывающая подпись председателя КУ.
Wiring:
- Сущность подключена в connection «marketplace», адаптер/маппер в
infrastructure-module, сервис/резолвер в application-module.
Не входит в Stories 5.3/5.4 (отложено по scope):
- FR45 signing infrastructure: реальная подпись Document2 (IPFS +
AR33 document factory) + on-chain submit signsupp/signchair. Сейчас
поля supplier_signsupp_tx_hash / chairman_signchair_tx_hash
содержат placeholder-значения; chain-port методы реализованы и
готовы к вызову, но сервис их пока не дёргает (TODO Story 5.6
follow-up).
- Push-уведомление поставщику в Варианте Б при создании АПП — обвязка
через notification-module.
- outgoing_payment и o.mkt.purch / o.mkt.payout — закрываются Story
5.6 / 5.7 отдельным коммитом эпика.
- GraphQL Subscription marketplaceAplReceptionUpdated — follow-up
по аналогии с Order subscriptions.
- Apollo-codegen интеграция TakeoverDialog в operator-стол.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-8][@ant] feat: добавить marketplace-реестр исходящих платежей с подтверждением кассиром — закрыть Stories 5.6 и 5.7 Эпика 5
Backend (controller/extensions/marketplace) — на закрывающей подписи
АПП-приёмки (Story 5.3/5.4) backend автоматически создаёт запрос
исходящего платежа поставщику; кассир видит задачу в своём столе,
подтверждает факт банковского перевода, статус переходит в
LEDGER_RECORDED.
Domain (новое):
- MarketplaceOutgoingPaymentRequestDomainEntity + types: статусы
PENDING_CASHIER_ACTION → CONFIRMED_BY_CASHIER → LEDGER_RECORDED
(плюс BLOCKED при отказе банка). Поля payment_reference (банковский
ПП), bank_statement_ref (URL/hash выписки), payout_tx_hash под
lazy-вариант L12, blocked_reason для аудита отказов.
Infrastructure (новое):
- MarketplaceOutgoingPaymentRequestEntity TypeORM с unique-индексом по
(coopname, apl_reception_id) — один платёж на одну АПП + hot-path
индексами по payee и статусу.
- Адаптер репозитория + маппер Row→Domain.
Application (новое):
- MarketplaceAplReceptionService.signAsChairman расширен: после
закрывающей подписи председателя создаётся запрос платежа со статусом
PENDING_CASHIER_ACTION. Промах не блокирует основной flow АПП
(логируется warn, наблюдаемость по статусам реестра).
- MarketplaceOutgoingPaymentService:
* confirm(payment_request_id, payment_reference, bank_statement_ref?)
— кассир подтверждает физический перевод; статус → LEDGER_RECORDED
(MVP-поведение pre-L12, см. ограничения ниже).
* markBlocked(payment_request_id, reason) — отказ банка; платёж
помечается, обязательство по 86 остаётся открытым, кооператив
разбирает вручную.
- MarketplaceOutgoingPaymentResolver:
* marketplaceConfirmOutgoingPayment — мутация подтверждения кассиром.
* marketplaceBlockOutgoingPayment — мутация блокировки.
* marketplaceListOutgoingPaymentsForCashier — стол кассира.
* marketplaceListOutgoingPaymentsAsSupplier — история выплат для
offerer-стола.
Wiring:
- Сущность подключена в connection «marketplace», адаптер/маппер в
infrastructure-module, сервис/резолвер в application-module.
MVP-ограничения и техдолг:
- C++ marketplace::signchair выполняет композитную пару
o.mkt.purch + o.mkt.payout атомарно (pre-L12 поведение). Поэтому
в текущей реализации подтверждение кассира трактуется как
audit-trail банковского перевода (LEDGER_RECORDED сразу), а не как
триггер lazy on-chain payout. Полная L12 (отдельная транзакция
o.mkt.payout на confirm) — требует разделения signchair и payout в
C++ контракте; техдолг PRD/FR57.
- Реестр marketplace-scoped (PG-only). Синхронизация с core
outgoing_payment (AR35) — отдельным follow-up'ом со связкой
core/gateway-API.
- On-chain submit signsupp/signchair с реальной подписью Document2
через AR33 document factory — FR45 техдолг, отдельный коммит после
настройки инфраструктуры подписания.
- Push-уведомления кассиру при создании запроса и поставщику при
подтверждении — обвязка через notification-module.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-8][@ant] feat: закрыть техдолги Эпика 5 — barcode_strategy на Offer, AR35 core sync, FR45 signing, UI SDK + каркасы 6 столов — чтобы Эпик 5 был cohesive и не висел недоделками
Закрывает блокеры завершения Эпика 5 «Поставка и приёмка имущества на КУ»
(см. план в `_blago/.../598-8-epik-5-...md`, секция «Sprint-план доделки»).
598-22 — barcode_strategy + pack_size на marketplace_offer:
- ALTER через TypeORM synchronize + bootstrap-v5 миграция для bump версии;
- Domain Offer entity + types + mapper + repo create/update;
- Валидация PER_PACKAGE → pack_size (диапазон 1..1000);
- @InputType расширен enum MarketplaceBarcodeStrategy + pack_size;
- MarketplaceInventoryLabelService теперь читает стратегию из Offer
(per-call параметр оставлен как admin-override);
- +5 unit-тестов для новых валидаций; toMarketplaceOfferDTO вынесен
в shared helper, убраны 3 дублирующих локальных toDTO.
598-17 / AR35 — синхронизация marketplace выплат с core outgoing_payment:
- Payment entity дополнен опциональными related_extension /
related_entity_id, PaymentDomainInterface — теми же полями;
- GatewayInteractor.createSystemOutgoingPayment без statement/method_id
(идемпотентность через payment_hash, source = extension);
- GatewayInteractorPort + оба адаптера расширены новым методом;
- marketplace_outgoing_payment_request получил core_payment_id колонку
и applyCorePaymentId / deleteById repository-методы;
- AplReceptionService.createOutgoingPaymentRequest вызывает core
с compensating delete на core-fail; OutgoingPaymentService на
confirm/block отзеркаливает статус в core (COMPLETED / CANCELLED).
598-15 — FR45 signing infrastructure для signsupp/signchair:
- MarketplaceAplReceptionDocumentFactory строит unsigned Document2
per-Order с meta (schema, reception_id, fact_quantity, accept_braname);
- Queries marketplaceAplReceptionSupplierSignablePayloads /
marketplaceAplReceptionChairmanSignablePayloads возвращают payload'ы
клиенту для подписания приватным ключом;
- Mutations marketplaceSignAplReception* расширены опциональным
signed_documents (массив signed Document2 per-Order);
- При наличии signed_documents AplReceptionService.submitOnChainSign*
отправляет реальные chainPort.signSupp/signChair и сохраняет
реальный tx_hash; compensating-rollback при on-chain fail (статус
АПП не меняется, ошибка наверх — клиент решает retry);
- Backwards-compat: без signed_documents сохраняется placeholder tx.
598-18 — SDK regenerated с операциями Эпика 5 + 6 UI-pages каркас:
- pnpm generate-schema + generate-client + sdk build (пятишаговая
процедура feedback_graphql_no_raw_strings_desktop);
- Selectors: shipment, inventory, aplReception (+signable payload),
outgoingPayment;
- Mutations.Marketplace: CreateShipment, LabelInventory,
CreateAplReception, SignAplReceptionAsSupplier,
SignAplReceptionAsChairman, ConfirmOutgoingPayment,
BlockOutgoingPayment;
- Queries.Marketplace: ListShipments, GetShipment, ListInventory,
ListAplReceptionsByKu, ListAplReceptionsAsSupplier,
ListOutgoingPaymentsForCashier, ListOutgoingPaymentsAsSupplier,
AplReceptionSupplierSignablePayloads,
AplReceptionChairmanSignablePayloads;
- 6 pages в pages/Marketplace/{OffererSupplyPreparation,
OffererPendingAplReceptions, OffererPaymentHistory,
OperatorReception, OperatorInventoryLabeling,
CashierOutgoingPayments} — каркасы с импортом @coopenomics/sdk и
per-role классами; полная интеграция с canonical widgets
(ExpeditorGroupingBoard, TTNPrintPreview, BarcodeScanner) —
вторым UI-PR'ом после визуальной приёмки в браузере.
598-21 — batch-маркировка штрих-кодом:
- OperatorInventoryLabeling-стол поддерживает режим «печать партии»:
все этикетки текущего КУ выводятся в одну CSS Grid с break-inside:
avoid + @media print 2-колончатая разметка для печати на термопринтере.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-20][@ant] feat: push-уведомления marketplace flow — поставщик/кассир/поставщик в 3 точках Эпика 5 — чтобы вариант Б АПП не висел polling'ом и кассир сразу видел новые задачи
— 3 Novu workflow в @coopenomics/notifications:
marketplace-apl-supplier-sign-request, marketplace-cashier-new-payment,
marketplace-supplier-payment-confirmed. Email + InApp + Push шаги,
payload-схемы (z.object) + slugify(name) → workflowId.
— MarketplaceNotificationService с 3 @OnEvent listener'ами:
слушает event-bus EventEmitter2 (marketplace.aplReception.b.supplier.signRequested,
marketplace.outgoingPayment.cashier.newTask,
marketplace.outgoingPayment.supplier.confirmed),
получает аккаунт через ACCOUNT_DATA_PORT, дёргает NOVU_WORKFLOW_PORT.
Ошибки Novu/missing subscriber_id — warn без блокировки flow (INV-12:
emit ПОСЛЕ save в PG, не блокирующий).
— Emit-точки в marketplace-services после save:
• MarketplaceAplReceptionService.create — push поставщику для
Варианта Б (EXPEDITOR), для Варианта А поставщик подписывает на
стойке оператора — push не нужен.
• MarketplaceAplReceptionService.signAsChairman — push кассиру
после createOutgoingPaymentRequest успеха (compensating-rollback
при core-fail убирает запись — push в этом случае не идёт).
• MarketplaceOutgoingPaymentService.confirm — push поставщику
после фиксации в LEDGER_RECORDED.
— MVP-ограничение: кассиром выступает chairman кооператива (role:
'chairman'); когда появится extension-роль cashier — поменять
фильтр в MarketplaceNotificationService.handleCashierNewPayment.
Закрывает 598-20. Из 7 техдолгов Эпика 5 в этом PR'е закрыто 6
(598-22, 598-17, 598-15, 598-18, 598-21, 598-20). 598-16 (L12 split
signchair/payout в C++ marketplace::supply) остаётся cross-repo
(mono-ai-5 contracts) — выходит за scope PR #388.
* [598-20][@ant] refactor: централизовать кассира в gateway по L12 — снять marketplace-only ручки confirm/markBlocked и подцепиться к payconfirm/paydecline через delta — кассир работает на весь кооператив, marketplace только зеркалит результат
Поток L12 (PR #389):
- backend marketplace::payout → inline gateway::createoutpay (outcome pending)
- кассир в общем столе gateway подтверждает банковский перевод → callback marketplace::payconfirm
- marketplace::paydecline при отказе кассира с reason
Listener (MarketplacePayoutSyncService) переводит per-Order projection PENDING → COMPLETED/DECLINED + setPaymentStatus в core + emit push поставщику.
Backend:
- entity/repository/mapper marketplace_outgoing_payment_request: 1 запись = 1 Order (order_hash unique), статусы PENDING/COMPLETED/DECLINED
- удалён MarketplaceOutgoingPaymentService.confirm/markBlocked + resolver мутации и query listForCashier — кассирского стола в marketplace нет
- MarketplaceAplReceptionService.signAsChairman: per-Order payOut() через chainPort + createSystemOutgoingPayment в core + projection PENDING (вместо агрегированной заявки)
- Novu workflow marketplace-supplier-payment-declined + событие MARKETPLACE_SUPPLIER_PAYMENT_DECLINED_EVENT
Frontend / SDK:
- удалён desktop стол CashierOutgoingPayments (кассир в gateway-расширении)
- удалены SDK mutations confirmOutgoingPayment/blockOutgoingPayment и query listOutgoingPaymentsForCashier
- selector marketplaceOutgoingPaymentRequest обновлён под per-Order поля
- regenerate schema + zeus client + sdk build зелёный
Resolves: ревью PR #388 (кассир централизован), часть E11 техдолга 598-16
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-20][@ant] refactor: документы АПП приёмки переведены на канон factory+SignedDocument — снять локальную самодельную обвязку Document2 (signature.info, builder hash) и подключиться к GENERATOR_PORT + ядерный SignedDigitalDocumentInputDTO как у participant/capital, плюс обязательная подпись на mutation подписания
Что было не по канону (ревью PR #388):
- локальная фабрика marketplace-apl-reception-document-factory.ts с самописным sha256(meta||doc_hash);
- MarketplaceSignatureInfoInputDTO / MarketplaceSignedDocumentInputDTO дублировали SignedDigitalDocumentInputDTO ядра;
- signed_documents nullable + placeholder-tx fallback на backend (документ мог не передаваться вообще);
- output query — кастомный SignablePayload, не GeneratedDocument из ядра.
Канон (как в participant/capital):
- cooptypes: новый registry 1102.MarketplaceAplReception (Action extends IGenerate, Model + html placeholder + переводы);
- controller/application/document/documents-dto/marketplace-apl-reception-document.dto.ts — Base + Generate + SignedMeta + SignedDocumentInput через IntersectionType с GenerateMetaDocumentInputDTO и MetaDocumentInputDTO;
- preview формируется через DocumentDomainService.generateDocument с registry_id=1102 per-Order;
- сервис принимает MarketplaceAplReceptionSignedDocumentInputDTO[], верифицирует подпись через @wharfkit/antelope (PublicKey.from + Signature.verifyDigest), индексирует документы по meta.order_id;
- signed_documents — обязательное (ArrayMinSize(1)), placeholder-режим удалён;
- resolver возвращает [GeneratedDocument!]!;
- module marketplace-application подключает DocumentDomainModule.
Заодно убран any в extract tx hash (G задача из ревью) — типизирован через узкий object literal.
Frontend OffererPendingAplReceptionsPage временно показывает уведомление «UI подписи в разработке» — backend ждёт реальную подпись пайщика через wif (диалог подписания — следующий этап).
Resolves: ревью PR #388 (документы по канону, A + G), удаляет техдолг FR45 на backend.
* [598-20][@ant] refactor: ku_id → braname в marketplace Эпика 5 — выровнять имена с контрактом, где приёмный КУ описывается полем accept_braname, а сам идентификатор КУ — это просто braname; «ku_id» путал агентов и десинхронизировал backend ↔ on-chain поля
Затронуты все слои Эпика 5: shipment / inventory / apl-reception в controller + sdk + desktop.
- domain entity/types: поле и геттеры braname вместо ku_id;
- TypeORM column + индексы: `IDX_marketplace_shipment_cycle_braname_unique` (был cycle_ku) + per-table covered index по (coopname, braname, status);
- repository: findByBraname / listByBraname (вместо ByKu);
- application services + DTO + resolvers: `marketplaceListAplReceptionsByBraname` (query name), `@Args('braname')`;
- SDK: queries `ListAplReceptionsByBraname` (файл переименован), selectors с полем `braname`;
- Desktop: `fetchInventoryByBraname`, view-интерфейсы (operator-inventory + apl-reception);
- schema.gql + zeus client регенерированы.
В dev-стеке `synchronize: true` подхватит column-rename автоматически; для prod-окружения отдельная двухэтапная миграция (ADD braname → backfill ← copy from ku_id → DROP ku_id) — вне scope этого PR, пока БД stateless для разработки.
Resolves: ревью PR #388 (C — ku_id → braname).
* [598-20][@ant] refactor: канон data/input + pug в шаблонах Эпика 5 — все мутации/queries marketplace принимают единый @Args('data', InputDTO) как в core/meet, голые скаляры обёрнуты в Input; шаблоны 5 столов переведены на pug — выровнять стиль с остальной частью desktop
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-8][@ant] refactor: новые столы Эпика 5 переведены на SuccessAlert/FailAlert/NotifyAlert — убрать сырой Notify.create в пользу канона src/shared/api; ошибки идут через FailAlert(e, 'кастомный текст'), успех через SuccessAlert, заглушки UI-этапа — через NotifyAlert; кейс ревью PR #388 (OffererSupplyPreparationPage:22)
* [598-8][@ant] feat: акт АПП registry 1102 — содержание 1-к-1 из 802.ReturnByAssetAct, переменные адаптированы под marketplace — текст и переводы юр.отдела выверены; никакой самодеятельности с placeholder-шаблоном; в Action добавлены act_id и transmitter, сервис проставляет их при генерации (act_id формируется из reception+order short-id, transmitter — account поставщика для строки «Передал заказ»); кейс ревью PR #388 (1102.MarketplaceAplReception:1)
* [598-8][@ant] feat: Story 5.4 ТТН — реальная генерация через document-factory (registry_id=1103) + локальный реестр marketplace_ttn_document, Shipment.ttn_document_id вместо MVP-stub pdf_url — закрыть замечание ревью «MVP-stub в shipment-create.service.ts:321»: при формировании партии Варианта Б backend факторит документ ТТН тем же путём, что и АПП (DocumentDomainService.generateDocument с Cooperative.Registry.MarketplaceTransportNote.Action), и сохраняет в отдельной marketplace-таблице — общий реестр документов кооператива не трогаем, экспедиторы пока не пайщики и подписывают перевозку вне платформы; из Shipment удалены ttn_pdf_url и ttn_document_registry_id, заменены на ttn_document_id (uuid → marketplace_ttn_document.id); кейс ревью PR #388 (marketplace-shipment-create.service:321)
* [598-8][@ant] chore: regenerate schema.gql + SDK zeus + shipment selector после правок ревью PR #388 — pnpm run generate-schema + generate-client; в SDK zeus теперь нет ttn_pdf_url/ttn_document_registry_id (удалены из MarketplaceShipment DTO), добавлен ttn_document_id; shipmentSelector.ts руками поправлен под новый набор полей; SDK typecheck и build зелёные
* [598-8][@ant] fix: desktop typecheck — починить рассинхрон tsc/vue-tsc + убрать pug-мусор — без vue-tsc и с trailing \ в шаблонах PR не катит тайпчек
— components/desktop/package.json: `typecheck` возвращён на `vue-tsc --noEmit --skipLibCheck` (был «hard update» 2025-10-14 на чистый tsc; tsc не парсит .vue и не видит named re-export типов из них — отсюда 21 ошибка TS2614 на widget index.ts из Эпика 10).
— 4 страницы Marketplace (OffererPaymentHistory/OffererPendingAplReceptions/OffererSupplyPreparation/OperatorReception): убраны trailing `\` внутри `:columns="[...]"` — pug не требует line-continuation внутри `()`, vue-tsc ругался TS1127 Invalid character.
— components/sdk/src/queries/marketplace/listOutgoingPaymentsAsSupplier.ts: `query` приведён к канону остальных queries (объект через `$()`, не функция (input)=>Selector(...)); desktop вызывал как объект — отсюда TS2560 «Did you mean to call it?».
— components/desktop/tsconfig.vue-tsc.tsbuildinfo: удалён tracked билд-кэш vue-tsc.
* [598-8][@ant] feat: паттерн doc_data в @coopenomics/factory + ввести 1102/1103 как канонные документы — приватные данные документа off-chain, on-chain только sha256 hash, шаблон обращается через зарезервированную {{ doc_data.* }}
Закрывает крайнее ревью PR #388 (2026-05-17):
— components/factory/src/Services/DocData/index.ts: новый DocDataService — sha256 + Mongo upsert по hash (идемпотентный save), get по hash (null если запись удалена — graceful degradation).
— components/factory/src/Factory/index.ts: в DocFactory методы saveDocData/getDocData/loadDocData; factory автоматически даёт фабрике помощник для подгрузки приватного payload по data.doc_data_hash.
— components/factory/src/index.ts: Generator-фасад выставляет saveDocData/getDocData в IGenerator.
— components/cooptypes/src/cooperative/document/index.ts: IDocDataRef { doc_data_hash?: string } — зарезервированная ссылка, которую Action любого нового документа расширяет.
— components/factory/README.md + AGENTS.md: раздел «Приватные данные документа: doc_data» с потоком, зарезервированными именами и graceful degradation.
— components/factory/test/doc-data.test.ts: unit — sha256, идемпотентность, стабильный порядок ключей, null при удалении, делегирование через Generator-фасад.
1102.MarketplaceAplReception:
— cooptypes registry/1102: Action упрощён до канона 802.ReturnByAssetAct (registry_id, order_id, order_hash, act_id, transmitter, braname?, doc_data_hash?); семантика — `user` = пайщик-получатель (USERNAME), `transmitter` = USERNAME председателя КУ или доверенного им лица, передающего имущество (ФИО подставляется factory через getUser → getFirstLastMiddleName). Текст и переводы — 1-в-1 с 802 (юр.выверенный текст). Никаких «филиалов».
— factory/Actions/1102, factory/Templates/1102: каноничная фабрика по образцу 802.ReturnByAssetAct (factory.getUser(username) → пайщик, factory.getUser(transmitter) → председатель); decision для marketplace — стаб (нет протокола совета на каждый order).
— controller/extensions/marketplace/.../marketplace-apl-reception.service.ts: при сборке Action в Action идут только канонические поля; ФИО пайщика берётся по order.orderer_account, transmitter = chairman_account ?? created_by_operator_account.
1103.MarketplaceTransportNote:
— cooptypes registry/1103: Action очищен — { ttn_number, cycle_id, shipment_id, accept_braname, supplier_account, total_amount, currency, doc_data_hash }. Все персональные данные экспедитора (ФИО, паспорт, телефон, госномер, адрес погрузки, время доставки) вынесены в interface PrivateData — payload off-chain.
— Шаблон обращается через {{ doc_data.expeditor_full_name }}, {{ doc_data.vehicle_number }} и т.п.
— factory/Actions/1103, factory/Templates/1103: фабрика загружает приватный payload через loadDocData; ошибка если doc_data_hash не задан или payload удалён.
— controller/extensions/marketplace/.../marketplace-shipment-create.service.ts: убран MVP-stub — приватные поля экспедитора сохраняются через documentDomainService.saveDocData(...) → doc_data_hash → in Action; on-chain Action не содержит персональных данных.
Прочие документы (700-1099, 300-304, 50-51, …) остаются как были — паттерн прогрессивный, для новых документов с PII и при явной потребности.
Архитектурный артефакт (_bmad-output/planning-artifacts/architecture.md в blago проекте 1-prilozhenie-stol-zakazov): дополнен разделом «Document Generation Pattern: doc_data (Private Document Data Store)». Story 5.8 заведена в blago через `blago create req`.
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-7][@ant] feat: добавить создание Order с атомарной серией блока средств — закрыть Story 4.1 Эпика 4
Backend (controller/extensions/marketplace):
- MarketplaceOrderDomainEntity (composite db + bc; IBlockchainSynchronizable) +
types (cycle_type / status / create_tx snapshot) + repository interface +
TypeORM entity + mapper + delta-mapper для marketplace::orders +
adapter (persistAfterBlock / findBySyncKey / sync-fork API).
- MarketplaceCanonicalBlockchainPort + adapter — submit canonical
Actions.CreateOrder (cooptypes из PR #385 / Story 11.1 PR #375); подпись
кооператива через VaultDomainService по data.coopname.
- MarketplaceOrderSyncService extends AbstractEntitySyncService: подписка
на `delta::marketplace::orders` через EventEmitter2 + fork handler с
компенсирующим offerCounters.onOrderRolledBack для Order'ов в block-state.
- MarketplaceOrderCreateService: guard FR11a (Offer existence/ACTIVE/
quantity_available); optimistic onOrderBlocked → chain createorder
(o.wal.conv → o.mkt.assign → o.mkt.block в ledger2::apply) → compensating
onOrderRolledBack при chain-failure → persist PG с tx_snapshot.
- GraphQL Mutation marketplaceCreateOrder (через DTO/Input + Resolver)
под Order:create marketplace-access-matrix.
- Wiring marketplace-infrastructure.module + marketplace-application.module.
Tests:
- 7 сценариев composite-entity (db-only, db+bc, sync-key mismatch, update bc
без затрагивания backend полей, derived флаги для каждого статуса,
hash normalization, hash валидация).
- 7 сценариев create-service (Guard FR11a 4 ветки, quantity ≤ 0,
compensating rollback при chain submit fail, happy path с проверкой
всех 11 полей ICreateOrder payload).
UI (desktop/widgets/Marketplace/CreateOrderDialog):
- TakeoverDialog с формой quantity + KU-выбор через KUMapWithList +
total-cost preview + mp-role-orderer обёртка; токены только через
marketplace-tokens.scss; CSS-vars / no hard-coded colors.
Не входит в Story 4.1 (отложено по scope):
- Apollo-codegen интеграция CreateOrderDialog в MarketplaceCatalogPage —
Story 4.6 («Мои заказы» нужен apollo) подключит mutation marketplaceCreateOrder
и WalletTimeline mount после успеха.
- GraphQL Subscription marketplaceOrderUpdated — Story 4.6 для live-tracking.
- Удаление legacy controller/application/marketplace/* (interactor/service/
resolver под клиринг) — отдельный refactor-PR; TSC уже сломан этим
legacy после PR #385 (canonical cooptypes), не блокирующий Story 4.1
canonical layer.
- Access-matrix регистрация Order:create permission — в общем update
Story 4.6.
Spec и контракт интеграции — _bmad-output/implementation-artifacts/
spec-3-4-bc-integration.md (Эпик 3 → Эпик 4) + controller/CLAUDE.md
(Composite-entity / Dispatch pipeline / Fork handling).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-8][@ant] feat: добавить cycle-type-aware агрегацию Order'ов в консолидированную заявку — закрыть Story 4.2 Эпика 4
Backend-only (Locked Decision L10): on-chain представления заявки НЕТ;
агрегация целиком в PG. Order'ы привязываются через
marketplace_order.cycle_id.
Поведение per cycle_type (Locked Decision L11):
- time_based: cron `@Cron(EVERY_5_MINUTES)` сканирует ACTIVE Offer'ы
с cycle_type='time_based' + cycle_days; для каждого находит cycle_start
как min(blocked_at) среди unassigned active Order'ов; если now >=
cycle_start + cycle_days решает:
sum >= min_threshold (или null) → consolidated_request status=
'PENDING_SUPPLIER_ACCEPT' + expires_at = now + 48ч + assignToCycle
Orders → 'ACCEPTED_PENDING_SUPPLIER';
sum < min_threshold → consolidated_request status='EXPIRED_NO_THRESHOLD'
БЕЗ assignToCycle (Story 4.3 unblk per-Order'ам пула).
- volume_based: метод `evaluateVolumeBasedAfterCreate` вызывается
СИНХРОННО из `MarketplaceOrderCreateService.applyCycleTypeHook` после
persist нового Order'а; если sum >= target_volume → instant
consolidated_request status='PENDING_SUPPLIER_ACCEPT'. Cron-fallback
`aggregateVolumeBasedExpired` (EVERY_HOUR) — если объём не накоплен
и now > cycle_start + max_wait_days → consolidated_request status=
'EXPIRED_NO_VOLUME' (Story 4.3 unblk).
- open_subscription: backend НЕ создаёт автоматически. Mutation
`marketplaceTriggerOpenSubscription` (Offer:update:own); поставщик
жмёт «Запустить» → consolidated_request status='ACCEPTED' сразу,
Orders пула → 'ACCEPTED' (нажатие = акцепт всего пула).
- individual: backend НЕ агрегирует. `applyCycleTypeHook` в
OrderCreateService делает Order.status: ACTIVE →
'ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL' сразу после persist.
Backend (controller/extensions/marketplace):
- domain/entities/marketplace-consolidated-request.{types,entity}.ts +
spec (4 сценария: PENDING/ACCEPTED/terminal-таблица/open_subscription
triggered).
- domain/repositories/marketplace-consolidated-request.repository.ts +
infrastructure (TypeORM entity + mapper + adapter; индексы под
offerer-стол / pre-aggregator idempotency / cron-scan).
- application/services/marketplace-cycle-aggregator.service.ts —
@Cron(EVERY_5_MINUTES) aggregateTimeBased + @Cron(EVERY_HOUR)
aggregateVolumeBasedExpired + evaluateVolumeBasedAfterCreate +
triggerOpenSubscription. ScheduleModule.forRoot() добавлен в
MarketplaceExtensionApplicationModule imports.
- application/resolvers/marketplace-cycle.resolver.ts — Mutation
marketplaceTriggerOpenSubscription + Query marketplaceListConsolidatedRequests
(Offer:update:own / Offer:read access).
- Расширены MarketplaceOrderDomainRepository (findUnassignedActiveByOffer
/ assignToCycle / sumUnassignedActiveByOffer) и
MarketplaceOfferDomainRepository (listAllActiveTimeBased /
listAllActiveVolumeBased).
- MarketplaceOrderCreateService.applyCycleTypeHook — per-cycle_type
backend hook сразу после persist Order'а; для individual instant
transition status, для volume_based — instant аггрегация при пороге.
Tests (10 сценариев):
- consolidated-request.entity.spec: PENDING/ACCEPTED/4 terminal/
open_subscription triggered.
- cycle-aggregator.spec: aggregateTimeBased (3: достигнут + sum >= threshold;
достигнут + sum < threshold; не достигнут), evaluateVolumeBasedAfterCreate
(2: ниже target / достигнут), triggerOpenSubscription (4: happy /
ForbiddenException не владелец / BadRequestException неправильный
cycle_type / BadRequestException пустой пул).
- OrderCreateService spec обновлён под новый constructor signature
(cycleAggregator dependency).
Не входит в Story 4.2 (отложено):
- UI для offerer-стола «Консолидированные заявки» (Story 4.5 supplier
accept/decline UI) — там же добавится списочный widget на канон
дизайн-системы.
- Mutation marketplaceAcceptConsolidatedRequest / DeclineConsolidatedRequest —
Story 4.5.
- Auto-decline по expires_at (PENDING > 48ч) — Story 4.3 cron-cleanup.
- Persist полей `acceptance_window_hours` в config кооператива (сейчас
захардкожено 48ч) — Story 9.x.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-9][@ant] feat: добавить auto-unblk Order'ов при провале триггера и cleanup непринятых заявок — закрыть Story 4.3 Эпика 4
Continuation Story 4.2 (PR #386? коммит 0ba2d51): на cron-агрегаторе и
volume_based-cron при достижении terminal-condition (sum < min_threshold
для time_based, sum < target_volume для volume_based, expires_at < now
для PENDING_SUPPLIER_ACCEPT) теперь автоматически разблокируются средства
каждого Order'а пула — без участия пайщика-заказчика или поставщика.
Что сделано (per acceptance criteria Story 4.3):
1. Расширен canonical port (`MarketplaceCanonicalBlockchainPort`) +
adapter (`MarketplaceCanonicalBlockchainAdapter`) методом
`expireOrder({coopname, order_hash})` — обёртка над C++
`marketplace::expireorder` (canonical action из PR #385 Story 11.1;
authorization = кооператив, action_name = 'expireorder', серия
o.mkt.unblk на total_cost + on-chain Order.status: active → cancelled).
2. Расширен Order-domain-repository: `findByCycleId(coopname, cycle_id)`
— все Order'ы привязанные к данной консолидированной заявке. Нужно
для нового cron-cleanup'а — нужно итерироваться по пулу заявки,
которая не дождалась ответа поставщика.
3. `MarketplaceCycleAggregatorService` теперь полноценно завершает
цикл при terminal-condition:
- В `processTimeBasedOffer` после persist `consolidated_request`
статуса `EXPIRED_NO_THRESHOLD` (sum < min_threshold) — серия
`expireOrder` per-Order пула + counter `onOrderUnblocked` +
Order.status → `EXPIRED_NO_THRESHOLD`.
- В `aggregateVolumeBasedExpired` аналогично, но Order.status →
`EXPIRED_NO_VOLUME` при sum < target_volume + now > cycle_start
+ max_wait_days.
- Новый `@Cron('*/10 * * * *') expireUnacceptedPending()` — каждые
10 минут сканирует `PENDING_SUPPLIER_ACCEPT` заявки с
`expires_at < now` (поставщик не нажал ни Accept ни Decline в
48-часовом окне): переводит заявку в `EXPIRED_NO_RESPONSE` с
`decline_reason='expired_no_response'` + серия `expireOrder` пулу
+ Order.status → `CANCELLED_BY_SUPPLIER` с тем же reason.
4. Private helper `expirePoolOnChain(pool, terminalStatus, reason)` —
инкапсулирует pattern «per-Order chain.expireOrder → counter
unblock → applyStatusTransition». Best-effort с partial-fail
tolerance: chain-fail одного Order'а лог error + продолжаем
остальные (следующий cron-тик подберёт повторно для time_based /
volume_based; expireUnacceptedPending же — single-shot, mismatch
админ-reconciliation Story 9.x). Counter-fail (Offer уже неактивен
и т.п.) — лог warn + всё равно applyStatusTransition (on-chain unblk
прошёл; единственное место рассинхронизации — quantity_available
Offer'а, восстанавливается reconciliation).
5. Side-by-side с `marketplace-order-delta.mapper.ts` поправлен
устаревший импорт `Interfaces.IOrderRow` → `Interfaces.Marketplace.IOrder`
(after PR #385 — единый `IOrder` тип).
Тесты (`marketplace-cycle-aggregator.service.spec.ts`):
Расширены mocks (chainPort + offerCounters + findByCycleId +
findExpiredAwaitingResponse + applyStatusTransition на orderRepo).
Добавлены 5 новых сценариев Story 4.3:
- **EXPIRED_NO_THRESHOLD pool unblk** — time_based ниже порога:
`chainPort.expireOrder` per-Order × N, `onOrderUnblocked` per-Order × N,
`applyStatusTransition(_, 'EXPIRED_NO_THRESHOLD', _)` × N.
- **EXPIRED_NO_VOLUME pool unblk** — volume_based по истечении
`max_wait_days` без `target_volume`: аналогично.
- **partial chain fail tolerance** — один Order падает на `expireOrder`,
остальные `succeeded`; лог error по failed, applyStatusTransition не
вызывается на failed Order'е.
- **counter onOrderUnblocked fail — applyStatusTransition всё равно** —
Offer удалён / неактивен → counter throws, Order всё равно идёт в
terminal-status (on-chain unblk уже прошёл).
- **expireUnacceptedPending happy + empty** — pending > 48ч заявка →
`EXPIRED_NO_RESPONSE` + Order'ы → `CANCELLED_BY_SUPPLIER`. Пустой
список — early return без расходов.
Существующий «EXPIRED_NO_THRESHOLD без assignToCycle» расширен Story 4.3
ассертами (expireOrder × 2, onOrderUnblocked × 2, applyStatusTransition × 2).
НЕ сделано в Story 4.3 (отложено):
- **acceptance_window_hours в config кооператива** — захардкожено 48ч,
задача Story 9.x (общая параметризация кооператива).
- **Реальные интеграционные тесты с C++ marketplace::expireorder** —
blockchain-side test через nodeos staging, Story 4.6 / CI e2e.
- **Manual reconciliation endpoint** для partial-fail случаев,
которые cron не подобрал — Story 9.x (admin tool).
- **GraphQL Subscription marketplaceOrderUpdated** — pubsub-канал
Order при auto-expire должен пушиться, но Story 4.1 sync-сервис
подписан на `delta::marketplace::orders` (on-chain expire триггерит
дельту через parser2 → syncer → pubsub). Прямой emit pubsub из
application-сервиса — anti-pattern (INV-12).
Файлы:
- `domain/ports/marketplace-canonical-blockchain.port.ts` — +expireOrder
- `domain/repositories/marketplace-order.repository.ts` — +findByCycleId
- `infrastructure/adapters/marketplace-canonical-blockchain.adapter.ts`
— +expireOrder impl
- `infrastructure/adapters/marketplace-order-repository.adapter.ts`
— +findByCycleId impl
- `infrastructure/mappers/marketplace-order-delta.mapper.ts` —
Interfaces.IOrderRow → Interfaces.Marketplace.IOrder fix
- `application/services/marketplace-cycle-aggregator.service.ts` —
constructor +chainPort +offerCounters, processTimeBasedOffer и
aggregateVolumeBasedExpired вызывают expirePoolOnChain, +cron
expireUnacceptedPending, +private expirePoolOnChain
- `application/services/marketplace-cycle-aggregator.service.spec.ts`
— 5 новых сценариев Story 4.3
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-10][@ant] feat: добавить отмену Order'а заказчиком до акцепта поставщика — закрыть Story 4.4 Эпика 4
Story 4.4 закрывает разрыв между Story 4.1 («заказчик создаёт Order и
блокирует средства») и Story 4.5 («поставщик акцептует или declined'ит»):
до того, как поставщик нажал Accept (либо до того, как time/volume-
agregator закрыл цикл за него), заказчик может отозвать свой Order сам.
Реализация:
1. Canonical port + adapter: `cancelOrder({coopname, orderer, order_hash})`
— обёртка над C++ `marketplace::cancelorder` (action_name='cancelorder',
authorization = кооператив; C++ внутри `require_auth(coopname)` +
`eosio::check(actor == order.orderer)` + status==ACTIVE).
Серия ledger2 `UNBLOCK_ON_CANCEL` (o.mkt.unblk на total_cost) +
on-chain Order.status: ACTIVE → CANCELLED. Сумма возвращается на
`w.mkt.member.available` пайщика — может быть потрачена на новый
заказ или выведена через `o.mkt.recall`.
2. Новый application service `MarketplaceOrderCancelService`:
**Guard** — Order существует / coopname match / orderer match /
status='ACTIVE'. Backend разрешает отмену только в ACTIVE (это
match с C++ check). После того, как:
- cron `aggregateTimeBased` / `aggregateVolumeBasedExpired`
перевёл Order в `ACCEPTED_PENDING_SUPPLIER`, или
- persist-hook `applyCycleTypeHook` перевёл в
`ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL` (individual cycle),
заказчик не может отменить сам — поставщик либо акцептует, либо
declined'ит через Story 4.5. Это защищает поставщика от
«закаривал-передумал» паттерна на пограничном acceptance-окне.
**Chain submit** — `chainPort.cancelOrder({coopname, orderer,
order_hash})`. Любой C++ check fail (например parallel acceptorder
race) — clean `BadRequestException` с extracted assertion message;
ни counter, ни Order.status не модифицируются.
**Counter** — `offerCounters.onOrderUnblocked(offer_id, quantity)`
best-effort: counter-fail (Offer уже не ACTIVE) → лог warn + всё
равно applyStatusTransition (on-chain unblk прошёл).
**Persist** — Order.status: ACTIVE → CANCELLED_BY_ORDERER с reason
'Отменён заказчиком'. `cancelled_at` = now (через
applyStatusTransition).
3. GraphQL Mutation `marketplaceCancelOrder({order_id})` →
`MarketplaceCancelOrderResult{order, tx_hash}`. ACL
`@RequireMarketplaceAccess('Order', 'cancel:own')` — marketplace-role
orderer + owner-check внутри сервиса (через guard orderer match).
Полная регистрация permission'а — в Story 4.6 access-matrix update.
4. UI: канон `OrderCard.vue` (`components/desktop/src/widgets/Marketplace/
OrderCard`) — для orderer-роли в статусе `placed` (mapping канона
для backend.ACTIVE) добавлена кнопка `{key:'cancel', label:'Отменить',
kind:'danger'}`. Parent-страница (Story 4.6 «Мои заказы») перехватит
`@action` событие с `key='cancel'` и оркестрирует confirm dialog +
apollo `marketplaceCancelOrder` mutation. OrderCard сам по
дизайн-канону не диалогизирует — лишь публикует event.
Тесты (`marketplace-order-cancel.service.spec.ts`, 8 сценариев):
- **happy path** — chain.cancelOrder с {coopname, orderer, order_hash} →
counter.onOrderUnblocked(offer_id, quantity) → applyStatusTransition
(order.id, 'CANCELLED_BY_ORDERER', 'Отменён заказчиком'); result
включает tx_hash и Order DTO.
- **NotFound** — Order не существует.
- **Forbidden** — Order чужого кооператива.
- **Forbidden** — отменяет не заказчик-владелец.
- **BadRequest для статусов** — `ACCEPTED_PENDING_SUPPLIER`,
`ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL`, `ACCEPTED`, `RECEIVED`,
`CANCELLED_BY_ORDERER`: jest.each покрывает 5 ветвей "статус ≠ ACTIVE".
- **chain fail — clean BadRequest** — Order не модифицирован,
counter не дёрнут.
- **counter fail — applyStatusTransition всё равно** — best-effort
(on-chain unblk уже прошёл, counter рассинхрон правится
reconciliation Story 9.x).
НЕ сделано (отложено):
- **Confirm dialog UI** — родительская страница «Мои заказы» Story 4.6
обернёт `@action(cancel)` в `q-dialog` с подтверждением. На Story 4.4
OrderCard.vue лишь публикует event (per канон-разделение).
- **GraphQL Subscription marketplaceOrderUpdated** — pubsub Order при
CANCELLED_BY_ORDERER должен идти по `delta::marketplace::orders`
(parser2 → syncer → pubsub). Прямой emit из application — anti-pattern
(controller/CLAUDE.md INV-12).
- **Apollo client mutation hook + composable** — клиентская часть
Story 4.6 («Мои заказы»).
- **Полная регистрация Order:cancel:own permission в access-matrix** —
Story 4.6 access-matrix-update package.
Файлы:
- `domain/ports/marketplace-canonical-blockchain.port.ts` — +cancelOrder
- `infrastructure/adapters/marketplace-canonical-blockchain.adapter.ts`
— +cancelOrder impl (паттерн как expireOrder/createOrder)
- `application/services/marketplace-order-cancel.service.ts` — новый
- `application/services/marketplace-order-cancel.service.spec.ts` —
новый (8 сценариев)
- `application/dto/marketplace-order-input.dto.ts` — +MarketplaceCancelOrderInputDTO
- `application/dto/marketplace-order.dto.ts` — +MarketplaceCancelOrderResultDTO
- `application/resolvers/marketplace-order.resolver.ts` —
+marketplaceCancelOrder Mutation
- `application/marketplace-application.module.ts` — providers + exports
- `components/desktop/src/widgets/Marketplace/OrderCard/OrderCard.vue`
— orderer.placed += cancel/danger кнопка
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-11][@ant] feat: добавить accept/decline поставщика по cycle_type — закрыть Story 4.5 Эпика 4
— расширил port + adapter методами acceptOrder/declineOrder (canonical actions из cooptypes PR #385)
— MarketplaceConsolidatedRequestAcceptDeclineService для batch (time_based/volume_based): per-Order chain.acceptOrder | chain.declineOrder + counter.onOrderUnblocked + applyStatusTransition + consolidated_request → ACCEPTED|DECLINED_BY_SUPPLIER (L10)
— MarketplaceOrderSupplierActionService: acceptIndividual / declineIndividual для cycle_type=individual + declineFromOpenPool для частичного отказа из open_subscription до запуска поставки
— 5 GraphQL Mutation: marketplaceAcceptConsolidatedRequest / marketplaceDeclineConsolidatedRequest / marketplaceAcceptIndividualOrder / marketplaceDeclineIndividualOrder / marketplaceDeclineOrderFromOpenPool с ACL Offer:update:own
— DTO: MarketplaceConsolidatedRequestActionResult / MarketplaceSupplierOrderActionResult + 5 input-DTO
— Best-effort partial-fail tolerance для batch: chain-fail per-Order → лог error + продолжаем; counter-fail → warn + applyStatusTransition всё равно
— UI OrderCard.vue: offerer.placed += decline-кнопка (parent оркестрирует confirm-dialog в Story 4.6)
— Spec: 13 сценариев для consolidated (happy / partial-chain-fail / NotFound / Forbidden / cycle_type / status / counter-fail) + 14 для supplier-action (3 группы happy/forbidden/cycle/status/chain-fail)
* [598-12][@ant] feat: добавить orderer-стол «Мои заказы» с жизненным циклом и отменой — закрыть Story 4.6 Эпика 4
— 3 GraphQL Query в MarketplaceOrderResolver: marketplaceListMyOrders / marketplaceListSupplierOrders / marketplaceGetOrder (ACL Order:read:own | read:to-self | read:all)
— MarketplaceListOrdersInput + MarketplaceGetOrderInput + MarketplaceOrderPaginationResult DTO
— toMarketplaceOrderDTO / toMarketplaceOrderCreateTxSnapshotDTO в dto.ts (общие helpers вне resolver)
— access-matrix: admin += Order:read:all (Story 4.6 AC)
— UI desktop: pages/Marketplace/MyOrders с api/index.ts (raw POST /v1/graphql, паттерн каталога) + types.ts + MyOrdersPage.vue
— MyOrdersPage: grid canon OrderCard, фильтр по статусам, polling 10s (Subscription marketplaceOrderUpdated отложена до kernel pubsub Story 9.x), confirm-dialog для cancel (Story 4.4 mutation)
— mp-role-orderer обёртка + STATUS_TO_CARD маппинг доменных статусов на canon OrderCard статусы (UX-DR20 цветные точки)
— Route /:coopname/market/my-orders переключён с legacy UserSuppliesListPage на MyOrdersPage; legacy сохранён под hidden my-supplies-legacy
* [598-13][@ant] feat: cycle_type при публикации Offer'а — закрыть Story 4.7 Эпика 4
Backend (Story 4.7 / L11):
* MarketplaceOfferService.assertCycleConditionals — per cycle_type
required-валидация для create и update:
- time_based → cycle_days REQUIRED (>=1); min_threshold optional
- volume_based → target_volume + max_wait_days REQUIRED (>=1)
- open_subscription → ничего не required
- individual → ничего не required
* На update смена cycle_type валидируется по merged-снимку (текущее
значение из БД + patch), частичный update без касания cycle-полей
не валидируется заново.
* Reset status → PENDING_MODERATION при любом update уже работает с
Story 3.2 (существенное изменение покрыто).
* +11 unit-тестов: create per cycle_type (4 invalid + 3 valid) + update
cycle_type change (4 сценария).
UI (Story 4.7 canon-форма):
* Новая страница `pages/Marketplace/CreateMarketplaceOffer` —
offerer-форма публикации canonical Offer'а через mutation
`marketplaceCreateOffer` (раньше роут `create-offer` указывал на
legacy CreateParentOfferPage с program_id/unit_cost через старый API).
* mp-role-offerer обёртка + токены `marketplace-tokens.scss`.
* q-option-group для cycle_type с подсказкой текущего варианта
(4 значения, каждый со своей пометкой).
* Conditional cycle-fields:
- time_based → cycle_days (required) + min_threshold (опц.)
- volume_based → target_volume + max_wait_days (оба required)
- open_subscription → max_wait_days (опц.)
- individual → нет полей
* При переключении cycle_type — defaults и зачистка чужих полей.
* Frontend-валидация дублирует backend per cycle_type (быстрый UX);
бэкенд — source of truth.
* После успеха — Notify positive + redirect на `marketplace-user-offers`.
* Legacy CreateParentOfferPage спрятан под `create-offer-legacy`
(hidden=true) на случай fallback'а; полное удаление — техдолг
marketplace2.
Не вошло (deferred):
* Seed-миграция «legacy Offer'ы без cycle_type → time_based» не
нужна: до Story 4.7 в БД нет Offer'ов с NULL cycle_type
(Story 3.2 ввела NOT NULL колонку с дефолтом). Если позднее
потребуется backfill старых данных — отдельная миграция Phase 2.
* Замена raw POST `/v1/graphql` на Zeus-типизированные Mutations
отложена до регенерации `@coopenomics/sdk` (общий техдолг
Marketplace UI).
Тесты:
* `controller`: marketplace-offer-service.test.ts → 35 passed (11 новых).
* TSC моих файлов: 0 ошибок (89 pre-existing legacy не в скоупе Story).
* [598-13][@ant] fix: правки ревью PR #386 Эпика 4 «Заказ и блокировка средств»
— описания GraphQL DTO/resolver переписаны бизнес-языком (без story/epic-меток)
— `cycle_type` / `status` в DTO и сервисах — через ENUM (registerEnumType + companion-константы), вместо строковых литералов
— стандартная пагинация `PaginationInputDTO + createPaginationResult` в `marketplaceListConsolidatedRequests`, `marketplaceListMyOrders`, `marketplaceListSupplierOrders`
— `ASSET_SYMBOL` / `ASSET_DECIMALS` из хардкода RUB/4 убраны в DI через `MARKETPLACE_ASSET_CONFIG` (factory из `config.blockchain.root_govern_symbol/precision`)
— user-facing ошибки в `marketplace-order-supplier-action.service` переписаны на понятный русский язык с уникальными маркерами поиска `[E4SAS-…]`
— комментарий в `marketplace-order-sync.service.afterForkProcessing` явно фиксирует разделение ответственности: сущности откатываются через `deleteByBlockNumGreaterThan` + parser2 replay, hook компенсирует только счётчики Offer'а
— legacy `CreateParentOfferPage` маршрут снесён вместе с зависимостями (`features/Request/CreateParentOffer/`, `pages/Marketplace/CreateParentOffer/`); канон — `CreateMarketplaceOfferPage`
— `frontend MyOrders/api` адаптирован к новому каноническому пагинационному аргументу `options`
— конфликт двух `@ObjectType('MarketplaceCategory')` разрешён переименованием в `category-tree.dto.ts` → `MarketplaceCategoryTreeNode`
— правила зафиксированы: `controller/CLAUDE.md` (описания, ENUM, пагинация), `desktop/extensions/market/CLAUDE.md` (Zeus pipeline)
Zeus regen на схеме блокирован устаревшим legacy `application/marketplace/*` стэком — отдельная story по cleanup'у; raw GraphQL во `CreateMarketplaceOffer/api` и `MyOrders/api` помечен явной TODO-меткой блокера.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [598-13][@ant] refactor: снести legacy marketplace стэк, regen Zeus, типизированный SDK для Эпика 4
Backend (controller):
— удалены `src/application/marketplace/*` и `src/domain/marketplace/*` (legacy
cooplace/marketplace resolver, interactor, service, port, ~30 DTO);
— удалён `infrastructure/blockchain/adapters/marketplace-blockchain.adapter.ts`
и его регистрация в `blockchain.module.ts`/`app.module.ts`;
— удалены `documents-dto/asset-contribution-*` и `documents-dto/return-by-asset-*`
(6 DTO) — оставались сиротами после сноса legacy resolver'а;
— переименован `@ObjectType('MarketplaceCategory')` в `category-tree.dto.ts`
→ `MarketplaceCategoryTreeNode` (конфликт имени с baseline-категорией Эпика 3);
— `schema.gql` регенерирована.
SDK (`@coopenomics/sdk`):
— удалены 23 legacy zeus-обёртки в `mutations/marketplace/` (publishRequest,
updateRequest, supplyOnRequest, unpublishRequest, receiveOnRequest,
prohibitRequest, moderateRequest, declineRequest, deliverOnRequest,
disputeOnRequest, completeRequest, cancelRequest, completeReceiveOnRequest,
confirmSupplyOnRequest, acceptChildOrder, createChildOrder, createParentOffer,
generateAssetContribution*, generateReturnByAsset*);
— Zeus client regenerated (`zeus/`, `types/controller/`);
— добавлены типизированные обёртки для нового стэка: `Mutations.Marketplace.{CreateOrder,
CancelOrder, CreateOffer, DetailKU, SetKUStatus, RetryKUGeocode}` и
`Queries.Marketplace.{ListMyOrders, GetOrder, ListCategories, ListKUDetails}`;
— добавлены селекторы `orderSelector` (Order/CreateResult/CancelResult/
PaginationResult), `offerSelector` (Offer/Category);
— `DetailKUInput`/`SetKUStatusInput` обновлены до `MarketplaceDetailKUInput`/
`MarketplaceSetKUStatusInput` (новые имена в схеме).
Desktop:
— удалены 16 legacy `features/Request/*` (Accept/Cancel/Complete/ConfirmRecieve/
ConfirmSupply/CreateChildOrder/Decline/DeliverOn/DisputeOn/Moderate/Prohibit/
Publish/RecieveOn/SupplyOn/Unpublish/UpdateRequest);
— удалены legacy `entities/Request/`, `widgets/Marketplace/CreateChildOrderCard/`,
`widgets/Marketplace/SupplyOrderRequestCard/`, `widgets/Marketplace/RequestCard/`;
— удалены legacy страницы `OfferPage/UserSuppliesList/WarehousePage/ShipmentsPage/
DisputePage/Moderation/Showcase/UserParentOffers/SuppliesList` и их маршруты;
— `pages/Marketplace/CreateMarketplaceOffer/api/index.ts` и `pages/Marketplace/
MyOrders/api/index.ts` переписаны с raw GraphQL на типизированные
`Mutations.Marketplace.*` / `Queries.Marketplace.*`;
— `entities/MarketplaceKUDetails/api/index.ts` — те же обёртки под новые
имена мутаций;
— redirect после создания Offer'а → `marketplace-catalog` (вместо удалённого
`marketplace-user-offers`);
— `AGENTS.md` обновлён под актуальный набор страниц.
Тесты:
— `tests/unit/marketplace/marketplace-{moderation,offer-counters}-service.test.ts`
— добавлены `listAllActiveTimeBased` / `listAllActiveVolumeBased` в моки
репозитория (приведено в соответствие с domain-интерфейсом).
Память: `feedback_graphql_no_raw_strings_desktop.md` обновлён — блокер
regen снят, raw GraphQL запрещён, процедура generate-schema/generate-client/
sdk build документирована.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(release): publish
* chore(release): publish
* ci: атомарный release.yaml, убрать workflow_run-связку (#366)
build-contracts + build-containers через workflow_run упёрлись в:
(а) default-branch caveat (новая логика не активна, пока не в main),
(б) `${{ github.event.workflow_run.head_sha }}` иногда пуст в YAML-
выражениях — описание см. в шаге Resolve tag, инцидент v2026.5.14
не дёрнул PRODUCTION_WEBHOOK_URL.
Замена — один `release.yaml` на push тэга `v*`: резолвит ветку через
`git branch --contains`, собирает контракты → пушит
`dicoop/contracts:<branch>`, собирает базу + сервисные образы →
пушит `dicoop/<svc>:<tag>`, шлёт webhook. Гонок нет by construction.
`build-contracts.yaml` остаётся только на push веток для CI-
обновления `dicoop/contracts:dev|testnet|main` без релизного тэга.
Триггер `tags: ['*']` и логика резолва ветки через --contains
оттуда удалены — это теперь забота release.yaml.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* chore(release): publish
* chore(release): publish
* ci: tag-only триггеры для build-contracts и docs (#367)
build-contracts.yaml — только workflow_dispatch (ручная пересборка
`dicoop/contracts:<branch>` для отладки на dev-ноде). Тэги обрабатывает
release.yaml атомарно (контракты + контейнеры + webhook), отдельная
сборка по push'у в ветку только давала вторую параллельную сборку.
publish-docs.yaml и build-contracts-docs.yaml — на push:tags v* с
gate-job'ом, пропускающим только продакшн-тэги (без -alpha/-beta/-rc/
-test) на main. Раньше docs пересобирались на каждый push в
main/testnet/dev/reports/marketplace2 — впустую.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(capital/time-tracking): личные доли estimate, partial-split, revert при decline
Три бага в распределении билетов времени, вскрытые на прод-инциденте voskhod
(проект CC7-1 «Концепция», estimate=15 ч, 3 creators):
БАГ #1 — recalcDoneEstimatesForContributorProject раздавал «общий остаток пула»
(estimate − committed_total) / N всем creators поровну. Закоммитивший свою долю
получал её ещё раз, остальные — урезанную (15/3=5 → после committed 5 у одного
становилось 10/3=3.33 у каждого, включая того кто уже закоммитил).
Фикс: личная доля = estimate/N − собственный committed estimate. Введён общий
helper redistributeIssueEstimateEntries, который используют и applyExplicit-
EstimateToTimeEntries (force=true), и recalcDoneEstimates (force=false с
no-op оптимизацией если раскладка уже совпадает с планом).
БАГ #2 — commitTime при partial split (entry.hours > requested) создавал новую
committed-запись без entry_type и estimate_snapshot. По default'у БД сохраняла
её как entry_type='hourly', что ломало последующий recalc (он фильтрует только
entry_type='estimate'). Фикс: явно копировать entry_type и estimate_snapshot
из оригинального entry.
БАГ #3 — declineCommit / handleDeclineCommit меняли только commit.status в БД,
но не возвращали time-entries в is_committed=false. После отказа мастера часы
оставались в total_committed_hours и не возвращались в available_hours. Фикс:
новый метод revertEntriesForDeclinedCommit в TimeTrackingInteractor + методы
findCommittedByCommitHash / revertCommittedEntriesByCommitHash в TimeEntry-
Repository. После revert делается force=true redistribute для затронутых DONE
задач, чтобы доли вернулись в норму.
Покрытие тестами: 17 unit-тестов в time-tracking.interactor.spec.ts с
регрессионными сценариями под каждый из трёх багов плюс integration-сценарий
полного lifecycle CC7-1 (estimate → коммит → decline → revert).
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Раньше MINIO_ENDPOINT имел default http://minio:9000, и на проде без
minio-контейнера контроллер падал на bootstrap в HeadBucket с
getaddrinfo ENOTFOUND minio — Nest application не поднимался вообще.
Теперь MINIO_ENDPOINT optional без default; адаптер хранит enabled-флаг
по наличию endpoint и при отсутствии — onApplicationBootstrap логирует
warning и возвращается без сетевых вызовов. Любая попытка getBucket /
fetchObjectForReadProxy кидает InterFileStorageBackendUnavailableError
с понятным сообщением.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Если syncCommit не дождался delta из блокчейна, interactor возвращает DB-only
entity, где description/meta остаются undefined — non-nullable GraphQL field
ломал ответ мутации capitalCreateCommit. Сделал оба поля nullable.
UI CreateCommitButton.vue: satisfaction_stars=0 по умолчанию, label "не указано"
пока пользователь не выбрал; блок contribution_feedback в payload только если
stars >= 1 или review_text непустой.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build-contracts.yaml — только workflow_dispatch (ручная пересборка
`dicoop/contracts:<branch>` для отладки на dev-ноде). Тэги обрабатывает
release.yaml атомарно (контракты + контейнеры + webhook), отдельная
сборка по push'у в ветку только давала вторую параллельную сборку.
publish-docs.yaml и build-contracts-docs.yaml — на push:tags v* с
gate-job'ом, пропускающим только продакшн-тэги (без -alpha/-beta/-rc/
-test) на main. Раньше docs пересобирались на каждый push в
main/testnet/dev/reports/marketplace2 — впустую.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
build-contracts + build-containers через workflow_run упёрлись в:
(а) default-branch caveat (новая логика не активна, пока не в main),
(б) `${{ github.event.workflow_run.head_sha }}` иногда пуст в YAML-
выражениях — описание см. в шаге Resolve tag, инцидент v2026.5.14
не дёрнул PRODUCTION_WEBHOOK_URL.
Замена — один `release.yaml` на push тэга `v*`: резолвит ветку через
`git branch --contains`, собирает контракты → пушит
`dicoop/contracts:<branch>`, собирает базу + сервисные образы →
пушит `dicoop/<svc>:<tag>`, шлёт webhook. Гонок нет by construction.
`build-contracts.yaml` остаётся только на push веток для CI-
обновления `dicoop/contracts:dev|testnet|main` без релизного тэга.
Триггер `tags: ['*']` и логика резолва ветки через --contains
оттуда удалены — это теперь забота release.yaml.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Этот файл — общая память агента для пяти чекаутов: `~/mono-ai-1`..`~/mono-ai-5`. Реальный файл лежит в `mono-ai-1/CLAUDE.md`, остальные четыре — симлинки сюда; правки коммитим из mono-ai-1 в ветку `dev`.
Когда план говорит «UI компонент» / «frontend integration» — путь `components/desktop/src/{features|widgets|pages|processes}/<name>/`, расширение `.vue` (composition API + `<script setup lang="ts">`), стили Quasar (QChip/QBtn/QCard/QDialog…), GraphQL через Apollo Client + сгенерированные типы из `components/sdk/`. Dev — `pnpm --filter @coopenomics/desktop run dev` или `devnet` (без SSR).
**НЕ путать:** НЕТ `components/app-cooperative/` — не предлагать. Все frontend-сессии работают в той же монорепе, что и backend. В стеке **никакого React нигде нет**.
## Worktree-политика
**Для mono-ai-4 (базовая ветка `marketplace2`, Стол заказов):****worktree приветствуется** — изоляция работ + параллельные ветки. Если worktree пуст от `node_modules` и `.env` (pnpm их не дублирует):
Аналогично для других пакетов, чьи тесты будут запускаться (`components/desktop/node_modules`, `components/sdk/node_modules`). `jest` из bin: `cd $WT/components/controller && ./node_modules/.bin/jest -i <test>` — корректно резолвит ts-jest и подхватывает `.env`.
**Подвох cooptypes:** когда `controller/node_modules` — симлинк на main, пакет внутри `node_modules/cooptypes -> ../../cooptypes` раскрывается **относительно main checkout**. `import { MarketContract } from 'cooptypes'` тянет d.ts из main, не из worktree. Если worktree обновлён, а main позади — TSC падает на отсутствующих типах. **Фикс:** перед TSC в worktree controller'а — `git pull` в main checkout до того же коммита (или хотя бы где cooptypes/src синхронен) + `pnpm build` в `main/components/cooptypes/`.
## PR-flow
**Базовая ветка mono-ai-1 — `dev`.** Прямые коммиты в `dev` для мелких фиксов разрешены и не требуют feature-ветки/PR (отменено 2026-05-22 пользователем). Для крупных задач — feature-ветка (`feat/...` или `fix/...`) от dev и PR в dev; merge делает пользователь на GitHub. Push в `main` / `mvp` — по-прежнему только через PR.
**Не 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)
Каждая story из MVP-эпиков «Стол заказов» (`coopenomics/mono`, базовая ветка `marketplace2`, чекаут `~/mono-ai-4`, BMad-spec'и `_blago/.../components/3-minimalnyy-produkt/_bmad-output/`) идёт через PR.
Workflow:
1. Worktree от `marketplace2` на feature-ветке `feat/<E>-<S>-<slug>`.
2. Edits + unit-тесты; tsc + jest должны быть зелёные.
5. Merge **пользователем на GitHub** (не вызывать `gh pr merge` без явной просьбы).
6. После merge — fetch + checkout marketplace2 в основной чекаут, удалить feature-ветку + worktree, запустить e2e / blockchain тесты против обновлённого marketplace2.
### Umbrella-PR на эпик vs цепочка PR
При работе story-by-story в одной feature-area **не плодить отдельные PR на каждую story с одинаковой целевой веткой**. Каждый последующий PR показывает кумулятивный diff (всё, что в head минус то, что уже мёрджнуто в base). Если предыдущие PR ещё не смерджены — в diff вылазят дубликаты файлов всех предыдущих stories.
**Default — umbrella-PR на эпик** (для крупных эпиков с >3 stories): одна `feat/<E>-epic` ветка + один PR; каждая story — отдельный коммит. Worktree последовательно, push после каждой story, PR обновляется. Так закрыт Эпик 1 Стола заказов (umbrella #380).
**Альтернатива — stacked-PRs с `base:<prev-feat>`:** каждый PR таргетится на предыдущую feature-ветку. После merge нижнего GitHub автоматически перетарджетит верхние. Требует дисциплины и тулинга.
**Анти-паттерн:** worktree от `feat/1-2-...`, потом от `feat/1-3-...`, и каждый PR в `marketplace2`. Цепочка branches правильная (изоляция), но цепочка PR — нет. Кейс Эпика 1 Стола заказов 2026-05-14: 11 PR `#370-#380` подряд от `marketplace2`, каждый +N stories назад. Пользователь дошёл до review #372 и обнаружил дубли. Закрыл #372-#379, оставил только #380.
## Сборка контрактов на ARM-машине
Скрипты `components/contracts/build-all.sh` и `build.sh` хардкодят образ
`dicoop/blockchain:latest`, который опубликован **только под `linux/amd64`** —
на ARM (Raspberry Pi 5, Apple Silicon без qemu-binfmt и пр.) `docker run` падает
с`exec /bin/bash: exec format error` сразу после старта контейнера.
**Для ARM использовать multi-arch образ `dicoop/blockchain_v5.1.1:dev`** —
у него тег `dev` указывает на manifest list с нативными `dev-arm64` и
Замена образа в build-скриптах на ARM — отдельный фикс (этот файл документирует
рабочий обход, пока скрипты не перевели на multi-arch манифест глобально).
## Локальные тесты
**Не запускать полный jest локально** ни в mono-ai-1, ни в mono-ai-4: живой dev-стек в docker (`nodeos`, `controller dev` nodemon, `parser dev`, `n8n`) вешает CPU/RAM и блокирует chain. Полный suite — задача CI после push'а PR.
В mono-ai-4 **запрещён параллельный режим jest** (worker-pool по умолчанию) — это вешает сервер. Если нужен unit-тест — точечный с `--runInBand`:
```bash
pnpm jest tests/unit/marketplace/marketplace-onboarding-service.test.ts --runInBand
```
`pnpm generate-schema` / `pnpm generate-client` — **МОЖНО и НУЖНО запускать когда требуется** (разрешено пользователем явно). Любое изменение GraphQL-сигнатуры резолвера или типов контрактов обязано сопровождаться полным циклом регенерации (generate-schema → generate-client → sdk build, при правках cooptypes — ещё и cooptypes build), чтобы desktop получил актуальные типы. Не оставлять заглушку «до regen».
Перед коммитом достаточно `tsc --noEmit` (быстрый, не блокирует).
## SDK login canon
`@coopenomics/sdk` экспортирует `Client.create({api_url, chain_url, chain_id})` + метод `client.login(email, wif)`. Он сам:
1. Генерит `now` (ISO timestamp).
2. Подписывает приватным ключом (WIF) через eosjs.
**Не дёргать `Mutations.Auth.Login` напрямую** — `LoginInput` ждёт `{email, now, signature}`, генерация подписи внутри SDK Client. Refresh: `Mutations.Auth.Refresh.mutation`с`{access_token, refresh_token}`. Канон используется в `blago-cli/src/session/index.ts` (loginInteractive) и в EMP-коннекторе `connectors/cooperative-tsk-login-connector` (Story 11.5).
## DRY — любое 2-кратное повторение выносится в общее (ОБЯЗАТЕЛЬНО)
Любой кусок кода (валидация, маппинг, guard, построение payload, helper-логика), повторённый **второй раз**, обязан быть вынесен в общее: `shared/`-helper / util / базовый класс (controller) или соответствующий FSD-слой `shared/` (desktop). Это **обязательное правило**, не рекомендация — не «то тут то там стряпать одно и то же».
**Триггер:** заметил второе вхождение → сразу выноси, не копируй. Применяется и в controller, и в desktop. (Зафиксировано пользователем в ревью PR #17 «Стол заказов».)
## Backend (controller) каноны
### 3 базовые роли — User / Member / Chairman
- **User** — обычный пайщик. Базовые потребительские права (заказывать, публиковать оферту, видеть свои данные).
- **Member** — **член совета** (не «член кооператива»!). User-права + read-only admin (видит склад, поток заказов, повестку — но не модерирует и не подписывает финальные действия).
- **Chairman** — председатель. User + admin (модерация, KU/whitelist/витрины, closing signature АПП-приёмки/выдачи, повестка совета на write).
Маппинг core-роли на extension-роль явно: User → orderer + опционально offerer/operator; Chairman → admin (полный write); Member → read-only admin (board_readonly). Пайщик может быть одновременно в нескольких extension-ролях — массив, не enum. Guard'ы в расширениях — локальные сейчас, в Phase 2 переключатся на платформенный CASL.
### `agreements` ссылается на существующий document registry_id
Когда расширение controller'а (marketplace, blagorost, любое следующее) хранит факт подписи документа пайщиком в глобальной on-chain таблице `agreements`, ссылка идёт через **существующий `registry_id` из платформенного реестра документов**, не через отдельный type-string типа `marketplace.cpp.stol-zakazov-v1`.
Поле `document_id` в `agreements` — FK на существующий platform registry. Extension-таблицы `*_onboarding_requirement` — `document_registry_id` ссылается на существующий ID. API запросов вида «какие документы подписаны member'ом» — `agreementsByMember(member_id, document_id_filter=[...])`.
**Технический долг платформы:** «Договор УХД сейчас не проходит через `agreements`» — отдельная задача core controller'а, вне scope конкретных расширений.
### Трёхуровневый онбординг расширений
Платформенный паттерн, обязателен для всех новых расширений controller'а.
**L1 — Кооператив (one-time):** председатель/совет принимает решение совета о подключении ЦПП, принимается положение ЦПП (статический документ из platform registry, через document factory с подстановкой параметров кооператива), оферта регистрируется в `coop_registration_offers_registry`.
**L2 — Пайщик при вступлении (per-membership):** в registration-flow появляется **выбор** ЦПП. Пайщик отмечает интересующие, document factory рендерит оферту с `{cooperative_params, member_params, agreement_date}`. Подпись пишется в глобальную on-chain `agreements`. Эта подпись **нивелирует** gate на столе расширения.
**L3 — Пайщик на рабочем столе (per-extension first visit):** backend проверяет через `agreementsByMember` — подписана ли оферта ЦПП. ДА → gate не показывается. НЕТ → gate показывается как explicit consent.
При проектировании любого нового расширения — обязательно три истории под три уровня. На MVP допустимо упростить: одинаковый `document_registry_id` для положения и оферты (физически разные документы могут быть в Phase 2).
### Marketplace asset через DI
В сервисах `components/controller/src/extensions/marketplace/**/*.service.ts` запрещён хардкод вида `const ASSET_DECIMALS = 4` / `const ASSET_SYMBOL = 'RUB'`. Decimals и symbol — через DI `MARKETPLACE_ASSET_CONFIG`:
Канон-пример: `marketplace-order-create.service.ts`. См. `marketplace-asset.config.provider.ts` — мапит `config.blockchain.root_govern_symbol` / `root_govern_precision`. Разные среды (mainnet/testnet/dev) имеют разный symbol+precision.
### registry_id=800 (ReturnByAssetStatement) — клиринг, не членские взносы
Marketplace в монорепе живёт в **двух контурах**:
1.**Система клиринга** (старый, не используется) — registry_id=800. Не использовать в новых фичах.
2.**Система членских взносов** (текущий MVP) — документы лежат **рядом** с актами приёма-передачи и ТТН; другая группа registry_id.
Для нового документа Marketplace-членские-взносы: завести новый registry_id рядом с актами/ТТН и проложить цепочку `cooptypes → factory → controller → desktop`:
1.`@coopenomics/cooptypes`: новый тип документа + регистрация в registry.
3.`@coopenomics/controller`: signed-document DTO + verify в сервисе.
4.`@coopenomics/desktop`: подпись через `Classes.Document`с новым registry_id.
## GraphQL каноны (controller + desktop)
### Описания @Field — бизнес-языком
В`@Field({ description })`, `@InputType`, `@ObjectType` нельзя писать «Story 4.1», «Эпик 3», «FR11a», «composite-entity», «dispatch pipeline», «tx_snapshot», «backend deterministic order_hash». Только пользовательский язык: «Идентификатор заказа», «ПВЗ получения», «Кол-во единиц товара». Story-ссылки и инвариант-комменты — только в inline-комментариях внутри сервиса.
### Enum вместо строковых литералов
Запрещены `if (offer.cycle_type === 'volume_based')`, `status: 'ACTIVE'`, `'PENDING_MODERATION'`. Любое поле с фиксированным набором (cycle_type, status, type, kind, role) — TypeScript `enum`, при необходимости `registerEnumType` для GraphQL. В тестах константы — тоже из enum, не дублирующие строки. Перед добавлением сравнения по строке — `enum MarketplaceOfferCycleType` / `MarketplaceOrderStatus` в `domain/entities/*.types.ts`.
### Пагинация — единый паттерн
В controller-resolver'ах пагинация делается единым каноническим паттерном:
- Вход: `@Args('options', { nullable: true }) options?: PaginationInputDTO` (импорт из `~/application/common/dto/pagination.dto.ts`, поля page/limit/sortBy/sortOrder).
Обязательная процедура перед UI-кодом под новую GraphQL-операцию:
1. Добавить/изменить DTO/resolver в `components/controller/src/...` (code-first).
2.`pnpm run generate-schema` в `components/controller/` → пересоздать `controller/schema.gql`.
3.`pnpm run generate-client` в `components/controller/` → graphql-zeus кладёт клиент в `components/sdk/src/zeus/`.
4.`pnpm run build` в `components/sdk/` → unbuild собирает `dist/`.
5. В desktop импортировать `Mutations.<Domain>.<Name>` / `Queries.<Domain>.<Name>` из `@coopenomics/sdk` — типизировано end-to-end.
Если на шаге 1 не хватает поля — добавить и запустить весь цикл; не оставлять заглушку «пока».
### Строгая типизация desktop API из SDK через IInput['data']
Каждый вызов `client.Mutation` / `client.Query` в `components/desktop/src/**/api/index.ts`:
1. Принимает аргументом объект `data: IXxxInput`, где `IXxxInput = Mutations.<Domain>.<Action>.IInput['data']` (или `Queries...`) — тип берётся **прямо из @coopenomics/sdk**, не переописывается.
2. Передаёт в `variables` объект `data` целиком: `{ variables: { data } }`. Запрещено разворачивать поля: `{ variables: { data: { a, b, c } } }`.
Канон — `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/прототипы):
| **Живой эталон** (`/_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-эквивалент попутно.
- Никаких локальных переопределений `.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 коде **все имена идентификаторов — английские**:
- Имена Vue-компонентов: `BaseInput`, `BaseDialog`, `WalletCard`, `IdentityPanel`.
- Имена файлов и директорий: `shared/ui/BaseInput/BaseInput.vue`.
**Why:** Unicode-имена ломают тулчейн (Vite/Webpack резолверы и aliases часто на ASCII-only regex; TypeScript symbol-resolution на не-ASCII нестабильно; ESLint `vue/component-name-in-template-casing` ждёт PascalCase ASCII; импорты `import БазоваяКнопка from '@/shared/ui/БазоваяКнопка'` невыносимы при review).
**Заголовки в .vue, label-ы кнопок, тексты в UI — по-русски** (это user-facing strings). Не путать с правилом «онтологические class_id по-русски» — то про EMP/ТЭМ и blago-документы, не про frontend.
**Кейс 2026-05-18:** при подготовке UX-спецификации для components/desktop ошибочно применил правило русских имён к Vue-компонентам (`БазоваяКнопка`, `БазовоеПолеВвода`); пользователь поправил.
## Стандарты процессов (.standard.yaml) — бизнес-языком
`components/contracts/cpp/**/*.standard.yaml` — документация для методолога/бухгалтера, не для разработчиков контракта. В `purpose`, `description`, `note`, `human` запрещены технические термины:
- никаких «callback», «soviet::exec», «soviet::createagenda», «AUTHORIZE_CALLBACK_SIGNATURE», «type-string», «registry N», «proposed расширение enum'а»;
- никаких «backend formирует», «controller вызывает», «contract отдаёт» — пишем кто что делает на уровне бизнеса (председатель / совет / заказчик / поставщик);
- технические `marketplace::propwroff`-имена в полях `action`/`name`/`triggered_by` оставляем как identifier'ы, но всё человеко-читаемое в `human`/`purpose`/`description` — на бизнес-словаре;
- если процесс встроен в более общий — ссылаемся на стандарт по имени-человеку («типовой процесс решения совета»), не на техническую реализацию повестки.
Эталоны: `p.mkt.return.standard.yaml`, `p.cap.rid.standard.yaml`, `reg.coop.standard.yaml`. Антипример — `p.mkt.wroff.standard.yaml` в PR #399 review 2026-05-18 (был забит callback-описаниями и «type=mktwroff»).
## Vault & SERVER_SECRET
WIF админ-аккаунта (например `voskhod`) хранится в `vaults` PostgreSQL зашифрованным AES-256-CBC с ключом `sha256(SERVER_SECRET)`. Если `SERVER_SECRET` потом меняли — **старые записи разрушаются**, `decipher.final()` бросает `error:1C800064:Provider routines::bad decrypt`.
**Симптомы:**
-В логе coopback: `[VaultDomainService] Ошибка при получении WIF ключа для пользователя voskhod: bad decrypt`.
-В UI каскад `SignAgreementDialog` не закрывается; SPA отправляет `sendAgreement` корректно, но `wallet::signagree` on-chain не происходит.
- Следствие — `wallet::users[<coop>]` пуст, и любой последующий `is_can_transfer`/трансфер AXON падает на `Отправитель не является участником ЦПП кошелька`.
**Фикс:**
1. Достать живой WIF (для voskhod в dev — `5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3` из boot config.ini, signature-provider).
2. Шифрануть им current SERVER_SECRET (см. `controller/src/utils/aes.ts` — sha256(SECRET) → AES-256-CBC, IV 16 байт, формат `ivHex:cipherHex`).
3.`UPDATE vaults SET wif='...', updated_at=now() WHERE username='voskhod' AND permission='active';`.
4. Перезапуск coopback **не нужен** — он читает каждый раз.
**Дефолт всех mono-репозиториев — `SECRET`.** Если в каком-то `.env` стоит другое — девиация, не норма. Перед re-encrypt'ом vault'а ВСЕГДА сначала смотреть `~/mono-ai-1/components/controller/.env` (или соседнего) — это origin truth для SERVER_SECRET.
**Кейс 2026-05-18 (Эпик 0):** в `mono-ai-5/components/controller/.env` стоял `e2e-fixture-secret-DO-NOT-USE-IN-PROD`, а WIF voskhod в vaults был зашифрован оригиналом — `SECRET`. Фикс — откатить SERVER_SECRET к `SECRET` и подровнять provider'а.
## Capital — фиксы билетов времени (PR #387 merged 2026-05-15)
В`components/controller/src/extensions/capital/` была серия багов в распределении билетов времени, закрытая PR #387 в dev. Три бага:
1.**`recalcDoneEstimatesForContributorProject` / `applyExplicitEstimateToTimeEntries`** раздавал «общий остаток пула» `(estimate − total_committed) / N` всем creators, включая того, кто уже закоммитил. Фикс: личная доля `max(0, estimate/N − own_committed_estimate)` через общий helper `redistributeIssueEstimateEntries`.
2.**`commitTime` partial split** создавал committed-запись без `entry_type` и `estimate_snapshot` — БД по default'у писала `entry_type='hourly'`, что ломало последующий recalc (фильтрует только `entry_type='estimate'`). Фикс: явно копировать оба поля.
3.**`declineCommit` / `handleDeclineCommit`** меняли только `commit.status='declined'`, но не возвращали `time-entries` в `is_committed=false`. Часы оставались в `total_committed_hours`. Фикс: `revertEntriesForDeclinedCommit` через новые `findCommittedByCommitHash` / `revertCommittedEntriesByCommitHash`.
**При симптомах** «25 ч подтверждено непонятно откуда» / «доступные часы не вернулись после decline» / «парные коммиты на одну работу» — первый чек: `grep -c redistributeIssueEstimateEntries time-tracking.interactor.ts` в контейнере > 5. Если нет — деплой устарел. Если есть, но баг — посмотреть `entry_type`у committed-записей по issue: legacy записи из БАГ #2 могут до сих пор быть с `entry_type='hourly'` для split-результатов от estimate (видны по `commit_hash IS NOT NULL AND entry_type='hourly' AND estimate_snapshot IS NULL` на DONE-задаче с estimate>0). Чинятся UPDATE'ом `entry_type → estimate`с правильным `estimate_snapshot`.
**Legacy data caveat:**`capital_time_entries`с`entry_type='hourly'` + `commit_hash != NULL` могут быть как (а) настоящей hourly работой до установки estimate, так и (б) split-наследием БАГ #2. Различить: если у задачи `estimate>0` и `DONE`, и записи hourly от тех же creators что в estimate-долях — это (б), чинить.
**Edge-case дробных остатков:** при `estimate=2.5` и одном creator, после commit'а 2 ч (`Math.floor < 1` для остатка) остаётся 0.5 ч uncommitted estimate, который никогда не закоммитится. Либо ручной DELETE uncommitted остатка, либо изменить estimate на целое (но estimate в `capital_issues` — on-chain, через UI mutation, не DELETE'ом в БД).
## Стол Заказов MVP — текущее состояние
Проект `1-prilozhenie-stol-zakazov` в blago (coopname voskhod, hash `feabc749…3841f73`). Кооперативная закупка/распределение имущества участка (продукты, товары, услуги); пилот — Красногорск; цель 6 мес: 10 кооперативов / 1200+ пайщиков.
- **`components/3-minimalnyy-produkt/_bmad-output/planning-artifacts/epics.md`** (SoT для MVP) — `stepsCompleted=[1,2,3,4]`, `status: complete 2026-05-12`, 65 FRs → 11 эпиков → 57 stories. **НЕ создавать дубль на верхнем уровне `_bmad-output/planning-artifacts/`** — MVP-компонент имеет собственный bmad-output.
-`requirements/` (5 файлов): `04-brif`, `0b-protsessy`, `0f-prd`, `7e-uxui`, `d6-arkhitektura` — дубли _bmad-output (намеренно, см. правило о дублях BMad-артефактов в global memory).
- **Эпик 1** — MERGED в `marketplace2`: PR #368/#370/#371/umbrella#380.
- **Эпик 2 «Сеть ПВЗ»** — MERGED PR #381 (после rebase). Workspace `market-pvz`, KU details + Yandex geocoder, Zeus SDK.
- **Эпик 11 Story 11.1 (Ledger2 canonical actions)** — MERGED PR #375 на C++ стороне. **TS-сторона cooptypes НЕ закрыта**: `cooptypes/src/contracts/marketplace/actions/index.ts` экспортирует только LEGACY клиринговые actions, `interfaces/marketplace.ts` auto-generated из устаревшего ABI. **Блокер Эпика 4** — нужен отдельный pre-эпик PR `feat/S11-1-cooptypes-canonical`: ABI regen через `eosio-abi2ts` из новой `marketplace.abi.json` после `coopcontracts` build, либо ручное добавление 18 canonical actions + canonical tables + canonical interfaces.
| desktop/pages/Marketplace | нет canonical | 11 страниц под клиринг; `desktop/extensions/market` пуст |
**Решение 2026-05-12:** стратегии миграции не делаем — donor уже не работает по старой модели; собираем новую membership-модель в существующем контуре, donor-код переписывается / удаляется напрямую без переходников.
В`/bmad-create-architecture` MVP **не вводить adapter-слой и не описывать миграционные пути**. Прямо фиксировать: какие C++ actions/DTO/Vue-страницы из donor-листа удаляются, какие переписываются под canonical (`signsupp/signchair/signiss1/signiss2/acceptbatch/declinebatch/expirecycle/prepship/createorder(новая сигнатура)/cancelorder`), какие сохраняются (shipment/coopstock — если попадают в MVP scope, отдельно проверить).
**Обязательная enforcement-база для backend** — `mono-ai-4/components/controller/CLAUDE.md` (Composite-Entity `db/bc/derived`, Write-mutation pool с placeholder/sync_key dedup, ParserClient + Redis Streams, ForkRegistry, ADR-002/008/009/011/012; параметры через `config/blockchain.config.ts`, не magic numbers).
Полная пошаговая инструкция, как поднять backend (`coopback`) + parser (`cooparser`) + блокчейн-ноду + БД на macOS через Docker Desktop. Прошёл — отметь, ниже разобраны типичные грабли.
## TL;DR
```bash
cd ~/dacom-code/foundation/monocoop
docker compose up -d # 1. поднять базу
./scripts/dev-setup-macos.sh # 2. одной командой (см. ниже скрипт)
```
Если скрипта ещё нет — выполняй шаги ниже руками.
## Шаги
### 1. Поднять контейнеры
```bash
docker compose up -d
```
Поднимутся: `node` (NodeOS), `mongo`, `monoredis`, `postgres`, `coopback`, `cooparser`. **MinIO** входит в дефолт. **OpenSearch** — нет (тяжёлый, см. опц. сервисы).
### 2. Проверить чтоб .env'ы указывали на service-имена, а не localhost
**Почему service-имена:** все контейнеры в bridge-сети `monocoop_default`. Внутри контейнера `127.0.0.1` = сам контейнер, не host. На macOS `network_mode: host` в Docker Desktop работает плохо — используем bridge + service names.
И вписать в `components/controller/.env` → `CHAIN_ID=...`. **Если chain_id в .env не совпадает с живым — on-chain транзакции упадут на verify, хотя приложение запустится.**
### 4. Если правил .env — пересоздать контейнер (не restart!)
```bash
docker compose up -d --force-recreate --no-deps coopback cooparser
```
`docker restart`**не перечитывает**`env_file`. `--no-deps` — чтобы не пересоздавать БД (потеряются данные).
### 5. Native binding libxmljs2 — пересборка под Linux
На свежем чекауте `pnpm install` запускается на macOS, и `libxmljs2` собирает Mach-O бинарник. В Linux-контейнере он падает:
docker compose up -d --force-recreate --no-deps coopback
```
## Грабли (если опять провозился пол-дня)
| Симптом | Причина | Фикс |
|---|---|---|
| `invalid ELF header` | host'овый `pnpm install` положил Mach-O | См. п. 5 — пересборка libxmljs2 в контейнере |
| `MongooseServerSelectionError: ECONNREFUSED 127.0.0.1:27017` | .env на localhost вместо service-имени | См. п. 2 |
| `password authentication failed for user "postgres"` | в .env `POSTGRES_PASSWORD=postgres` вместо `postgres!23!23` | См. п. 2 |
| Coopback ушёл в restart-loop, но рядом есть другой `monocoop-coopback-1` Up | Случайно запустился старый `components/controller/docker-compose.yml` (network_mode: host, без bind-mount, без CMD) | `docker stop coopback && docker rm coopback` |
| `docker restart` не подхватил новые переменные .env | `restart` не перечитывает `env_file` | `docker compose up -d --force-recreate --no-deps <svc>` |
| Coopback зависает на 5+ минут, лога нет | Это нормально для cold-start ts-node | Подождать. CPU должен крутиться ≥50%. Если CPU = 0% — другая проблема |
| `[VaultDomainService] Ошибка при получении WIF... bad decrypt` | SERVER_SECRET не совпадает с тем, чем шифровали WIF в БД | См. CLAUDE.md → раздел Vault & SERVER_SECRET. Дефолт — `SECRET` |
| Транзакции падают на on-chain verify | CHAIN_ID в .env не совпадает с живым | См. п. 3 |
| `Custom endpoint \`minio://9000\` was not a valid URI` на bootstrap | `MINIO_ENDPOINT` без схемы | `MINIO_ENDPOINT=http://minio:9000` (со схемой!) |
| nodeos в restart-loop с `Database dirty flag set` после sleep/crash Docker Desktop | unclean shutdown chain state | `docker stop monocoop-node-1`; `mv blockchain-data/state-history{,.broken-$(date +%s)}`; `docker compose up -d node` — nodeos сам сделает replay |
| Cold-start coopback 10+ минут (вместо обычных 3-5) | ts-node без cache + bind-mount через osxfs + параллельная нагрузка (nodeos replay) | Подождать; в будущем — `tsx` / `ts-node --swc` для dev-режима |
Назначение скилла — собрать краткое содержание (memo) встречи по транскрипции звонка, согласовать его с пользователем и опубликовать в backend через `blago` так, чтобы оно появилось в desktop UI на странице транскрипции звонка (поле «Заметка о звонке»).
Право редактировать memo на сервере есть только у председателя и членов совета — убедись, что активная сессия `blago login` принадлежит подходящей роли.
Перед началом прочитай: `~/.claude/config/blago/config.yaml`. Запомни оттуда `workspace_base`, `active_workspace_env` и путь из `workspaces`, соответствующий `active_workspace_env` — назовём его `workspace_path`.
#### Входной артефакт
Транскрипции звонков лежат в `<workspace_path>/<project-slug>/meetings/<stem>.md`. Имя файла `<stem>` — UTC-метка окончания звонка (`YYYY-MM-DD_HHMMSS_mmm`). Этот файл — pull-only: его текст приходит с сервера и в `blago push` не уходит.
Связь meeting-файла с UUID транскрипции хранится в `.blago/index.json` рабочей копии: запись с `entity_type=call_transcription`, `relative_path` равен пути к meeting-файлу, `entity_hash` — UUID транскрипции. Сам `blago transcription memo` этот UUID резолвит автоматически по пути.
`blago pull` всегда кладёт рядом с meeting-файлом `meetings/<stem>.memo.md` (тип `call_transcription_memo`, `entity_hash = "<uuid>:memo"`). Если на сервере memo пустой — файл создаётся пустым. Это сделано специально, чтобы редактирование и публикация шли в одно действие: открыть существующий sibling, дописать, опубликовать. Никаких отдельных шагов «создать файл» делать не нужно.
#### Формат содержимого `.memo.md`
Жёсткие требования к телу memo (применяй всегда, даже без напоминания пользователя):
1.**Первая строка — резюме одним предложением, длиной ≤150 символов.** Это «о чём вообще был звонок». Без markdown-форматирования (без `#`, `*`, `-`, ссылок, цитат), без переноса строки внутри. Считай длину в символах (Unicode codepoints), а не в словах. Если не помещается — переформулируй до сути, не сокращай прилагательными.
2. Следующая пустая строка-разделитель, затем — развёртка по сути. Сжимай до минимума:
- оставляй: о чём договорились, что обсудили, какие решения, какие открытые вопросы и кто за что взялся;
- короткие маркированные списки лучше абзацев; имена/числа/даты сохраняй точно.
3. Markdown отображается в desktop UI редактором (Milkdown через `WrappedEditor`). Можно: заголовки `##`/`###`, списки `-`, инлайн `**жирный**`, ссылки. Не используй декоративный markdown, который ничего не добавляет к смыслу.
#### Процедура
1. Получи от пользователя путь к meeting-файлу (или скажи ему: «дай путь к нужному meeting-файлу»). Прочитай meeting-файл целиком.
2. Открой sibling-файл `meetings/<stem>.memo.md` (он уже создан `blago pull`, возможно пустой). Если файла нет — попроси пользователя выполнить `blago pull` для нужного проекта и повтори.
3. Собери summary по правилам выше: первая строка — одно предложение до 150 символов, дальше пустая строка и сжатая суть. По прямому промпту пользователя адаптируй акценты (что выделить, какие выводы). Если промпта нет — попроси его. Запиши результат в существующий sibling.
4. Покажи пользователю обновлённое содержимое `.memo.md` целиком (или дай явную ссылку на файл). Проверь сам: первая строка — одно предложение, ≤150 символов, без markdown. Если нет — переписывай, пока не выполнится. Спроси, всё ли устраивает. Не публикуй без явного «ок / публикуй / поехали».
По умолчанию команда читает `.memo.md` рядом с указанным meeting-файлом. Альтернативы:
-`blago transcription memo <stem>.md --file path/to/draft.md` — взять текст из произвольного файла;
-`blago transcription memo <UUID> --text "<строка>"` — передать текст строкой и адресовать транскрипцию напрямую по UUID.
6. Команда вернёт строку вида `Memo обновлено на сервере: транскрипция <uuid> (символов: N; источник: …)`. Если сервер ответил ошибкой (например, прав не хватает или сессия истекла) — останови работу, сообщи пользователю текст ошибки и предложи `blago login`.
#### Замечания
-`blago pull` всегда создаёт `meetings/<stem>.memo.md` рядом с каждой meeting-транскрипцией — пустой, если на сервере memo пустой. Файл сразу проиндексирован (`entity_type=call_transcription_memo`), поэтому редактирование→`blago transcription memo` идёт без warning'ов.
- Файл `.memo.md` в `blago push` не уходит (артефакты `meetings/` исключены из staging) — это «локальный канал» вокруг серверного поля `memo`.
- Если ты редактировал sibling локально и не публиковал, а на сервере появилась новая версия — повторный `blago pull` запишет git-style маркеры конфликта (`<<<<<<< blago/local` … `>>>>>>> blago/remote`); разрулите вручную и опубликуй через `blago transcription memo`.
- Если на сервере memo всё ещё пустой, а у тебя локальный черновик не публиковался — `blago pull` сохранит локальный текст и проиндексирует его как baseline; никакие правки не пропадают.
- Чтобы перезаписать опубликованный memo — повтори ту же команду; сервер хранит одно текущее значение поля `memo`.
- Ограничения на длину больше нет (ни на фронте, ни на бэкенде) — можно публиковать развёрнутые саммари с выводами и пунктами.
'Управление транскрипциями звонков: публикация краткого содержания (memo) на сервер (отображается в desktop на странице транскрипции).',
)
transcriptionCmd
.command('memo')
.description(
'Опубликовать краткое содержание (memo) для транскрипции звонка через GraphQL-мутацию chatcoopUpdateTranscriptionMemo. <pathOrId>: путь к `meetings/<stem>.md` (id берётся из .blago/index.json, entityType=call_transcription) или UUID транскрипции. Без --text/--file читает sibling-файл `<stem>.memo.md` рядом с meeting. Доступно председателю и членам совета.',
)
.argument(
'<pathOrId>',
'путь к meeting-файлу или UUID транскрипции',
)
.option('--file <path>','путь к файлу с текстом memo (markdown)')
.option('--text <inline>','текст memo строкой (приоритет над --file и sibling)')
`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}.`,
`Конфликт memo транскрипции ${transcriptionId}: локальный черновик и серверная версия различаются. В «${rel}» записаны маркеры слияния («<<<<<<< blago/local» … «>>>>>>> blago/remote»). Оставьте одну версию текста и опубликуйте через «blago transcription memo».`,
'Не указан текст memo. Передайте --text "<строка>" или --file <путь>, либо первым аргументом — путь к meeting-файлу (тогда читается `<stem>.memo.md` рядом).',
`Рядом с «${path.basename(resolved.meetingAbsPath)}» нет файла «${path.basename(siblingAbs)}». Выполните «blago pull» (он создаст sibling-файл; пустой, если на сервере memo пустой), затем отредактируйте и повторите команду — либо передайте --file / --text.`,
)
}
}
functiontoIso(v: unknown,fallback: Date):string{
if(vinstanceofDate){
returnv.toISOString()
}
if(typeofv==='string'||typeofv==='number'){
constd=newDate(v)
if(!Number.isNaN(d.getTime())){
returnd.toISOString()
}
}
returnfallback.toISOString()
}
/**
* После успешной мутации записываем `meetings/<stem>.memo.md` рядом с meeting-файлом и
* заносим запись в `.blago/index.json` с типом `call_transcription_memo`, чтобы:
* - следующий `blago pull` видел индексированный файл и шёл по штатной ветке syncEntityFile;
* - `blago push` его не подхватывал — он внутри `meetings/`, исключённого через `pull-only-paths`.
* Это шаг только при наличии meeting-пути (sibling-резолв из <pathOrId>); UUID-режим без файла — пропускаем.
console.error('[e2e] order created:',ORDER_ID,created.marketplaceCreateOrder.order.status)
// 1. accept individual order (offerer) → авто-Shipment
constsupTok=awaitlogin(SUPPLIER)
constacc=awaitgql(supTok,`mutation($i:MarketplaceAcceptIndividualOrderInput!){ marketplaceAcceptIndividualOrder(input:$i){ order{ id status } tx_hash } }`,{i:{order_id: ORDER_ID}})
constordInfo=awaitgql(supTok,`query($d:MarketplaceGetOrderInput!){ marketplaceGetOrder(data:$d){ id status cycle_id } }`,{d:{order_id: ORDER_ID}}).catch(()=>null)
constsh=awaitgql(supTok,`query($d:MarketplaceListShipmentsInput){ marketplaceListShipments(data:$d){ id status braname cycle_id } }`,{d:{statuses:['SUPPLY_PREPARED']}})
if(!shipment)thrownewError('Нет SUPPLY_PREPARED партии после accept')
if(Number(process.env.STOP_AFTER)===2){
console.error(`[e2e] STOP_AFTER=2 — SHIPMENT ${shipment.id} в SUPPLY_PREPARED (готов к маркировке EAN-13), ORDER ${ORDER_ID}`)
return
}
// 3. оператор КУ создаёт АПП
constchairTok=awaitlogin(CHAIR)
constcr=awaitgql(chairTok,`mutation($d:MarketplaceCreateAplReceptionInput!){ marketplaceCreateAplReception(data:$d){ apl_reception{ id status braname } } }`,{d:{shipment_id: shipment.id}})
constcp=awaitgql(chairTok2,`query($d:MarketplaceAplReceptionByIdInput!){ marketplaceAplReceptionChairmanSignablePayloads(data:$d){ hash rawDocument{ full_title html hash meta binary } document{ version hash doc_hash meta_hash meta signatures{ id signer public_key signature signed_at signed_hash meta } } } }`,{d:{apl_reception_id: aplId}})
// ── Магистраль II шаг 8: оператор КУ маркирует имущество EAN-13 ───────
// переиспользуем токен председателя: повторный login того же аккаунта в
// пределах одного блока даёт одинаковый now → одинаковую подпись → дубль
// токена сессии (UQ constraint).
constlbl=awaitgql(chairTok2,`mutation($d:MarketplaceLabelInventoryInput!){ marketplaceLabelInventory(data:$d){ inventory{ id barcode_value barcode_format quantity_per_label status product_name_snapshot } } }`,{d:{order_id: ORDER_ID,format:'EAN13'}})
console.error('[e2e] LABEL OK (EAN-13):',JSON.stringify(lbl.marketplaceLabelInventory.inventory?.map((i: any)=>`${i.barcode_value}/${i.barcode_format}/${i.status}`)))
// ── Магистраль II шаги 9-10: ВЫДАЧА (тот же двухподписный канон) ──────
// 9. председатель КУ выдачи открывает выдачу первой подписью (signiss1).
constoip=awaitgql(chairTok2,`query($d:MarketplaceIssueActPayloadInput!){ marketplaceIssueActChairmanSignablePayload(data:$d){ full_title hash meta binary } }`,{d:{order_id: ORDER_ID}})
constfp=awaitgql(ordTok2,`query($d:MarketplaceIssueActPayloadInput!){ marketplaceIssueActOrdererSignablePayload(data:$d){ hash rawDocument{ full_title html hash meta binary } document{ version hash doc_hash meta_hash meta signatures{ id signer public_key signature signed_at signed_hash meta } } } }`,{d:{order_id: ORDER_ID}})
console.error('[diag] signed',signed_documents.length,'docs; first signatures:',JSON.stringify(signed_documents[0]?.signatures?.length))
constm=`mutation($d:MarketplaceSignAplReceptionInput!){ marketplaceSignAplReceptionAsSupplier(data:$d){ apl_reception{ id status supplier_signed_at } } }`
inlineconstexpreosio::nameREQUEST_WITHDRAW="o.wal.wthreq"_n;///< Запрос на возврат паевого: BLOCK на SHARE_FUND_PAY (без Dr/Cr).
inlineconstexpreosio::nameDECLINE_WITHDRAW="o.wal.wthdec"_n;///< Отклонение запроса на возврат: UNBLOCK на SHARE_FUND_PAY (без Dr/Cr).
inlineconstexpreosio::nameCOMPLETE_WITHDRAW="o.wal.wthcpl"_n;///< Завершение возврата паевого взноса (Dr 80 / Cr 51, BURN с WITHDRAW_PENDING — деньги уходят из системы, без wallet_to).
inlineconstexpreosio::nameREQUEST_WITHDRAW="o.wal.wthreq"_n;///< Запрос на возврат паевого: TRANSFER SHARE_FUND_PAY → WITHDRAW_PENDING (резерв, без Dr/Cr).
inlineconstexpreosio::nameDECLINE_WITHDRAW="o.wal.wthdec"_n;///< Отклонение запроса на возврат: TRANSFER WITHDRAW_PENDING → SHARE_FUND_PAY (снятие резерва, без Dr/Cr).
inlineconstexpreosio::nameCONVERT_TO_MEMBER="o.wal.conv"_n;///< Конвертация цифрового рубля в универсальный членский кошелёк (Dr 80 / Cr 86, TRANSFER SHARE_FUND_PAY → CK_MEMBER). Conditional-шаг серии createorder в «Столе заказов».
}
@@ -83,26 +83,17 @@ namespace operations {
inlineconstexpreosio::nameCONVERT_TO_BLAGO="o.cap.cnvbl"_n;///< Конвертация сегмента: РИД → ЦПП «Благорост» (TRANSFER GENERATOR_FUND → BLAGOROST_FUND, без Dr/Cr — бухпроводка уже была сделана в ACCEPT_RID).
}
// marketplace — членская модель «Стола заказов» (refactor 2026-05-11; старые
// Составные бухгалтерские проводки через транзит счёта 91 «Прочие доходы
// и расходы» разбиты на пары операций (часть 1 + часть 2). Контракт
// вызывает обе последовательно в одной транзакции Antelope — атомарность
// обеспечивается транзакцией. Имена с суффиксом «2» — закрытие транзита.
// marketplace — членская модель «Стола заказов».
namespacemarketplace{
inlineconstexpreosio::nameASSIGN_TO_PROGRAM="o.mkt.assign"_n;///< Целевое назначение членского взноса пайщика в программу Marketplace (TRANSFER CK_MEMBER → MARKETPLACE_MEMBER, без проводки — оба кошелька на счёте 86). Conditional-шаг серии createorder.
inlineconstexpreosio::nameBLOCK_FOR_ORDER="o.mkt.block"_n;///< Блокировка членского взноса заказчика под конкретный Order (BLOCK на MARKETPLACE_MEMBER, без Dr/Cr).
inlineconstexpreosio::nameUNBLOCK_ON_CANCEL="o.mkt.unblk"_n;///< Разблокировка членского взноса при отмене Order'а (UNBLOCK на MARKETPLACE_MEMBER, без Dr/Cr). Сумма остаётся на .available и может быть потрачена на следующий заказ в программе.
inlineconstexpreosio::nameRECALL_TO_UNIVERSAL="o.mkt.recall"_n;///< Вывод программного членского взноса в универсальный членский кошелёк пайщика (TRANSFER MARKETPLACE_MEMBER → CK_MEMBER, без проводки — оба кошелька на счёте 86). Явное действие пайщика, не часть авто-flow отмены.
inlineconstexpreosio::namePURCHASE_FROM_SUPPLIER="o.mkt.purch"_n;///< Приёмка имущества кооперативом по АПП приёмки от поставщика (Dr 10 / Cr 86, NONE — только бухпроводка, кошельки не двигаются; имущество — аналитика по 10). Атомарно с PAY_SUPPLIER на закрывающей подписи председателя.
inlineconstexpreosio::namePAY_SUPPLIER="o.mkt.payout"_n;///< Оплата поставщику с расчётного счёта по факту приёмки (Dr 86 / Cr 51, ISSUE ∅ → SUPPLIER_PAYMENTS). Атомарно с PURCHASE_FROM_SUPPLIER.
inlineconstexpreosio::nameCONSUME_BY_MEMBER="o.mkt.consum"_n;///< Выдача имущества пайщику по АПП выдачи (часть 1 композитной проводки через транзит 91): Dr 91 / Cr 10, REVOKE MARKETPLACE_MEMBER.blocked → 0 — выбытие имущества со склада на «прочие». Закрытие транзита — отдельной операцией CONSUME_TRANSIT_CLOSE, атомарно в той же транзакции signiss2.
inlineconstexpreosio::nameCONSUME_TRANSIT_CLOSE="o.mkt.consum2"_n;///< Выдача имущества пайщику (часть 2 композитной проводки): Dr 86 / Cr 91, NONE — закрытие транзита 91 на счёт ЦФ программы. Срабатывает после CONSUME_BY_MEMBER в той же транзакции.
inlineconstexpreosio::nameRETURN_BY_MEMBER="o.mkt.return"_n;///< Гарантийный возврат имущества пайщиком — compensating forward к CONSUME_BY_MEMBER (часть 1 композитной проводки): Dr 91 / Cr 86, ISSUE ∅ → MARKETPLACE_MEMBER — восстановление «прочих» за счёт ЦФ + восстановление .available заказчика. Реверты ledger2::revert в Столе заказов не используются.
inlineconstexpreosio::nameRETURN_TRANSIT_CLOSE="o.mkt.return2"_n;///< Гарантийный возврат (часть 2 композитной проводки): Dr 10 / Cr 91, NONE — имущество назад на склад через закрытие транзита. Срабатывает после RETURN_BY_MEMBER в той же транзакции decretvisit.
inlineconstexpreosio::nameWRITE_OFF_PERISHABLE="o.mkt.wroff"_n;///< Утилизация скоропорта со склада (часть 1 композитной проводки): Dr 91 / Cr 10, NONE — выбытие со склада на «прочие». По протоколу совета.
inlineconstexpreosio::nameWRITE_OFF_TRANSIT_CLOSE="o.mkt.wroff2"_n;///< Утилизация скоропорта (часть 2 композитной проводки): Dr 86 / Cr 91, NONE — закрытие транзита 91 на счёт ЦФ программы. Срабатывает после WRITE_OFF_PERISHABLE в той же транзакции execwroff.
inlineconstexpreosio::nameLOCK_ORDER="o.mkt.lock"_n;///< Резервирование средств заказчика под конкретный Order (TRANSFER w.wal.share → w.mkt.order, Dr 80 / Cr 86 — паевой переходит в целевое финансирование на резерв-кошелёк). Единственный обязательный шаг ledger2 при createorder.
inlineconstexpreosio::nameCONVERT_TO_MKT_MEMBER="o.mkt.conv"_n;///< Конвертация паевого взноса в членский кошелёк «Стола заказов» (TRANSFER w.wal.share → w.mkt.member, Dr 80 / Cr 86). Доплата по факту (signiss2, actual > ordered) идёт ИМЕННО с членского программы — паевой сперва конвертируется сюда, напрямую с паевого не списываем.
inlineconstexpreosio::nameLOCK_FROM_MEMBER="o.mkt.lockm"_n;///< Добор резерва заказа с членского «Стола заказов» (TRANSFER w.mkt.member → w.mkt.order, без Dr/Cr — оба кошелька на 86). Парный к o.mkt.conv шаг доплаты по факту: после конвертации добирает резерв под этот же Order.
inlineconstexpreosio::nameUNLOCK_ORDER="o.mkt.unlock"_n;///< Снятие резерва при отмене Order'а или недовыдаче (TRANSFER w.mkt.order → w.mkt.member, без Dr/Cr — оба кошелька на 86). Средства возвращаются на членский «Стола заказов» (не на универсальный членский) — остаются в программе и могут быть потрачены на следующие заказы.
inlineconstexpreosio::namePURCHASE_FROM_SUPPLIER="o.mkt.purch"_n;///< Приёмка имущества кооперативом по АПП приёмки от поставщика (Dr 10 / Cr 86, NONE — только бухпроводка, кошельки не двигаются; имущество — аналитика по 10). Атомарно с PAY_SUPPLIER на закрывающей подписи председателя.
inlineconstexpreosio::namePAY_SUPPLIER="o.mkt.payout"_n;///< Оплата поставщику с расчётного счёта по факту приёмки (Dr 86 / Cr 51, ISSUE ∅ → SUPPLIER_PAYMENTS). Атомарно с PURCHASE_FROM_SUPPLIER.
inlineconstexpreosio::nameCONSUME_BY_MEMBER="o.mkt.consum"_n;///< Выдача имущества пайщику по АПП выдачи (BURN с w.mkt.order, Dr 86 / Cr 10 — сжигание резерва заказа и выбытие имущества со склада через целевое финансирование).
inlineconstexpreosio::nameRETURN_BY_MEMBER="o.mkt.return"_n;///< Гарантийный возврат имущества пайщиком — compensating forward к CONSUME_BY_MEMBER (ISSUE ∅ → w.mkt.member, Dr 10 / Cr 86 — восстановление средств на членском «Стола заказов» заказчика и возврат имущества на склад). Реверты ledger2::revert в Столе заказов не используются.
inlineconstexpreosio::nameWRITE_OFF_PERISHABLE="o.mkt.wroff"_n;///< Утилизация скоропорта со склада (NONE Dr 86 / Cr 10). По протоколу совета.
// баланса упразднена. Резерв средств под заявку на возврат паевого теперь
// выражается переводом на отдельный кошелёк-резерв `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 — больше не выдаются и невалидны на входе).
enumclassWalletOp: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 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
REVOKE=6,///< изъятие amount с wallet_from.blocked в пустоту, без wallet_to. Обязательная пара Dr/Cr. Отличие от BURN — списание из blocked, а не из available; применяется на целевом расходовании предварительно заблокированной суммы (например, выдача имущества пайщику по АПП выдачи). Введён 2026-05-11 под членскую модель «Стола заказов» (o.mkt.consum).
ISSUE=0,///< первичный вход средств на кошелёк wallet_to (wallet_from = empty)
TRANSFER=1,///< перемещение wallet_from → wallet_to (с Dr/Cr ИЛИ без — по парам account_id)
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 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
};
/**
@@ -162,7 +162,7 @@ enum class WalletOp : uint8_t {
inlineconstexpreosio::nameSUPPLY="p.mkt.supply"_n;///< Прямая поставка-приобретение имущества (9 операций: o.wal.conv + o.mkt.assign + o.mkt.block + o.mkt.unblk + o.mkt.recall + o.mkt.purch + o.mkt.payout + o.mkt.consum + o.mkt.consum2 — последние две композитная проводка через транзит 91). Старый p.mkt.reqst (клиринговый) удалён 2026-05-11.
inlineconstexpreosio::nameRETURN="p.mkt.return"_n;///< Гарантийный возврат имущества пайщиком — compensating forward к o.mkt.consum (o.mkt.return + o.mkt.return2 — композитная проводка через транзит 91).
inlineconstexpreosio::nameWRITEOFF="p.mkt.wroff"_n;///< Утилизация скоропорта со склада КУ (o.mkt.wroff + o.mkt.wroff2 — композитная проводка через транзит 91, по протоколу совета).
staticconstexpreosio::nameWITHDRAWALS_SINK="w.wal.wthdrw"_n;///< DEPRECATED 2026-05-21: исторический sink возвратов. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
staticconstexpreosio::nameWITHDRAW_PENDING="w.wal.wpend"_n;///< Резерв паевого под заявку на возврат (COOPERATIVE-пул). o.wal.wthreq переводит сюда с w.wal.share, o.wal.wthdec возвращает обратно, o.wal.wthcpl сжигает отсюда. Заменил механику blocked/BLOCK/UNBLOCK 2026-05-24.
staticconstexpreosio::nameBLAGOROST_FUND="w.cap.blago"_n;///< Благорост — единый агрегированный кошелёк программы (USER_SHARED; ADR-009)
staticconstexpreosio::nameGENERATOR_FUND="w.cap.gen"_n;///< Генератор — единый агрегированный кошелёк программы (USER_SHARED; ADR-009)
staticconstexpreosio::nameGENERATOR_FUND="w.cap.gen"_n;///< Генератор — единый агрегированный кошелёк программы (COOPERATIVE — кооперативный пул, без L3-разреза по пайщику; L3-разрез из ADR-009 отменён из-за несовместимости с CRPS-перераспределением, см. wallets.hpp:107)
staticconstexpreosio::namePREIMP_FUND="w.cap.preimp"_n;///< Первичный учёт РИД-взносов до перехода на электронный учёт (USER_SHARED; o.cap.preimp / o.cap.drppre)
// marketplace — резерв под Order + членский «Стола заказов» + выплаты
staticconstexpreosio::nameMARKETPLACE_ORDER_LOCK="w.mkt.order"_n;///< ЦПП «Стол Заказов» — резерв средств пайщика под конкретный Order (USER_SHARED). TRANSFER w.wal.share → w.mkt.order на createorder (Дт 80 / Кт 86); обратный TRANSFER на w.mkt.member при cancel/decline/expire (без проводки); BURN с w.mkt.order на signiss2 (Дт 86 / Кт 10).
staticconstexpreosio::nameMARKETPLACE_MEMBER_FUND="w.mkt.member"_n;///< ЦПП «Стол Заказов» — членский кошелёк пайщика программы (USER_SHARED, счёт 86). Сюда возвращается остаток резерва при отмене/недовыдаче и гарантийном возврате; сюда же конвертируется паевой взнос (o.mkt.conv) при доплате по факту — списание под заказ идёт ИМЕННО с членского программы, не с паевого напрямую.
eosio::namepayout_status=OrderPayoutStatus::NONE;///< Locked Decision L12 — состояние выплаты поставщику через gateway (см. namespace OrderPayoutStatus)
std::stringpayout_decline_reason;///< Заполняется только при payout_status == DECLINED (текст причины из gateway::outdecline)
uint64_treturn_request_id=0;///< 0 если активного гарантийного возврата нет
// Все timestamp'ы переходов состояний (createorder/accepted/received_to_coop/
- Order находится в ожидании подтверждения выплаты (`order.payout_status == pending`).
- from:accepted_to_coop
to:ready_to_receive
action:marketplace::signiss1
@@ -415,7 +472,6 @@ transitions:
ledger_code:p.mkt.supply
operations:
- o.mkt.consum
- o.mkt.consum2
guards:
- Заказчик лично присутствует на пункте выдачи участка.
- Председатель сверил фактически выдаваемое имущество с составом заказа.
@@ -517,14 +573,50 @@ scenario:
description:>
Председатель участка ставит вторую — завершающую — подпись на
акте приёма-передачи. С этого момента партия юридически принята
кооперативом, поставщику уходит оплата, имущество приходуется на
склад участка.
кооперативом, имущество приходуется на склад участка, у кооператива
возникает обязательство выплатить поставщику. Сама выплата —
отдельным шагом по факту подтверждения кассиром банковского
перевода (Locked Decision L12).
pre:
- На акте приёма-передачи имеется первая подпись поставщика.
- Действие выполняет председатель того участка, который принимает поставку.
post:
- Поставка принята кооперативом.
- Поставщику направлена оплата.
- Обязательство выплаты поставщику открыто и ждёт фактического банковского перевода.
- step:6a
title:Инициация выплаты поставщику
actor:backend
action:marketplace::payout
description:>
Кооператив через свой backend регистрирует исходящий платёж
поставщику в контракте gateway. У кассира появляется задача в
столе процессинга. Бухгалтерия по выплате на этом шаге не
двигается — обязательство перед поставщиком остаётся открытым
вплоть до подтверждения банковского перевода кассиром.
pre:
- Поставка принята кооперативом (приёмка завершена).
- Выплата по этому заказу не в процессе и не завершена.
post:
- В контракте gateway появилась запись об исходящем платеже со статусом pending.
- Заказ помечен как «выплата ожидает подтверждения».
- step:6b
title:Подтверждение выплаты поставщику
actor:gateway
action:marketplace::payconfirm
description:>
Кассир провёл реальный банковский перевод поставщику и
подтвердил это в столе процессинга. Контракт gateway вызывает
callback в контракт marketplace, и кооператив фиксирует
выплату — обязательство перед поставщиком закрывается, расход
целевых средств с расчётного счёта отражается в бухгалтерии.
pre:
- В контракте gateway зарегистрирован исходящий платёж в статусе pending.
- Кассир подтвердил фактический банковский перевод поставщику.
post:
- Обязательство выплаты поставщику закрыто.
- Заказ помечен как «выплата проведена».
- step:7
title:Открытие выдачи
@@ -551,8 +643,10 @@ scenario:
расхождении (например, развесной товар:фактический вес меньше
заказанного) сумма заказа предварительно корректируется до
фактической:при меньшем количестве остаток возвращается на
программный членский кошелёк заказчика, при большем — недостающая
сумма берётся с паевого заказчика. Заказчик ставит финальную
программный членский кошелёк «Стола заказов» заказчика, при большем —
недостающая сумма вносится дополнительным паевым взносом, который
конвертируется в членский «Стола заказов» и им добирается доплата
(напрямую с паевого не списываем). Заказчик ставит финальную
подпись на акте приёма-передачи и забирает заказ.
pre:
- Заказ готов к выдаче.
@@ -591,6 +685,19 @@ scenario:
заказчиков снимается. Договорные санкции при отказе — вне этого
процесса, по регламенту кооператива.
- branch:Кассир отклонил банковский перевод поставщику
at_step:6b
action:marketplace::paydecline
actor:gateway
description:>
Кассир в столе процессинга отметил, что банковский перевод
поставщику не состоялся — нет реквизитов, ошибка ввода, банк
отклонил. Контракт gateway callback'ом дёргает контракт
marketplace. Бухгалтерия не двигается — обязательство перед
поставщиком остаётся открытым с сохранением причины отказа.
Backend может повторно инициировать выплату после исправления
реквизитов (`marketplace::payout` ещё раз).
- branch:Передача партии через экспедитора
at_step:5
action:marketplace::signsupp
@@ -659,67 +766,67 @@ documents:
note:"TODO: создать шаблон ТТН в registry. При самовывозе не используется. Подписи проставляются вне блокчейна (на бумажном носителе); фотофиксация председателя по результатам очной сверки сохраняется как основание для дистанционной подписи поставщика."
# ── Секция 6. Операции (Ledger2) ────────────────────────────────────────────
# Восемь ledger2-операций для процесса прямой поставки-приобретения. Имена
# ≤12 символов eosio::name. Кошельки идентифицируются eosio::name-строками
# с префиксом w.<contract>.<waltype> (рефакторинг 2026-04-27, ветка reports):
title:Формирование и внесение проекта на рассмотрение совета
actor:chairman
action:marketplace::propwroff
description:>
Кооператив по расписанию (по умолчанию раз в месяц, точная дата
согласовывается с бухгалтером) опрашивает склады участков и
собирает позиции, которые невозможно выдать заказчику:
просроченные и не востребованные, повреждённые, малооценные.
Сформированный список становится проектом решения и выносится
на повестку совета.
Кооператив по расписанию (по умолчанию раз в месяц; точную дату
согласовывает бухгалтер) опрашивает склады участков и собирает
позиции, которые невозможно выдать заказчику:просроченные и не
востребованные, повреждённые, малоценные. Председатель оформляет
проект списания со списком позиций, подписывает заявление и
вносит проект на рассмотрение совета.
pre:
- Наступил регламентный срок очередного цикла списания.
- На складах участков найдены позиции, удовлетворяющие критериям списания.
- Председатель подписал Заявление о списании скоропорта.
post:
- Проект списания сформирован.
- Проект вынесен на повестку совета.
- Проект списания сформирован и подписан председателем.
- Проект внесён на рассмотрение совета.
- step:2
title:Рассмотрение проекта советом
actor:council
action:marketplace::execwroff
action:null
description:>
Совет рассматривает проект — на очном заседании или в форме
заочного голосования — и принимает решение, оформленное
протоколом. При положительном решении кооператив списывает
перечисленные в проекте позиции со складов участков. Имущество
уходит из учёта как безвозвратные потери. По кошелькам
заказчиков движений не происходит.
Совет рассматривает заявление председателя по типовому процессу
решения совета — на очном заседании или в форме заочного
голосования — и принимает решение. При положительном решении
председатель подписывает протокол о списании. При отрицательном —
проект отклоняется.
pre:
- Проект внесён на рассмотрение совета.
- Совет принял положительное решение по проекту и оформил его протоколом.
post:
- Списание исполнено.
- Списанные позиции ушли со складов участков.
- Совет принял решение по проекту и подписал протокол.
- step:3
title:Исполнение списания
actor:chairman
action:marketplace::execwroff
description:>
По принятому советом протоколу кооператив последовательно
списывает каждую позицию проекта со склада участка. Имущество
безвозвратно выбывает из учёта; сумма закрывается на счёт
целевого финансирования программы. По кошелькам заказчиков
движений нет.
pre:
- Совет принял положительное решение по проекту.
- Протокол совета о списании подписан.
post:
- Все позиции проекта списаны со складов участков.
- Имущество ушло из учёта как безвозвратные потери.
alternatives:
- branch:Совет отклонил проект
at_step:2
action:marketplace::declwroff
action:null
actor:council
description:>
Совет отклонил проект списания (например, требуется
дополнительная экспертиза) либо отложил его рассмотрение.
Позиции остаются на складах участков и попадут в следующий цикл
списания.
Совет отклонил проект списания — например, требуется
дополнительная экспертиза — либо отложил рассмотрение. Позиции
остаются на складах участков и могут попасть в следующий цикл
списания. Движений по имуществу и средствам не происходит.
- branch:Альтернатива — переуступка имущества по сниженной цене
at_step:1
action:null
actor:backend
actor:chairman
description:>
Альтернативный путь для имущества, ещё пригодного к выдаче
(например, накануне истечения срока годности):кооператив сам
Альтернативный путь для имущества, ещё пригодного к выдаче —
например, накануне истечения срока годности:кооператив сам
выступает поставщиком в новом предложении по сниженной цене и
ставит имущество на следующий цикл отсечки заявок. В MVP только
помечен; реализуется отдельным процессом в более поздней фазе.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
# В процессе подписывается протокол решения совета (authorization), который
# является типовым документом совета и обслуживается через процесс
# sov.decision (повестка → голосование → протокол). Этот процесс
# напрямую не оформляет своих документов — только пункт повестки.
# Заявление председателя — собственный документ процесса (вносится советом
# на рассмотрение). Протокол совета — документ типового процесса решения
# совета (по стандарту sov.decision), к которому привязан проект списания.
documents:
- action:marketplace::execwroff
title:Протокол решения совета о списании скоропорта
registry_id:0
signed_by:[council_members]
stored_in:"(authorization — параметр действия execwroff, обслуживается через sov.decision)"
note:"TODO: использовать существующий шаблон протокола совета (sov.decision-протокол) с прикреплённым перечнем позиций; либо создать специализированный шаблон в cooptypes/cooperative/registry/<id>.MarketplaceWriteoffProtocol при необходимости отдельной формы."
- action:marketplace::propwroff
title:Заявление о списании скоропорта
registry_id:1106
signed_by:[chairman]
stored_in:writeoff_proposals.statement
note:"Подписывается председателем перед внесением проекта на рассмотрение совета. Содержит список позиций к списанию (участок, наименование, количество, сумма, причина). Источник правды — components/cooptypes/src/cooperative/registry/1106.MarketplaceWriteoffStatement"
- action:null
title:Протокол совета о списании скоропорта
registry_id:1105
signed_by:[chairman]
stored_in:writeoff_proposals.protocol
note:"Протокол по типовому процессу решения совета (стандарт sov.decision). Подписывается председателем после принятия советом положительного решения. Источник правды — components/cooptypes/src/cooperative/registry/1105.MarketplaceWriteoffProtocol"
# ── Секция 6. Операции (Ledger2) ────────────────────────────────────────────
# Одна ledger2-операция со СОСТАВНОЙ проводкой: транзит через счёт 91.
# В журнале это одна именованная операция; в бухгалтерии — две проводки
# (или одна составная). Реализация: в .cpp execwroff вызывает ledger2::apply
# дважды с одинаковым operation_id (или один раз с composite-record), так
# чтобы транзит через 91 был атомарным и не нарушал инвариантов
# Дт/Кт-баланса по 91. WalletOp ACCOUNT_ONLY — proposed расширение enum'а
# ledger2 для операций без движения по кошелькам.
# Одна ledger2-операция — списание имущества со склада через целевое
# финансирование программы. Срабатывает по каждой позиции проекта в
# транзакции исполнения списания.
operations:
- ledger_code:o.mkt.wroff
human_name:Утилизация скоропорта — выбытие со склада
wallet_op:NONE # NONE = только бухпроводка без движения по кошелькам
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.