Compare commits

...

214 Commits

Author SHA1 Message Date
coopops 6ee7eb93df [design-wave1][@dark] fix: палитра команд — рамка курсора на любом столе, не только активном
Заголовок workspace-row получал outline только когда стол активный
(.is-active .row.is-selected). На неактивных столах при навигации курсор
«терялся»: только лёгкая смена фона на surface-2, без рамки — в отличие
от page'ей, которые подсвечиваются outline'ом всегда. Выровнял правило:
outline на любом .workspace-row.is-selected.
2026-05-28 11:52:16 +00:00
coopops 1f15f368a6 [design-wave1][@dark] fix: палитра команд — clamp навигации стрелками вместо wrap-around
На верхней/нижней позиции стрелка дальше прыгала в противоположный
конец списка из-за (idx+delta+len)%len. Заменено на clamp в [0..len-1]:
курсор остаётся на месте, как в нативном listbox/меню. Пустое выделение
+ ↓ ставит на первый, ↑ — на последний.
2026-05-28 11:47:46 +00:00
coopops f72b4900ab [design-wave1][@dark] fix: палитра команд — снят sticky-баннер активного стола
Активный workspace был position:sticky top:0 — при навигации стрелкой
вверх курсор-выделение «исчезал» за sticky-overlay'ем, потому что
scrollIntoView({block:'nearest'}) считал элемент видимым в normal flow,
не учитывая sticky-перекрытие. Активный стол и так идёт первым по
сортировке + есть badge «Активный», sticky не нужен.
2026-05-28 11:40:01 +00:00
coopops f859651038 [design-wave1][@dark] chore: снос мёртвых shared/ui компонентов
Удалены 5 компонентов без call-site'ов (проверено grep'ом по имени, не
только по пути — barrel-импорты тоже учтены):
AxonWalletDisplay, BlockchainStatusBar, DescriptionSaveButtons,
TabStyles, WrappedEditor. Соответствующие реэкспорты из shared/ui/index.ts
вычищены.
2026-05-28 10:32:30 +00:00
coopops d763f08fec [design-wave1][@dark] feat: страницы 404 и «Нет доступа» на canon EmptyState
BlankPage (404) и PermissionDenied переписаны: вместо вёрстки на q-card с
text-h4 теперь EmptyState + BaseButton. Иконки в EmptyState через slot=icon,
действия — через slot=action. Текст пользовательский: «Страница не найдена /
такой страницы нет или она была перенесена» и «Недостаточно прав доступа /
обратитесь к председателю».
2026-05-28 10:30:25 +00:00
coopops 805ac73ed7 [design-wave1][@dark] refactor: ModalBase под canon-стили — 40+ диалогов сразу в canon без правки call-site'ов
ModalBase визуально приведён к canon (как BaseDialog): surface, h3-заголовок,
round close-btn, --p-r-md, --p-shadow-modal. API сохранён (title/show_close/
slot=default/slot=title) — внешний q-dialog по-прежнему держит родитель.
Чистая миграция на BaseDialog с v-model:modelValue отложена в Cleanup.

Попутно фикс несуществующих токенов --p-elev-* (CommandPalette, DetailsDrawer,
NotificationCenter): --p-elev-3 → --p-shadow-modal, --p-elev-2 → --p-shadow-pop.
2026-05-28 10:28:24 +00:00
coopops b82b778b81 [design-wave1][@dark] refactor: CmdkMenu → canon CommandPalette — palette в primary, мёртвый CmdkTrigger удалён
CmdkMenu стал тонкой обёрткой над shared/ui/domain/CommandPalette: данные и
навигация из useCmdkMenuStore (роли/conditions/⌘K), UI — canon-палитра.
В CommandPalette заменил accent на primary (бирюзовый по канону).
CmdkTrigger нигде не использовался — удалён вместе с экспортом.
2026-05-28 10:23:53 +00:00
coopops bcb76b4cd9 merge: разрешены конфликты с dev — boot-скрипт под dev, кошельки под canon
Typecheck / desktop (pull_request) Failing after 8m35s
Typecheck / controller (pull_request) Successful in 13m22s
- clean_reboot.sh: версия origin/dev (унификация трёх reboot-скриптов)
- WalletProgramWidget.vue: canon-версия (WalletCard + EmptyState)
- ParticipantWalletsPage.vue: canon-токены --p-pos/--p-warn + value-blocked

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:07:41 +00:00
ant 4c4c611a3d [nodemon-restart-storm][@ant] fix: debounce nodemon + SIGTERM в controller/nodemon.json — устранить restart-storm, вешавший coopback на bind-mount
Без delay nodemon реагировал на каждое fs-событие пачки (правки/сборка/git на хосте) десятками рестартов в секунду; pstree.remy без `ps` в образе виснет на обходе /proc (накапливались зомби-сканеры), холодный старт ts-node прерывался, порт 2998 не занимался — бэкенд молча лежал. delay:2000 схлопывает пачку в один рестарт, signal:SIGTERM даёт чистое завершение дочернего процесса.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:22:31 +05:00
coopops 61d53e7ea0 Merge remote-tracking branch 'origin/main' into dev
Build bootstrap container / build (push) Failing after 2m13s
2026-05-25 10:46:47 +00:00
coopops 25a917866c ci(release): вернуть webhook второй документации + переименовать токен в DOCS_DISPATCH_TOKEN
При удалении publish-docs я снёс не только мёртвый gh-pages-push, но и реальную
публикацию ВТОРОЙ документации (доки mono) через DOCS_DEPLOY_WEBHOOK_URL —
она отдельная от coopenomics и должна уезжать синхронно. Возвращаю её отдельным
lean-джобом trigger-mono-docs (только webhook, без gh-pages и mkdocs-сборки —
приёмник деплоит сам).

Токен GITEA_DISPATCH_TOKEN → DOCS_DISPATCH_TOKEN: Gitea резервирует префикс
GITEA_ для имён секретов, такой секрет не создать.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:40:30 +00:00
coopops c73e9b9e80 ci(release): дёргать coopenomics на ветке master (дефолтная), не main
Дефолтная ветка C9S/coopenomics — master; workflow_dispatch надо слать на
ref=master, иначе Gitea вернёт 404 по ветке.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:28:24 +00:00
coopops d7ad0d7251 ci(release): убрать publish-docs (gh-pages), триггер coopenomics через Gitea workflow_dispatch
После переезда на Gitea два downstream-джоба были наследием GitHub:
- publish-docs пушил site/ в gh-pages github.com под secrets.GITHUB_TOKEN
  (на Gitea — гитейный токен, невалиден) → Authentication failed. Pages
  устарел, доки теперь деплоит C9S/coopenomics. Джоб удалён целиком.
- trigger-coopenomics-docs слал repository_dispatch на github.com через
  peter-evans (COOPENOMICS_PAT пуст). Репо переехало в C9S/coopenomics на
  Gitea, а Gitea не имеет API для repository_dispatch — только
  workflow_dispatch. Заменено на curl к Gitea API workflow_dispatch с
  secret GITEA_DISPATCH_TOKEN; целевой workflow получит входы mono_sha/mono_ref.

publish-packages не трогаю — там нужен лишь NPM_TOKEN в секретах (E404 на
scoped-пакетах = отсутствует npm-авторизация).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:25:13 +00:00
ant 3f5859eb3e Merge pull request 'feat(chatcoop): комнаты секретаря + синхронизация непроектных комнат в blago' (#34) from feat/chatcoop-secretary-rooms into dev
Reviewed-on: #34
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-05-25 09:53:32 +00:00
coopops 46e3686550 [E14][@dark-sun] feat(шапка): крошка-название страницы слева + фикс переноса заголовка ModalBase
Крошка: в #crumb рядом с BackButton выводится route.meta.title текущей
страницы (тот же, что подсвечен в меню) — именно для неё действия и
сдвинуты вправо. Длинное название обрезается ellipsis в .topbar__crumb b.

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 09:41:18 +00:00
coopops e20ddb4a9c [E14][@dark-sun] fix(WithdrawButton): «получить возврат» → «Получить возврат» — заглавная
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 09:39:51 +00:00
claude 393ea148f0 chore(compose): run dev-сервисы от uid 1000, чтобы codegen не плодил root-файлы
coopback/cooparser/desktop монтируют ./:/app, а node:22-slim без USER пишет
от root — codegen (generate-client, schema.gql, logs, quasar-кэш) кладёт
root-owned файлы в дерево и ломает git checkout (git работает от ant без sudo).
Добавлен user: "1000:1000" + HOME=/tmp, чтобы вывод контейнеров принадлежал
хостовому ant. node_modules/.pnpm-store уже ant:ant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:54:31 +05:00
Alex Ant 9029fa1f9d chore(release): publish
Build bootstrap container / build (push) Successful in 3m17s
Release / release (push) Successful in 25m39s
Release / publish-docs (push) Failing after 9m10s
Release / trigger-coopenomics-docs (push) Failing after 5s
Release / publish-packages (push) Failing after 9m23s
2026-05-25 12:41:40 +05:00
coopops e640b6add6 chore(controller): синхронизировать controller/zeus с регенерацией
Typecheck / desktop (pull_request) Successful in 13m19s
Typecheck / controller (pull_request) Successful in 12m49s
generate-client пишет копию zeus и в components/controller/zeus — обновляю вместе
со schema.gql, чтобы не оставлять дрифт сгенерированных артефактов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 06:29:06 +00:00
coopops b2c56651f9 chore(controller,sdk): регенерация schema.gql + zeus под комнаты секретаря
Typecheck / desktop (pull_request) Successful in 13m29s
Typecheck / controller (pull_request) Successful in 13m0s
generate-schema + generate-client после добавления резолверов SecretaryRooms и
chatcoopListNonProjectCommunicationRooms. Без этого @coopenomics/sdk:build падал
в CI на отсутствующих в zeus типах (ChatcoopSecretaryRoom / CreateSecretaryRoomInput
/ RemoveSecretaryRoomInput / ChatcoopNonProjectCommunicationRoom) — общий build-шаг
обеих джоб typecheck. Локально sdk build (prebuild tsc --noEmit) зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 06:28:39 +00:00
coopops 5c6062420f feat(desktop/chatcoop): страница «Комнаты секретаря» на столе связи
Typecheck / desktop (pull_request) Failing after 7m26s
Typecheck / controller (pull_request) Failing after 6m28s
- entity SecretaryRoom (api/model/store) поверх SDK chatcoopListSecretaryRooms /
  chatcoopCreateSecretaryRoom / chatcoopRemoveSecretaryRoom
- SecretaryRoomsPage: список комнат реестра (тип, наличие секретаря, шифрование),
  создание комнаты (публичная/приватная) и удаление комнат секретаря
- маршрут chatcoop-secretary-rooms, доступ роли chairman+member
- системные/проектные комнаты — read-only (удаление только у kind secretary)

Затипизируется после generate-schema/generate-client (новые SDK-операции).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 05:56:57 +00:00
coopops d1ce3924d5 feat(sdk,blago-cli): SDK-операции комнат секретаря + pull непроектных транскрипций
SDK (рукописные обёртки над Zeus — затипизируются после generate-schema/client):
- queries: ListNonProjectCommunicationRooms, ListSecretaryRooms
- mutations: CreateSecretaryRoom, RemoveSecretaryRoom
- селекторы ChatcoopNonProjectCommunicationRoom / ChatcoopSecretaryRoom

blago-cli:
- pullNonProjectCommunicationArtifacts: переписка + транскрипции + memo комнат
  пайщиков/совета/секретаря в отдельную верхнюю папку rooms/<folder>/{messages,meetings}/
- курсор transcriptionLastEndedExclusiveByRoom (по matrixRoomId)
- системные комнаты — стабильные папки komnata-paishchikov / komnata-soveta;
  комнаты секретаря — slug + хвост хеша matrixRoomId

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 05:49:06 +00:00
coopops d7c2c90290 feat(chatcoop): комнаты секретаря + синхронизация непроектных комнат в blago
Задача 1 (синхронизация в blago):
- roomKind 'secretary' в реестре управляемых Matrix-комнат
- репозиторий: findAll + findNonProjectCommunicationRooms (всё кроме capital_project)
- inter-порт listNonProjectCommunicationRooms + тип InterNonProjectCommunicationRoomRef (kind)
- query chatcoopListNonProjectCommunicationRooms (members/council/secretary) для blago-cli

Задача 2 (комнаты секретаря, backend):
- SecretaryRoomManagementService: создание комнаты (public/private, всегда plaintext,
  force-join только секретаря, создатель — модератор) и удаление (kick секретаря + дерегистрация)
- SecretaryRoomsResolver: chatcoopListSecretaryRooms / chatcoopCreateSecretaryRoom /
  chatcoopRemoveSecretaryRoom; доступ chairman+member
- matrix-api: inviteUser / kickUser
- конфиг SECRETARY_ROOM_MATRIX

Принцип: секретарь присутствует только в backend-созданных комнатах; force-join в
произвольную чужую комнату запрещён (общий Synapse на все кооперативы).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 05:46:00 +00:00
coopops 5eb03c74c4 ci(release): retry на docker push (3 попытки) — гасит транзиентный DNS
registry-1.docker.io на runner'е изредка не резолвится (DNS-таймаут к
127.0.0.53), из-за чего push валит весь релиз уже после успешной сборки
образов. Обёртка dpush() повторяет push до 3 раз с паузой 10с во всех трёх
push-шагах (контракты, base, сервисы).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 04:18:33 +00:00
Alex Ant 64bb8a0be8 chore(release): publish
Build bootstrap container / build (push) Successful in 3m20s
Release / release (push) Successful in 25m42s
Release / publish-packages (push) Has been skipped
Release / publish-docs (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
2026-05-25 08:55:05 +05:00
Alex Ant a7c8f8cd17 Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev 2026-05-25 08:54:48 +05:00
coopops da00407a7c ci(release): доставить рантайм-либы CDT (libz3-4 и др.) при установке из .deb
clang-9 падал "error while loading shared libraries: libz3.so.4". Пакет cdt
объявляет только libcurl4-gnutls-dev, но бинари тулчейна (objdump -p NEEDED)
требуют libz3.so.4/libtinfo.so.6/libxml2.so.2/libz.so.1. В образе
dicoop/blockchain они были из сборки исходников, из .deb не тянутся —
ставим libz3-4 libtinfo6 libxml2 zlib1g явно.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:54:39 +00:00
Alex Ant cfb9398ec6 chore(release): publish
Build bootstrap container / build (push) Successful in 3m25s
Release / release (push) Failing after 2m29s
Release / publish-packages (push) Has been skipped
Release / publish-docs (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
2026-05-25 08:44:36 +05:00
coopops 966dcc45f2 ci(release): собирать контракты из cdt .deb прямо в job, без вложенного docker
Шаг компиляции контрактов в release.yaml падал под Gitea act_runner:
build-all.sh монтирует $(pwd):/project в sibling-контейнер, но job сам
исполняется в контейнере → хостовый демон не видит путь, /project пуст,
cmake падает "no CMakeLists.txt". Релиз не собирался зелёным (runs #144, #158).

- build_contracts_cdt.sh: чистый cmake/make без docker (общий источник флагов).
- build-all.sh: тонкая docker-обёртка над ним для локальной сборки.
- release.yaml: вместо pull образа + build-all.sh — установка cdt v4.2.0 .deb
  в окружение job'а + симлинк /cdt/build → /usr/opt/cdt/4.2.0 (сводит хардкод
  toolchain-путь CMakeLists без его правки) + вызов build_contracts_cdt.sh.
  Компиляция в самом job-контейнере, без вложенного docker. sudo-агностично —
  одинаково на GitHub-VM и Gitea-контейнере.
- Убран избыточный typecheck-гейт из release; typecheck.yaml остаётся PR-гейтом.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:42:34 +00:00
Alex Ant 25f10e6782 Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev
Build bootstrap container / build (push) Successful in 3m10s
2026-05-24 23:29:58 +05:00
Alex Ant 3d52d816a4 chore(release): publish
Build bootstrap container / build (push) Successful in 3m22s
Release / typecheck (push) Successful in 23m58s
Release / release (push) Failing after 2m27s
Release / publish-packages (push) Has been skipped
Release / publish-docs (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
2026-05-24 23:29:42 +05:00
claude 665c22fc87 fix(boot): унифицировать extra_reboot.sh и clean_reboot.sh с reboot.sh
Build bootstrap container / build (push) Successful in 3m14s
Все три reboot-скрипта теперь идентичны, кроме строки pnpm run boot[:extra|:clean]:
- wipe blockchain-data контейнером (docker run alpine rm) вместо sudo rm —
  работает без sudo на любой ноде независимо от владельца данных (на Pi нет
  passwordless sudo; на проде nodeos пишет данные под root). Единый способ с reboot.sh.
- monoredis в down -v и up -d — без него coopback падает на старте
  (getaddrinfo EAI_AGAIN monoredis → nodemon crash), и провайдер не получает
  org-данные partner1.
- coopback через up -d --force-recreate (перечитывает env), как в reboot.sh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:26:40 +05:00
claude b897627d72 fix(boot): partner1 провижининг под EXTRA_RENT, enrollment в ЦПП Кошелька, идемпотентная activateFeature
Build bootstrap container / build (push) Successful in 3m10s
- booter.ts: installExtraData (регистрация partner1 с auto-approve → триггер аренды
  провайдером) выполняется только под EXTRA_RENT=1. boot:extra используется и для
  других задач (пересев совета/чейна), где аренда VM не нужна.
- infra.ts: partner1 подписывает wallet-соглашение (wallet::signagree, program_id=1)
  при регистрации. Без членства в ЦПП Кошелька provider.performInitialTransfers
  (150 AXON на partner1 перед ACTIVE) падает ассертом eosio.token::is_can_transfer
  «Получатель не является участником целевой потребительской программы кошелька»,
  и инстанс навсегда висит в INSTALL.
- blockchain/index.ts: activateFeature не роняет boot при protocol_feature_exception
  (фича уже активна на не-обнулённом чейне) — ловим и продолжаем.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:13:39 +05:00
ant 72cec26c14 Merge pull request 'Убрать механику blocked из L3-кошельков (резерв возврата → отдельный кошелёк w.wal.wpend)' (#31) from feat/remove-l3-blocked into dev
Build bootstrap container / build (push) Successful in 3m21s
Reviewed-on: #31
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-05-24 17:32:03 +00:00
coopops 7a163389c6 chore(controller,sdk): полная регенерация schema.gql и zeus (generate-schema + generate-client)
Typecheck / desktop (pull_request) Successful in 13m28s
Typecheck / controller (pull_request) Successful in 13m9s
Штатный синк вместо точечной правки: schema.gql приведён в соответствие с
актуальными DTO контроллера (был протухший — накопленный дрейф полей,
не только blocked). zeus перегенерирован из новой схемы; оба
controller/zeus и sdk/src/zeus идентичны.

Результирующие изменения относительно L3-blocked-ветки:
- удалён blocked из Ledger2Wallet/ProgramWallet (цель PR);
- синхронизированы ранее не закоммиченные дрейфы: is_server_init в SystemInit,
  enum-значение AWAITING_AUTHORIZATION, актуализация описаний/ролей @Field.
- blocked сохранён в ChartOfAccountsItem (legacy chart-of-accounts).

Проверено локально: sdk typecheck и controller typecheck зелёные.

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 16:45:39 +00:00
coopops 5c494db262 fix(sdk): синхронизировать schema.gql и zeus с удалением blocked из L3-кошельков
Typecheck / desktop (pull_request) Successful in 13m29s
Typecheck / controller (pull_request) Successful in 13m8s
CI Typecheck падал на @coopenomics/sdk:build (tsc): селекторы
programWalletSelector/ledger2WalletSelector и зависимые capital-селекторы
больше не содержат blocked, но zeus-типы (MakeAllFieldsRequired) всё ещё
требовали его как обязательное поле.

- schema.gql: убран `blocked: String!` из типов Ledger2Wallet и ProgramWallet
  (точечно, без полной регенерации — она тянет несвязанный дрейф схемы).
- zeus (controller/zeus + sdk/src/zeus): убран blocked из Ledger2Wallet и
  ProgramWallet во всех 4 секциях (ValueTypes/ResolverInputTypes/ModelTypes/
  GraphQLTypes). blocked в ChartOfAccountsItem (legacy chart-of-accounts) и
  статус-комментарии (accepted|blocked) сохранены.
- CoopCard.vue: удалён мёртвый закомментированный блок с wallet.blocked.

Проверено локально: sdk typecheck и controller typecheck зелёные.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:43:07 +00:00
coopops 0969505a71 feat(ledger2): убрать механику blocked из L3-кошельков, перевести возврат на отдельный кошелёк-резерв
Typecheck / desktop (pull_request) Failing after 6m36s
Typecheck / controller (pull_request) Failing after 6m30s
Упраздняем субсчёт "заблокировано" на кошельках. Резерв средств под заявку
на возврат паевого теперь выражается переводом на отдельный COOPERATIVE-кошелёк
w.wal.wpend, а не блокировкой внутри w.wal.share. Все движения — только переводы
и сжигание, без BLOCK/UNBLOCK.

Контракты (ledger2/wallet/capital):
- wallets.hpp: новый кошелёк WITHDRAW_PENDING (w.wal.wpend, COOPERATIVE).
- operations.hpp: из WalletOp удалены BLOCK/UNBLOCK/BURN_BLOCKED (числовые
  значения ISSUE/TRANSFER/BURN/NONE сохранены для совместимости истории).
  Переключены 3 операции возврата: o.wal.wthreq -> TRANSFER share->wpend;
  o.wal.wthdec -> TRANSFER wpend->share; o.wal.wthcpl -> BURN wpend.
- walletop.cpp/revert.cpp: удалены кейсы и валидации блокировки.
- migrate(): свёртка blocked->available по ВСЕМ коопам (выполняется автоматически
  при деплое; сигнатура без аргументов). Поле blocked в структурах таблиц
  оставлено deprecated — физическое удаление = смена layout таблицы на живых
  коопах, отдельный cleanup-деплой.
- capital balances/importcontr: больше не читают blocked.
- p.wal.wthdrw.standard.yaml: стандарт приведён к модели "резерв на кошельке".

cooptypes/SDK:
- operations.ts: WalletOp без BLOCK/UNBLOCK/BURN_BLOCKED; 3 операции возврата
  переключены; wallets.generated.ts регенерён (добавлен w.wal.wpend).
- selectors: убран blocked из programWalletSelector/ledger2WalletSelector.

Backend (controller): убрано поле blocked из GraphQL DTO ProgramWallet и
Ledger2Wallet. TypeORM-колонка/внутренние интерфейсы оставлены deprecated
(без DB-миграции).

Frontend (desktop): убраны все поля "Заблокировано" — WalletProgramWidget,
WalletWidget, ParticipantWalletsPage, CoopWalletsPage, CapitalWalletsCardsWidget,
ContributorsListWidget, optimistic blocked_delta.

Границы (не в этом PR): legacy soviet::progwallets blockbal/unblockbal и
donor-marketplace; legacy ledger v1 chart-of-accounts "Заблокированные средства";
полное удаление поля blocked из C++-структур таблиц.

Требует CI-шага: regen GraphQL schema + zeus client (generate-schema/-client)
для согласования schema.gql и zeus-типов с удалённым полем blocked.

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 06:39:59 +00:00
coopops f20764adbd Merge branch 'dev' into worktree-design-wave1
Подтянуть hotfix controller TypeORM, SWC dev-builder и mongo standalone.
2026-05-23 05:33:20 +00:00
coopops 712080ff8f Merge branch 'dev' into worktree-design-wave1
Подтянуть свежие изменения dev в дизайн-ветку перед продолжением миграции стола пайщика.
2026-05-23 05:09:17 +00:00
coopops 76513b8526 [E14][@dark-sun] fix(canon .wallet): просто wrap по пробелам, без overflow-wrap: anywhere
Why: предыдущий фикс `overflow-wrap: anywhere; word-break: break-word`
ломал слова в любом месте даже когда колонка достаточно широкая —
«Минимальный неснижаемый остаток» рендерился по одному слову на строку,
несмотря на ~480px доступной ширины. Реально нужен только wrap по пробелам;
agressive break-word оправдан только для URL-подобных нерасчленяемых строк.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Wave 2 / E10 закрыт.

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

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

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

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

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

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

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

Все props-only, темо-зависимые значения только через --p-* токены.
2026-05-21 17:02:51 +00:00
coopops de245ee810 fix(_dev/ui): «Член кооператива» → «Пайщик» в PersonCard demo
Канон-терминология blago/ЦК: пайщик, не член. Прецедент закреплён
в auto-memory feedback_kooperatsiya_pai.
2026-05-21 16:57:50 +00:00
coopops e7dd0ce641 fix(IdentityPanel): compact тоже в карточке — padding+border+bg
Раньше compact-вариант рендерился без обрамления — avatar упирался
в левый край контейнера, визуально «проваливался» из общего стека
full-карточек (Screenshot_2026-05-21_21-51-09).

Compact теперь: padding 8/12px (тоньше чем full 16px), тот же border
+ surface + radius. В одну строку avatar + ФИО + AccountBadge + status,
но визуально это всё-таки карточка, не голая строка.
2026-05-21 16:52:41 +00:00
coopops e1113391cf fix(ContactSheet): label и значение в отдельных строках
Body раньше был обычным block-контейнером, а .head и .value-row —
оба display: inline-flex. Без явного block-форматирования inline-flex
элементы становятся inline и рендерились в одну строку. Визуально:
Emailivanov@example.ru, Телефон+7 (903)..., Telegram@ivanov — слипшиеся.

Body теперь display: flex + flex-direction: column, gap canon — 8px
в comfortable, 4px в compact. Head/value-row переведены на обычный
display: flex (вместо inline-flex), gap внутри сохранён.

Прецедент 2026-05-21 — Screenshot_2026-05-21_21-42-16, 21-42-31.
2026-05-21 16:44:40 +00:00
coopops 25b6d6c42b chore: убрать закоммиченный symlink node_modules 2026-05-21 16:25:22 +00:00
coopops 05a8ac0a51 fix(domain): E8 — copy-кнопки, отступы и compact-layout
- AccountBadge: copy-кнопка не сжимается (flex-shrink: 0), размер 20×20 + icon 14px — раньше 18×18 + 12px тонула рядом с текстом badge.
- DataRow: column-gap var(--p-3, 12px) → var(--p-5, 20px) + min-label-width 140 → 160px + padding-right на label. Между label и value был визуально слипшийся стык.
- ContactSheet: comfortable margin-top между label и value 2px → 8px (compact 4px). Раньше label «налипал» на значение.
- IdentityPanel compact: переделан в flat flex-row (avatar + ФИО + AccountBadge + status badge) — раньше grid с .body загонял имя и AccountBadge в две строки и avatar «уезжал» от имени. Spec 8.1 требует «только avatar + ФИО + EntityIdBadge в одну строку».
2026-05-21 16:24:45 +00:00
coopops 8344808676 feat(ui): волна 2 / E8 — идентификация и контакты
5 props-only canon-компонентов в src/shared/ui/domain/, регистрация
в boot/ui.ts, demo-секции 21-25 в _dev/ui:

- AccountBadge (story 8.5) — on-chain account name в mono-шрифте,
  copyable + опциональный explorer link. Назван AccountBadge вместо
  EntityIdBadge: имя EntityIdBadge занято под numeric ID пайщика
  в capital. Decision зафиксирован в epics.md и planning-artifacts.
- DataRow (story 8.3) — `label: value` пара для реестровых карточек,
  mono-режим, copyable, slot value-override, hint.
- ContactSheet (story 8.2) — email/phone/address/tg/web с автоиконками,
  кликабельными mailto:/tel:/t.me ссылками, copy и verified-чек.
- IdentityPanel (story 8.1) — Avatar + ФИО + AccountBadge + статус
  (active/blocked/pending) + role + actions-slot; compact/full.
- PersonCard (story 8.4) — Avatar + ФИО + role + AccountBadge +
  ContactSheet + slot meta; compact/comfortable.

Все props-only: useStore/useRouter/useApi внутри запрещены. Темо-зависимые
значения только через --p-* токены, никаких body--dark селекторов.
2026-05-21 15:57:21 +00:00
coopops a192e2c398 fix(BaseCheckbox): приглушить заливку галочки через --p-primary-press
q-checkbox по умолчанию красит .q-checkbox__bg в --q-primary, который в
dark теме равен #2dd4bf — на маленьком квадрате 18px этот яркий бирюзовый
смотрится ядовито. Переопределяем заливку filled-состояния на
--p-primary-press (#134e4a light / #14b8a6 dark) — глубже и спокойнее,
чек-символ остаётся белым.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:37:24 +00:00
coopops d99413f896 fix(tokens): приглушить --p-accent + унифицировать .agreement-link
Старое значение --p-accent (#D84315 light / #FF7043 dark) — слишком
насыщенное, выглядит ядовито рядом с тёплой палитрой ink/canvas. Меняем
на тёплую терракоту: #B85C38 (light) / #E89472 (dark) — тот же warm-tone,
но без перевозбуждения красного канала.

.agreement-link (псевдо-ссылки на просмотр документа в согласиях) была
hardcoded в #1c64f2 (синий) light / #ff9f43 (жёлто-оранжевый) dark и не
совпадала со ссылкой «Устав кооператива». Унифицируем: все ссылки внутри
согласий теперь var(--p-accent), визиt-цвет тот же — без фиолета.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:34:28 +00:00
coopops 77ae6bb47d fix(ReadStatement): чекбоксы согласий — выравнивание + gap + accent ссылка
BaseCheckbox block-вариант: убран собственный display:flex + align-items:
flex-start, который перебивал внутренний flex Quasar и сдвигал label вверх
относительно inner. Теперь Quasar сам центрирует inner на первой строке.
Добавлен padding-left: 8px на .q-checkbox__label — больше воздуха между
галочкой и текстом согласия.

ReadStatement: ссылка «Устав кооператива» — --p-primary → --p-accent
(тёплый оранжевый #D84315 / #FF7043 dark) как акцентный цвет палитры.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:26:19 +00:00
coopops 6016c0597b feat(UserDataForm): canon-стиль полей + тип аккаунта на BaseRadioCard
UserDataForm/UserDataForm.vue:
- Выбор типа аккаунта: три q-btn(glossy, height:75px) → три BaseRadioCard
  с title/description; больше нет teal-«кнопок-плиток»
- Возврат к выбору типа: flat q-btn → BaseButton(ghost) с arrow_back

Sub-forms (Individual / Entrepreneur / Organization + Create*):
- q-input/q-select: standout="bg-teal text-white" → outlined color='primary'
  (canon-tone, единый primary-акцент вместо устаревшего teal)
- .q-gutter-sm.q-mt-md → .user-data-stack (flex column, gap токены)
- OrganizationDataForm: кнопка «совпадает» — color='teal' → flat primary

Валидация q-form.validate() и :rules сохранены — это всё ещё q-input
с rules-массивом, только tone и контейнер переехали на canon-токены.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:16:31 +00:00
coopops eeecab96da feat(ui): BaseCheckbox + BaseRadioCard и миграция SignUp-шагов
- BaseCheckbox: обёртка q-checkbox с canon-стилями, block-вариант для длинных
  согласий (чекбокс прижат к верху, label многострочный)
- BaseRadioCard: карточка-опция с радио-индикатором справа, props title /
  description / meta + slot fallback'и; используется для выбора программы

Миграции SignUp:
- ReadStatement: q-checkbox → BaseCheckbox(block); согласия в .agreements-стек
- SelectProgram: q-list+q-radio → список BaseRadioCard
- GenerateAccount: q-input → BaseInput(readonly, mono) с copy в #append;
  q-checkbox → BaseCheckbox; автоселект через querySelector('input').select()
  вместо ref.select() (BaseInput не пробрасывает внутренний API q-input)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:11:56 +00:00
coopops 127cf36ca9 fix(desktop): .ws-switcher__title padding-top 2px
Воздух над заголовком стола внутри WorkspaceSwitcher — заголовок не упирается
в верхнюю границу caption «ПК «ВОСХОД»».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:03:05 +00:00
coopops f158afe485 fix(desktop): .rail__top padding 16px 8px + убрать disabled-стиль у WorkspaceSwitcher
- .rail__top: padding 18px 20px → 16px 8px (унификация боковых отступов)
- WorkspaceSwitcher: убраны :disabled-атрибут и hover:not(:disabled) — у пайщика, которому доступен только один стол, кнопка выглядит как обычно (без not-allowed-cursor, без приглушённого цвета). Меню q-menu просто не открывается через v-if='workspaces.length > 1'. Тихое игнорирование клика вместо явного запрета.
2026-05-20 18:55:36 +00:00
coopops 521575ab6a fix(desktop): WorkspaceSwitcher — фильтр столов по ролям + 3-строчный title
WorkspaceSwitcher показывал ВСЕ workspaceMenus, включая столы выше роли пользователя (например, «Стол председателя» видел рядовой пайщик), что приводило к ошибке access-denied при попытке переключения.

- Добавлена фильтрация по иерархии: chairman ⊇ member ⊇ user. Председатель видит столы любых ролей; член совета — user+member; пайщик — только user. Workspace без meta.roles (или с пустым массивом) — публичный для всех (та же логика, что в legacy WorkspaceMenu, плюс корректное наследование прав).
- Title допускает перенос на 3 строки (line-clamp 3) — «Стол вычислительных ресурсов» теперь умещается полностью
- Расширена кнопка switcher'а (margin сторон 8→4 в LeftDrawerMenu), даёт больше места под текст
2026-05-20 18:41:42 +00:00
coopops c2eefbf244 fix(desktop,signup): WorkspaceSwitcher polish + Bank toggle QR↔реквизиты
WorkspaceSwitcher:
- Brand-строка теперь собирается из vars.short_abbr + «vars.name» (получается «ПК «ВОСХОД»»). Раньше показывал только short_abbr — название кооператива было «потеряно»
- Title допускает перенос на 2 строки (line-clamp 2) — длинные названия типа «Стол вычислительных ресурсов» больше не обрезаются ellipsis'ом с одного слова
- Chevron приклеен к верху строки (align-self start) — корректно при двухстрочном title
- Меню workspace: canon padding (8/12) вокруг q-item, отступ между avatar-section и текстом — иконка и название больше не упираются в края
- Затемнение фона при открытом меню (rgba(9,9,11,0.32) overlay через Teleport в body) — меню больше не сливается с контентом за drawer'ом

Bank.vue (карточка платежа):
- Убрали q-expansion-item (раскрытие вниз внутри карточки выглядело перегружено)
- Кнопка-toggle: «Показать реквизиты» рядом с «Скачать QR». Клик переключает зону под сводкой между QR и списком 9 BaseInput'ов с copy-кнопками
- В режиме реквизитов: кнопки меняются на «Скопировать всё» (primary) + «Показать QR» (secondary)
- QR-canvas использует v-show чтобы сохранять состояние при toggle назад; реквизиты — v-if чтобы не держать BaseInput'ы в DOM
2026-05-20 18:22:58 +00:00
coopops 3f43a77167 feat(desktop): WorkspaceSwitcher + фикс ReadStatement стилей + плоский Bank.vue
ReadStatement: backend-HTML генерирует с inline-стилями и Quasar text-h* классами, которые перебивали :deep правила. Усилили все ключевые селекторы !important + добавили охват .text-h1/h2/h3/h4/h5/h6 и .text-right (под мета-блок «УТВЕРЖДЕНО…»).

Bank.vue (карточка платежа):
- убрали отдельные surface-карточки вокруг сводки и QR (получалось «карточка-в-карточке-в-карточке»)
- сводка → плоский <dl> со строкой-разделителем снизу (border-bottom var(--p-line))
- QR → просто центрированный canvas без обёртки/фона/рамки; белый цвет внутри canvas — функциональное требование контрастного сканирования, не дизайн-выбор
- detail-expansion → без своего фона, только border-top/bottom

WorkspaceSwitcher (новый widget src/widgets/Desktop/WorkspaceSwitcher/):
- Карточка-кнопка в шапке drawer: иконка кооп + caption «ПК «ВОСХОД»» + bold «Стол совета» (текущий) + chevron
- Клик → q-menu со списком всех workspaceMenus (Стол пайщика, Стол совета, Стол председателя и т.д.); выбор → selectWorkspace + goToDefaultPage
- Подключён через #brand slot AppDrawer'а в LeftDrawerMenu, заменяет дефолтный brand-row
2026-05-20 17:48:25 +00:00
coopops 9ea9467c2c feat(signup): canon-типографика заявления + рефакторинг карточки платежа
ReadStatement (этап 6.1):
- Backend генерирует HTML документа — локально нормализуем через :deep
- h1 уменьшен с монструозного дефолта до canon h3 (20px), центрирован
- h2/h3, p, strong, ul/ol, table, hr — типографика по var(--p-*)
- мета-блок .approved/.meta-right (УТВЕРЖДЕНО…) — ink-2, мельче, плотнее
- Чекбоксы соглашений снизу не трогали (как просил пользователь)

Bank (этап 6.2 — карточка платежа PayWithProvider):
- QR-код вынесен в фокус: сначала сводка (получатель/сумма/назначение) → QR → действия → реквизиты для ручного перевода скрыты под q-expansion-item
- Полные 9 полей (ИНН/БИК/КПП/корр-счёт/...) переведены на BaseInput(readonly, mono) с copy-кнопкой в #append; раскрываются только при необходимости
- Кнопки «Скачать QR» (primary) и «Скопировать реквизиты» (secondary) — BaseButton
- Удалены: q-btn(push), case-mixed «скопировать реквизиты», стиль #qr через global селектор
2026-05-20 17:20:15 +00:00
coopops 9a3ee9afa4 feat(invite): этап 5 — миграция Invite на canon AuthCard + BaseInput/BaseButton
- InvitePage: row/col-обёртка → .invite-page flex-center + canon padding (как у SignInPage)
- Invite widget: AuthCard со штатными title/subtitle (вместо CAPS-заголовка с background-clip градиент-текста)
- BaseInput(mono, readonly) для приватного ключа — canon-оформление key/hash полей
- BaseButton(ghost) для копирования + BaseButton(primary) для «Установить ключ»
- Удалены: градиент-текст, semi-transparent blue/green tint карточки вокруг инструкции и чекбокса, hover translateY + box-shadow glow, custom border-radius/box-shadow для q-input и кнопок, мобильные @media-блоки (canon-токены и AuthCard сами адаптивны)
2026-05-20 16:54:02 +00:00
coopops fd4bb48870 fix(signup): canon-рамка для контейнера подписи вместо неонового glow
- .signature-container: 3px primary border + 3-слойный box-shadow glow → 1px dashed по var(--p-line-2), surface-2 фон, без свечения; на hover лишь окрашивается рамка
- .signature-hint: переехала в центр контейнера, canon ink-2 цвет, убран frosted-chip с blur/shadow — теперь спокойная метка «Оставьте собственноручную подпись в рамке», pointer-events: none пропускает клики на canvas
- min-height снижен 300→220, padding на canon-токены
2026-05-20 16:52:23 +00:00
coopops d41ce5712d fix(signup): отступ сверху, canon фон q-stepper, ширина email-поля
- SignUp.vue: добавлена обёртка .signup-page (padding + center + min-height) — как у SignInPage; раньше страница была прижата к шапке
- AuthCard maxWidth: 1000 → 720 — историческое 1000 для трёх-колоночной UserDataForm; для остальных шагов выглядит абсурдно широко
- q-stepper: убраны собственный фон/тень/padding (не «карточка в карточке»), линии-разделители переведены на var(--p-line)
- EmailInput: max-width поля 360px — email не должен растягиваться во всю ширину карточки
2026-05-20 16:39:11 +00:00
coopops ed9f483e7a fix(authcard): убрать оборванный barrel-экспорт после удаления старого AuthCard
vue-tsc ругался: shared/ui/index.ts:21 — Cannot find module './AuthCard'.
- shared/ui/index.ts: убран `export * from './AuthCard'`
- LoginRedirectPage.vue: импорт переключён на canon `shared/ui/domain/AuthCard`, CAPS-заголовок заменён на canon `title` prop
- Invite.vue: импорт переключён на canon (полная миграция Invite — этап 5)
2026-05-20 16:16:11 +00:00
coopops d974d7698b feat(signup): этап 4 — миграция SignUp на canon AuthCard + BaseButton/BaseInput
- canon `shared/ui/domain/AuthCard` переписан на pug
- SignUp.vue переключён со старого «глянцевого» AuthCard на canon (title prop вместо CAPS-заголовка)
- старый `shared/ui/AuthCard` удалён (использовался только в SignUp)
- 7 подэкранов SignUp (EmailInput, SetUserData, SelectProgram, GenerateAccount, SelectBranch, ReadStatement, SignStatement): q-btn → BaseButton (ghost для «назад», primary для «Продолжить»)
- EmailInput: q-input → BaseInput, валидация rules переведена на computed :error
- q-input в GenerateAccount оставлен из-за зависимости от ref.select()
2026-05-20 16:10:50 +00:00
coopops bc5d7deda5 content(brand): og-image из poster-logo-horizontal + новый длинный дескриптор
- og-image.png — взят brand-постер ~/blago/production/shared/poster-logo-horizontal.png,
  отресайзен до 1000px по ширине и центрирован на полотне 1200×630
  с белыми краями по вертикали. Шрифт/композиция родные бренда.
- description / og:description / twitter:description: «Цифровой Кооператив —
  система управления хозяйством. Регистрация пайщиков, заказы на поставку
  и приобретение имущества, собрания совета, общие собрания пайщиков,
  взаимные расчёты на блокчейне, автоматический документооборот и бухбаланс
  на основе простой электронной подписи.»
2026-05-20 15:56:13 +00:00
coopops 44628dfde4 content(brand): обновить превью и мета — система управления хозяйством на платформе «Благо»
- og-image.png перерисован: «Цифровой Кооператив» / «Система управления
  хозяйством» / «на платформе БЛАГО — кооперативной экономики для жизни» /
  «Регистрация пайщиков · заказы на поставку и приобретение имущества ·
  расчёты на блокчейне».
- description / og:description / twitter:description выровнены под этот
  же копирайт. Слово «взаимоотношения с пайщиками» убрано как слишком
  узкое — управление хозяйством включает в себя ещё заказы, реестры
  и документы.
2026-05-20 15:44:57 +00:00
coopops 4fd79ffda9 feat(brand): системный логотип ЦК + чистка ассетов + OG-превью
- src/assets/logo.svg, public/favicon.svg, public/logo.svg — единая SVG
  логотипа кооператива (path с «ц» внутри). Скачано с
  https://цифровой-кооператив.рф из шапки сайта.
- src/assets/logo.svg: fill -> currentColor чтобы лого внутри
  canon .rail__brand наследовал --p-primary (для тёмной темы тоже).
- LeftDrawerMenu подключает лого через slot #brand-icon AppDrawer
  (импорт ?raw + v-html). Внутри зелёного квадрата шапки рейла теперь
  наш системный знак, а не дефолтный q-icon "dashboard".
- index.html: favicon → /favicon.svg, apple-touch-icon тоже на SVG.
  Удалены неработающие ссылки на /favicon.ico и /apple-touch-icon-*.png.
- index.html: добавлены description + расширенные og:* и twitter:*
  с конкретным описанием и og-image (1200×630), чтобы ссылки в чатах
  выглядели прилично.
- public/og-image.png — превью 1200×630, ImageMagick-сгенерированный
  композит из логотипа + текста.
- Удалены неиспользуемые ассеты:
  - public/icons/, public/pwa/ — старые favicon/PWA-PNG'и всех размеров;
  - src/assets/* — 40 легаси SVG/PNG (anime, blockchain, dacom*, flow*,
    header-logo, quasar-logo-vertical, system*, club*, welcome.jpeg и т.д.).
  Оставлены только src/assets/pin.svg (используется в Map.vue) и
  src/assets/logo.svg (новый).
2026-05-20 15:35:23 +00:00
coopops fca07fc3c7 fix(layout): убрать TS-каст из pug-шаблона AppDrawer
Pug-template передаёт выражения в Vue compiler как строки; конструкции
вида (entry as RailItem).route не превращаются в JS и падают
SyntaxError в браузере: «Unexpected identifier 'as'».

Касты унесены в <script>: добавлены computed flatItems и
normalizedGroups, шаблон работает с уже типизированными массивами без
inline-TS.
2026-05-20 15:20:25 +00:00
coopops dfd1ebd2a9 fix(layout): этап 3.3 — все canon-компоненты на pug + фикс импорта cmdk
- LeftDrawerMenu: импорт useCmdkStore → useCmdkMenuStore (правильное имя).
  Без этого ломался ESM build всего drawer'а: «no exported member
  useCmdkStore» — RailUserCard не монтировался, поэтому и chevron свёртки
  «не работал», и Найти/Пополнить не реагировали.
- AppDrawer.vue, AppHeader.vue, RailUserCard.vue переведены на pug
  (lang="pug"). В проекте везде pug — никакого исторического HTML в
  canon-компонентах быть не должно.
2026-05-20 15:12:38 +00:00
coopops 7d6592d1b6 fix(layout): этап 3.2 — pug + v-model:collapsed + рабочие триггеры
- q-drawer width 240 → 248px (совпадает с canon --p-rail-w, кошелёк больше
  не уплывает справа из-за обрезки на 8px)
- LeftDrawerMenu: убран весь :deep override на .rail__usercard/.rail__signout,
  возвращаем canon margin/padding как есть
- LeftDrawerMenu: добавлен v-model:collapsed на RailUserCard с сохранением
  в localStorage — chevron «свернуть/развернуть» снова работает
- LeftDrawerMenu: кнопка Найти теперь дёргает cmdkStore.openDialog() напрямую
  (фейковый KeyboardEvent не доходил до глобального обработчика)
- LeftDrawerMenu: «Пополнить» → useDepositDialog().open(); DepositButton +
  WithdrawButton рендерятся скрыто как держатели q-dialog (q-portal в body)
- Header.vue и LeftDrawerMenu.vue переписаны на pug (как остальной проект)
2026-05-20 14:40:31 +00:00
coopops e5799ba702 fix(layout): этап 3.1 — RailUserCard в footer + canvas-фон + отступы рейла
- AppDrawer: плоский items оборачивается в .rail__nav, появляется gap 4-8px до cmdk
- cmdk margin 16→8px, чтоб выровнять с rail__nav (8px от стенки)
- LeftDrawerMenu footer: WalletCardMini+LogoutButton → один canon RailUserCard
  (avatar+balance в primary-soft + Пополнить + встроенный signout)
- :deep override на .rail__usercard margin 16→12/8 и .rail__signout padding 12/20→12/12
- quasar-canon: body--light/dark + q-layout/q-page-container/q-page красятся
  через --p-canvas, чтобы фон страницы соответствовал шапке и рейлу
2026-05-20 14:19:18 +00:00
coopops 1e1a70ded2 feat(layout): этап 3 — WalletCardMini + canon LogoutButton в footer AppDrawer
LeftDrawerMenu:
- legacy `MicroWallet` (ColorCard teal с большой карточкой пайщика + ИП-badge
  + Deposit/Withdraw micro-кнопки) → canon `WalletCardMini` из
  widgets/wallet-card-mini (тонкая карточка из shared/ui/domain/WalletCard
  compact-variant, читает walletStore сама).
- Убран toggle «свернуть/развернуть нижнюю секцию» — нижняя секция теперь
  всегда видна (короткий компактный блок: WalletCardMini + Выйти).
- Удалены неиспользуемые `onMounted`/`ref` импорты, slide-анимация и CSS
  toggle-кнопки.

LogoutButton:
- pug + q-item с красной полупрозрачной плашкой → плоский ghost-button в
  стиле rail-пункта AppDrawer: прозрачный фон в покое, на hover —
  `--p-neg-soft` фон + `--p-neg` текст/иконка.
- Из uppercase «ВЫЙТИ» в title-case «Выйти» (canon — никаких uppercase).

Визуально левый drawer теперь полностью в canon-семье: единый rail с
пунктами + поиском + компактным wallet-балансом + ghost-кнопкой выхода.

Закрывает этап 3 из 5 в волне «приземление DS на реальный layout».
Дальше — SignUp (этап 4) и Invite (этап 5).
2026-05-20 14:01:08 +00:00
coopops 61bd6f2000 feat(layout): этап 2 — AppHeader в реальном layout (заменяет MainHeader)
widgets/Header/CommonHeader/Header.vue теперь использует canon AppHeader
из shared/ui/layout. Высота шапки 56px (--p-topbar-h), border-bottom
hairline, без shadow и фоновых градиентов.

Слоты:
- #crumb: BackButton (для авторизованных), либо title с названием
  кооператива из system.info.vars для гостей/install.
- #actions: headerActions injection через useHeaderActionsReader (всё что
  страницы инжектят — Deposit/Withdraw, SettingsDropdown и пр. — рендерим
  как было).
- #notifications: NotificationCenter (только loggedIn + isClient).
- #theme: ToogleDarkLight — пока legacy, сохраняет storage + PWA-цвет;
  наш canon ThemeToggle их не делает (привяжу позже отдельным эпиком).
- #profile: BaseButton primary для гостей — Регистрация/Вход (в зависимости
  от текущего route).

Удалены:
- MainHeader.vue (вся логика scroll arrows + carousel actions group ушла —
  canon `.topbar__actions { gap: 8px }` достаточно; overflow-scroll
  будет вернут отдельно если потребуется на узких экранах).
- HeaderStyles.scss (стили scroll-arrow и q-toolbar overrides больше
  не нужны — canon-разметка через `<header class="topbar">`).

q-header Quasar остаётся wrapper'ом (sticky-poзиционирование в q-layout),
внутри — AppHeader даёт canon-разметку.
2026-05-20 11:54:27 +00:00
coopops f651f05a06 feat(layout): этап 1 — AppDrawer в реальном layout (заменяет LeftDrawerMenu)
Переписан widgets/Desktop/LeftDrawerMenu/LeftDrawerMenu.vue: внутри теперь
canon `AppDrawer` из shared/ui/layout с rail-видом MONO v2 (логотип ПК +
короткое имя кооператива в шапке рейла, ⌘K-поиск, пункты-router-link'и
с иконкой/бэйджем, sticky-footer).

Адаптер store→canon:
- items: desktop.activeSecondLevelRoutes → RailItem[], с прежней
  фильтрацией по roles + meta.conditions + meta.hidden (один-к-одному
  логика из SecondLevelMenuList).
- activeKey: вычисляется из router.currentRoute с поддержкой группового
  паттерна project-* → projects-list.
- @select: router.push({name, params:{coopname}}) или
  actionsStore.executeAction(meta.action), закрытие drawer на mobile.
- @cmdk: эмиттим keyboard event ⌘K → CmdkMenu (он смонтирован глобально
  в default.vue) подхватывает.

Footer-слот: пока сохранён legacy MicroWallet + LogoutButton + toggle
свернуть/развернуть (заменим на WalletCardMini в этапе 3).

default.vue: q-drawer width 200 → 240 (canon-ширина rail'а).

Что НЕ затронуто этим коммитом: верхняя шапка (legacy MainHeader),
правый drawer для actions, q-footer для гостей. Их меняем отдельными
этапами 2–3.
2026-05-20 11:38:56 +00:00
coopops f3d0192e6b fix(ui): убрать конфликт с Quasar — .row → .u-row
Корневая причина «оторванной» галочки в ResetKeyForm: я ранее задал в
mono-platform/components.css глобальное `.row { display: flex;
align-items: center; gap: 12px; }`. Quasar внутри `q-checkbox` (а также
q-radio, q-toggle, q-btn-toggle и др.) сам добавляет class="row" на
корень компонента и использует его как inline-flex — мой `gap: 12px`
раздвигал `.q-checkbox__inner` и `.q-checkbox__label` на 12px по всему
приложению.

Решение:
- Переименовать утилиту `.row` → `.u-row` (та же семантика, тот же набор
  модификаторов --wrap, --gap-2/4/6). У Quasar теперь свободный .row.
- Обновить все 6 использований в /_dev/ui.
- Откатить предыдущий :deep(.q-checkbox__label) padding-left:4px workaround
  в ResetKeyForm — лечил симптом, причина устранена.

Затрагивает только нашу dev-витрину и canon-utility; legacy pug-виджеты
с `.row.justify-center` продолжают работать через стандартный Quasar
flex-grid класс.
2026-05-20 11:18:39 +00:00
coopops 7e429c481b fix(ui): ResetKeyForm — сжать зазор между галочкой и текстом «Я сохранил ключ»
Quasar по умолчанию даёт ~12–16px padding на .q-checkbox__label, в
compact-форме сохранения ключа это смотрелось «оторвано». Уменьшаем до 4px
через :deep override на классе .rk-form__confirm.
2026-05-20 11:05:02 +00:00
coopops 7819d9c5bb feat(dev): секция 20 — ResetKey двухэтапный flow в /_dev/ui
Три кадра в одной секции: LostKey (ввод email), ResetKeyForm check-mail
(уведомление «письмо отправлено»), ResetKeyForm save-key (демонстрация
сохранения ключа с моковым сгенерированным аккаунтом). Submit на третьем
кадре регенерирует мок-ключ для повторной проверки.

Заменяет ошибочную секцию ChangeKey из предыдущей итерации.
2026-05-20 10:44:50 +00:00
coopops c0df025e1f fix(ui): откатить ChangeKey + мигрировать настоящий ResetKey flow на base-компоненты
ChangeKey-фича (формы current/new/confirm WIF + успешный диалог) была
ошибочной интерпретацией E7. Реальный flow «сменить ключ» в продукте —
это **двухэтапный ResetKey**:

  1. LostKey: пайщик вводит email (он потерял ключ — текущего WIF нет),
     бэк присылает письмо со ссылкой `?token=...`.
  2. ResetKey: при заходе по ссылке клиент сам генерирует новый ключ
     прямо в браузере (через `useCreateUser().generateAccount()`),
     показывает приватный ключ readonly с «Скопировать», требует
     подтверждения «Я сохранил ключ» и вызывает on-chain
     `resetKey({token, public_key})`.

Текущий ключ ввести нельзя (его нет). Новый ключ ввести нельзя (только
генерируется). Никакого подтверждения нового ключа — он один и тот же.

Что сделано:
- Удалена ошибочная `features/User/ChangeKey/`.
- Старый `widgets/Registrator/ResetKey/ui/ResetKey.vue` (pug + сырые
  q-input/q-checkbox + кастомные градиентные стили) разбит на:
  * `ResetKeyForm.vue` — presentation (props-driven, режимы
    `check-mail` / `save-key`), без useRouter/useApi — пригоден для
    /_dev/ui витрины.
  * `ResetKey.vue` — connected обёртка, читает `route.query.token`,
    генерит account через `useCreateUser`, диспатчит `resetKey` через
    `useResetKey`, на успех → router.push на signin.
- Новая UI собрана на BaseForm + BaseInput readonly mono + BaseButton +
  BaseBanner + AuthCard.

/_dev/ui секция 20 теперь показывает три кадра подряд: LostKey (email
шаг), ResetKey check-mail, ResetKey save-key с моковым сгенерированным
ключом.

FR11, UX-DR9, UX-DR15.
2026-05-20 10:44:32 +00:00
coopops 5dba22d457 fix(ui): BaseInput/BaseSelect — явный проброс слотов вместо v-for in $slots
Generic-проброс `<template v-for="(_, slotName) in $slots" #[slotName]>`
рушится в Quasar 2.19 рендере: при наличии scoped-слота #append вылетает
`Cannot read properties of null (reading 'key')` в renderSlot + Vue warns
по `_isVue`/`constructor` свойствам. Заменяем на явный список слотов
(prepend / append / before / after / hint у q-input; +option, selected-item
у q-select) — синтаксис, который Quasar держит корректно.

Кейс всплыл на ChangeKeyForm: #append с q-icon (toggle visibility) ронял
страницу /_dev/ui целиком.
2026-05-20 08:47:48 +00:00
coopops 698b1d6333 feat(dev): секция 20 — ChangeKey flow в /_dev/ui (визуальная проверка E7)
Подключение ChangeKeyForm + ChangeKeySuccessDialog к dev-витрине.
Submit запускает заглушку useChangeKey().changeKey() (≈600 мс), по успеху
открывает диалог с новым WIF. Generate генерирует валидный K1 ключ через
SDK и заполняет оба новых поля. Confirmed сбрасывает форму.
2026-05-19 20:03:38 +00:00
coopops 8b3263bf70 feat(ui): E7.2 ChangeKeySuccessDialog — подтверждение через BaseDialog без backdrop-exit
Диалог успеха смены ключа: новый WIF в моноширинном блоке с copy-кнопкой,
BaseDialog с closeOnBackdrop=false, closeOnEscape=false, hideCloseButton=true.
Закрытие только через primary-кнопку «Я сохранил ключ»; до копирования
кнопка показывает guard-баннер вместо немедленного закрытия. По confirmed
эмитим событие — реальный flow подключит router.push на дашборд.

FR11, UX-DR11, UX-DR15. Story 7.2 из epics.md.
2026-05-19 20:03:26 +00:00
coopops 9578dc1b86 feat(ui): E7.1 ChangeKeyForm — смена ключа на BaseForm + BaseInput + BaseBanner
Форма смены ключа пайщика на новых компонентах дизайн-системы.
BaseBanner severity=warn с предупреждением о потере, checklist последствий,
поля current/new/confirm WIF моноширинные с toggle visibility, ghost-кнопка
«Сгенерировать» через PrivateKey.generate из @wharfkit/session.

Бизнес-логика — заглушка с TODO на привязку on-chain change-key action
(pattern см. features/User/ResetKey/api) и TODO на OtpInput шаг подтверждения
(приходит в Эпике 10).

FR11, UX-DR9, UX-DR15. Story 7.1 из epics.md.
2026-05-19 20:03:12 +00:00
coopops 45cfa29940 fix(ui): Avatar tone — единая семантика neutral/primary/accent, убрать ink
Bug после rename --p-accent → --p-primary: класс .avatar--accent смотрел
на --p-primary-soft (canon teal), а должен был — на --p-accent-soft
(warm orange по новой семантике). И вариант ink (белый bg + dark text)
визуально выпадал из палитры — крайний xl-аватар на демо-витрине казался
«не пришей к звезде рукав».

Новая семантика AvatarTone:
- neutral (default) — серый surface-3 + ink-1 (нейтральный)
- primary — canon-teal soft + canon-teal text (основной выделение)
- accent — warm-orange soft + warm-orange text (редкое выделение,
  например подсветка пайщика по batch_id)

Удалён вариант ink — не использовался в реальном коде.
В dev-витрине xl-аватар: tone="ink" → tone="primary".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:46:27 +00:00
coopops d0a8dc0080 revert(ui): убрать override центрирования suffix/prefix — оставить Quasar дефолт
Override (padding-top:0 + align-self:center на dense) не отрабатывал —
Quasar в dense держит prefix/suffix по baseline вводимого значения,
и пользователю это устраивает. Убираем неработающий override, чтобы
не мешался в дальнейших правках.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:42:30 +00:00
coopops 8d3cabab9b fix(ui): BaseDialog — отступ через margin-top второго ребёнка
Inline `style="margin:0"` на <p>-описании перебивал мой margin-bottom
через CSS (специфичность inline > selector). Переключаюсь на
`> :first-child + *` margin-top — отступ крепится ко второму ребёнку,
не зависит от inline-стилей первого.

Работает универсально: <p> + <BaseInput>, <p> + <BaseForm>, любая
другая пара «описание + контент».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:39:29 +00:00
coopops 5ba95a2e6d feat(ui): --p-accent (warm orange) + suffix-центр + точная вёрстка gap
1. --p-accent (canon token): #D84315 (light) / #FF7043 (dark).
   Совпадает с legacy Quasar $accent (warm orange, Material Deep Orange).
   Проброшен в --q-accent через CSS var — legacy color="accent",
   var(--q-accent), text-accent продолжают работать без изменений,
   но теперь следуют за canon-темой (light/dark). Резерв под редкое
   выделение: ссылки batch_id и т.п.
2. quasar-canon.css: .q-field--dense .q-field__suffix/prefix —
   padding-top:0 + align-self:center. Quasar по умолчанию ставит
   padding-top: 24px под floating-label, отчего «RUB» визуально
   опускался к низу control'а. Теперь в одну линию со значением.
3. BaseForm: убран gap 12px из .base-form__body — reserve-hint-space
   у q-input даёт ~24px снизу под error/hint, дополнительный gap
   делал расстояние между инпутами неприятно большим.
4. BaseDialog: убран универсальный gap из body, оставлен margin-bottom
   только на первом <p>/.intro — между описанием и первым инпутом
   воздух нужен, между остальными детьми reserve-hint-space хватает.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:33:11 +00:00
coopops 6d5d614a2c fix: убрать случайный symlink node_modules из индекса
Промахнулся `git add -A` — захватил worktree-symlink node_modules
(он pnpm-shared с основным репо). .gitignore покрывает node_modules/
но симлинк-файл — отдельный путь.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:24:25 +00:00
coopops 0bac215035 refactor(ui): --p-accent → --p-primary — устранить конфликт с Quasar primary
Контекст: после проброса --q-primary = var(--p-primary) (4fddfe2343)
наш токен --p-accent и Quasar primary стали обозначать ОДИН и тот же
canon-teal — два имени для одного цвета это когнитивный шум, и при
росте платформы будет путать. Имя `accent` оставляем свободным под
будущий ВТОРОЙ цвет (warm-orange, для редкого выделения вроде ссылок
по batch_id) — это отдельная задача когда понадобится.

Переименовано везде (109 ссылок в 6 файлах):
- --p-accent          → --p-primary
- --p-accent-hover    → --p-primary-hover
- --p-accent-press    → --p-primary-press
- --p-accent-soft     → --p-primary-soft
- --p-accent-line     → --p-primary-line
- --p-accent-strong   → --p-primary-strong
- --p-ink-on-accent   → --p-ink-on-primary

Файлы: tokens.css, quasar-canon.css, components.css, RailUserCard,
AuthCard, _dev/ui (включая user-facing labels палитры «Accent → Primary»
и заголовок секции «Акцент → Основной цвет»).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:23:58 +00:00
coopops 4fddfe2343 feat(ui): canon-цвет через --q-primary CSS var; вернуть dense + rounded
- tokens.css: проброс canon-палитры в Quasar brand-переменные
  (--q-primary/negative/positive/warning/info = соответствующие
  --p-* токены). Резолвится lazy → автоматически следует за темой.
  Теперь Quasar САМ красит focus q-field, q-btn color="primary",
  bg-primary utility-классы в canon-цвета без CSS-overrides;
- quasar-canon.css: убраны border-color overrides на ::before
  (default/focus/error) — Quasar управляет цветом через --q-primary
  и собственный механизм --focused/--highlighted. Оставлены только
  background, text-color и rounded углы (border-radius var(--p-r-sm));
- BaseInput/Select: вернули `dense` (компактный 40px) и явный
  `color="primary"` (canon teal через --q-primary).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:14:20 +00:00
coopops 9f38148dc4 fix(ui): BaseDialog — уменьшить отступ между body и footer-кнопками
Между нижним инпутом и footer-кнопками был ~32px (q-card-section
padding-bottom 20 + q-card-actions padding-top 12) — выглядело
растянуто. Сводим к 16px (8+8) — кнопки сидят ближе к контенту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:06:27 +00:00
coopops 559493a4b2 fix(ui): q-field overrides — оставить только цвет, всё остальное дефолт Quasar
Регрессия мерцания границы и приплюснутости — следствие борьбы наших
overrides с встроенной Quasar анимацией. Возвращаем дефолт: Quasar
сам решает геометрию (height/padding/border-radius/font), анимации,
opacity ::before и focus-индикацию. Наша задача — только покрасить
в canon-цвета.

Удалено из .q-field--outlined:
- height/padding/border-radius/font-size/font-family overrides;
- transition border-color (Quasar сам делает свой transition);
- opacity:1 !important на ::before (был костыль от мерцания);
- box-shadow:none на --focused/--highlighted;
- display:none на ::after underline;
- override .q-field--dense;
- override .q-field--float .q-field__label (Quasar анимирует label сам);
- focus-shadow / outline reset на native input.

Оставлено (только цвет):
- background: var(--p-surface);
- border-color: line-1 / accent (focus) / neg (error);
- color текста, placeholder, label, prefix/suffix, hint/error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:05:57 +00:00
coopops ed61bb46b6 fix(ui): мерцание границы q-input на focus + unused import в BaseTable
- Quasar при focus анимирует opacity ::before (прячет старую границу
  перед показом ::after underline). Так как ::after мы спрятали,
  получался видимый gap ~200-400 ms «граница исчезла → появилась
  с подсветкой». Фикс: opacity:1 !important на ::before,
  transition только по border-color.
- BaseTable.vue: убран неиспользуемый импорт BaseTableColumn.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:01:31 +00:00
coopops 113a7b8a1b fix(ui): убрать dense из BaseInput/Select; gap в Dialog/Form body; wallet 17px
- BaseInput/BaseSelect: убран `dense` — возвращаемся к дефолтному
  outlined Quasar (control ~56px). Floating-label получает достаточно
  воздуха, текст значения визуально центрируется без сжатия;
- BaseDialog: `.base-dialog__body` теперь flex-column с gap 16px —
  описание/инпуты/таблицы внутри диалога получают единый воздух
  без правки потребителя (пример: «Паевой взнос: укажите сумму…» →
  отступ до инпута появляется автоматически);
- BaseForm: `.base-form__body` gap 4px → 12px — те же требования
  внутри формы;
- WalletCard: `.wallet__metric-val` 19→17px — итоговое значение
  для аккуратного баланс-блока FULL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:00:14 +00:00
coopops 390c5e9a5c fix(ui): убрать stack-label, двойную focus-подсветку и mini-wallet из шапки
1. Stack-label: BaseInput/BaseSelect → только `dense`. Quasar
   стандартно сам корректно держит floating-label в dense+outlined,
   без CSS-override метка работает по документации Quasar.
2. Двойная focus-подсветка: убран наш box-shadow var(--p-focus-ring)
   поверх Quasar внутренней focus-индикации (виден был как «белый
   ринг» на тёмной + наш teal). Теперь только border-color меняется
   через ::before (canon-минимализм). Дополнительно `box-shadow: none
   !important` на --focused/--highlighted control и `display:none` на
   ::after underline (на случай dense Quasar variants).
3. Mini-wallet в правом верхнем углу AppHeader: слот #wallet удалён
   из API (баланс уже отображается в RailUserCard в drawer'е,
   дубль в шапке избыточен). Убран из dev-витрины каркаса.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:52:12 +00:00
coopops 0515994833 fix(ui): WalletCard и mini-wallet — снизить визуальный вес цифр
Регрессия после Quasar wrapper-pivot: метрики кошельков выглядели
крупно по сравнению с заголовком/sub-label, а padding'и были
избыточны для compact-варианта.

- .wallet (FULL): padding 18→14px по верт, иконка 44→40px,
  metric-val 22→19px, ccy 13→12px;
- .wallet--row (compact): padding 14→10px, иконка 36→32px,
  metric-val 18→16px — выглядит как row в списке, а не как карточка;
- .rail__balance-val (mini-wallet в drawer): 22→18px — теперь не
  доминирует над identity-блоком пайщика.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:38:31 +00:00
coopops b7717e0e1f fix(ui): canon q-field/q-btn пропорции — убрать раздавливание floating-label
Корень регрессии: жёсткий height: 40px в .q-field--outlined раздавил
вертикальную разметку q-input (Quasar держит padding-top:24px под
floating-label слот; при сжатии текст значения сдвигается). Те же
эффекты для select.

- BaseInput/BaseSelect → dense + stack-label по умолчанию;
  Quasar сам корректно держит 40px в dense, метка статически стоит
  над инпутом без floating-механики;
- .q-field--outlined: убран принудительный height; .q-field--dense
  получает только padding-x, высоту контролирует Quasar;
- .q-btn: font-weight 500→450, min-height 40→36px, padding 16→14px —
  кнопки больше не выглядят «жирно/тяжело»;
- .q-btn--outline: hairline-граница через ::before (Quasar pattern),
  без двойной рамки;
- размеры sm/lg/dense пропорционально уменьшены;
- .q-chip: высота 24→26px, padding 10→12px, font-weight 500→450;
  + .q-chip__icon размер 14px (для точечных чипов).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:36:29 +00:00
coopops 1ac70c0405 refactor(desktop/base): остальные base/ и AuthCard → обёртки Quasar
BaseSelect → q-select (outlined, reserve-hint-space, map-options/emit-value
для совместимости с {value,label} options API).

BaseCard → q-card flat с q-card-section head/body. Variants flat/inset/quiet
override-ят через scoped style.

BaseChip → q-chip с color mapping (variant → primary/positive/negative/
warning/info). Square для canon-look.

BaseBadge → q-badge с тем же color mapping. Dot-вариант через scoped style
(8×8 круг).

BaseBanner → q-banner rounded dense с border-left по variant.

BaseDialog → q-dialog + q-card с size→maxWidth mapping. closeOnBackdrop/
closeOnEscape пробрасываются через :persistent / :no-*-dismiss.

BaseTable → q-table с трансформацией columns в QTableProps['columns'].
Hide-pagination, binary-state-sort. Cell-slots работают через body-cell-{key}.

AuthCard (domain) — был на native canon `.card` div. Переведён на q-card flat
для консистентности. Усиленный shadow + accent-stripe сверху сохранены.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:12:12 +00:00
coopops 8ddcd30bc9 refactor(desktop/base): BaseInput/Button/Form → обёртки Quasar
BaseInput теперь рендерит <q-input outlined reserve-hint-space no-error-icon>
с проброшенными slots. reserve-hint-space от Quasar резервирует место
под error/hint встроенно — раньше делали вручную через NBSP. Все Quasar
features (rules через v-bind \$attrs, mask, ref API, native slots) работают
прозрачно. Props API совместим (modelValue, label, hint, error, prefix,
suffix, mono, readonly, disabled, autocomplete, name, id, type).

BaseButton — q-btn с variant→Quasar mapping:
  primary → :unelevated color="primary" (заливка accent)
  secondary → :outline (border + surface)
  ghost → :flat (transparent)
  danger → :flat color="negative" (transparent + neg-cвет)
:no-caps :ripple=false для canon-look. Props API совместим.

BaseForm — q-form-обёртка с сводным error-баннером (q-banner) и slot footer.
banner—neg styling прилетает из quasar-canon.css через .bg-negative-soft.

Глобальная стилизация Quasar в quasar-canon.css даёт canon-look всем
этим компонентам и существующим q-input/q-btn/q-card по всему проекту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:09:34 +00:00
coopops 82f44284fa feat(desktop/css): canon-стилизация Quasar поверх (quasar-canon.css)
Архитектурный pivot: вместо native HTML5-компонентов под canon-классами
стилизуем Quasar поверх. Причины:
- Сохраняется весь Quasar API из коробки (rules/mask/dense/slots/refs)
- Существующий код (сотни q-input, q-btn, q-card, q-select) автоматом
  получает canon-look без правок
- Меньше регрессий — Quasar отвечает за accessibility/keyboard/focus

Файл src/css/mono-platform/quasar-canon.css содержит overrides под canon
для: q-field--outlined (q-input/q-select), q-btn (unelevated/outline/flat),
q-card, q-chip, q-badge, q-banner, q-dialog/q-menu, q-item, q-table,
q-checkbox/q-toggle.

Подключен в quasar.config.cjs css[] после components.css. tokens.css
без изменений — все цвета/радиусы/тени тянутся как переменные.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:08:36 +00:00
coopops 6029342c7e fix(desktop): удалить legacy глобальный .card { transform: scale(1.03) }
Истинный источник «прыгалки» AuthCard на hover: глобальное правило
`.card:hover { transform: scale(1.03) }` в src/app/styles/style.css
(и его дубль в pages/Marketplace/MainPage/ui/MainPage.vue, тоже не scoped)
бьёт по любому элементу с классом card. AuthCard имеет корневой
<div class="card auth-card"> — каждый hover дёргает её на 3%.

Поиск показал: класс `card` без модификаторов в templates никто кроме
новых canon-компонентов (BaseCard, AuthCard) не использует. Этот глобал —
мёртвый legacy, удаляем безопасно.

NBSP-фикс layout-shift из предыдущего коммита (2a2d9a786a) остаётся
актуальным — он лечил другую (на уровне поля) проблему.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:06:59 +00:00
coopops 2a2d9a786a fix(desktop/ui): добить layout-shift, ужать gap полей, accent-стрип AuthCard, затемнить canvas
1) BaseInput — layout shift жив: text-interpolation `{{ error || hint || ' ' }}`
   рендерила обычный пробел, браузер схлопывает text-node с одним whitespace
   в 0-width content → div сжимается → форма прыгает. Заменили на
   `<span>&nbsp;</span>` (NBSP в DOM, не схлопывается).

2) BaseInput — canon `.field { margin-bottom: 16px }` создавал слишком
   большой разрыв между полями (16px margin + 10px form gap + 17px message =
   ~43px). Override `.field { margin-bottom: 0 }` в scoped — расстояние
   управляется gap'ом BaseForm.

3) AuthCard — статичный accent-стрип 3px сверху (`::before`). Визуальный
   якорь без анимации — даёт бренд-маркер «здесь главное».

4) tokens.css light — canvas #fafafa → #f4f4f5 (cool-gray), canvas-2
   #f4f4f5 → #ebebed. Surface остаётся #ffffff, теперь карточка чётче
   проступает на фоне.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:54:32 +00:00
coopops d6ab6e3cc6 fix(desktop/ui): убрать layout shift в BaseInput и оживить AuthCard на светлой
BaseInput: контейнер field__message теперь рендерится всегда с min-height
calc(--p-fs-meta * 1.4) — раньше div появлялся/исчезал на каждом keystroke
при валидации (см. LostKey emailError), форма прыгала вверх-вниз.
Когда нет ни ошибки, ни хинта — рендерится NBSP, высота сохраняется.

AuthCard: добавлен box-shadow поверх canon-минималистского .card —
canvas #fafafa и surface #ffffff на светлой почти неразличимы, без тени
карточка выглядит плоской. На тёмной отдельный shadow усилен (поверх
canon `--p-shadow-card`, который оптимизирован под data-cards, не hero).
border-color подкручен с --p-line на --p-line-1 для большей чёткости.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:48:01 +00:00
coopops 5d1d79d9ea feat(desktop/LostKey): E6 Story 6.3 — миграция «Перевыпуск ключа» на canon
Scope-adjustment: plan'овая Story 6.3 предполагала миграцию SignUp как
второго auth-flow, но Register — multi-step wizard (10+ файлов в
pages/Registrator/SignUp/), который требует VerticalStepper из Эпика 10
и не помещается в Волну 1. Вместо этого мигрируем LostKey — простую
одно-полевую auth-страницу, того же auth-домена.

LostKey (widget): pug → html, старый AuthCard (q-card с shimmer) →
canon AuthCard. q-input → BaseInput с inline email-валидацией;
q-btn → BaseButton; всё обёрнуто в BaseForm с loading/error.
Бизнес-логика (useLostKey.startResetKey + router redirect на resetkey)
сохранена 1:1.

LostKeyPage: footer-кнопка «Назад» через ghost BaseButton в footer-slot
AuthCard; центровка через flexbox.

Не мигрировано в Волне 1 (известный gap):
- SignUp wizard (требует VerticalStepper из E10)
- ResetKey / Invite (на старом AuthCard, мигрируются в Волне 2)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:29:04 +00:00
coopops 34e1ea8a37 feat(desktop/SignIn): E6 Story 6.2 — миграция Login на canon-компоненты
LoginForm (feature): q-input → BaseInput, q-btn → BaseButton, обёрнут в
BaseForm с управлением loading/error. Бизнес-логика (useLoginUser, redirect,
OpenReplay tracking, NotificationPermissionDialog) сохранена 1:1.
Ошибки входа теперь показываются через сводный error BaseForm
(плюс legacy FailAlert остаётся как fallback).

SignIn (widget): импорт AuthCard переключён на новый canon-композит
(src/shared/ui/domain/AuthCard), убран pug-template и градиентные стили.
Добавлен footer-slot — пробрасывается в AuthCard.

SignInPage: переписан с pug → html, footer-кнопки «Потеряли ключ?» /
«Нет аккаунта?» вынесены в footer-slot AuthCard через BaseButton ghost-sm.
Центровка через flexbox вместо QRow grid.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:28:31 +00:00
coopops ef80504a80 feat(desktop/AuthCard): E6 Story 6.1 — AuthCard композит + AuthLayout
AuthCard (src/shared/ui/domain/AuthCard) — canon .card + center-aligned
head (title/subtitle/slot head) + body-slot + опциональный footer-slot.
Старый src/shared/ui/AuthCard (q-card с shimmer-градиентом) остаётся
для не-мигрированных страниц (SignUp wizard, ResetKey, Invite).

AuthLayout (src/app/layouts/AuthLayout.vue) — wrapper-обёртка для full-page
auth-страниц: фон var(--p-canvas), центрирование, тогл темы в правом верхнем.

Витрина: dev-секция 19 с примером login-формы (BaseForm + BaseInput ×2 +
BaseButton + footer-ссылки).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:27:11 +00:00
coopops 51f89b3fe4 feat(desktop/widgets/wallet-card-mini): E5 Story 5.2 — connected-обёртка WalletCard
WalletCardMini подключает WalletCard к Pinia (useWalletStore.program_wallets) +
System store (info.symbols.root_govern_symbol) + useRouter (клик →
name: 'wallet'). Программный mapping: wallet→MAIN, blagorost→BLAGOROST,
generator→GENERATOR (Zeus.ProgramType). Loading определяется автоматически
по наличию данных в store (или пробрасывается через prop).

Подключено в слот wallet AppHeader на dev-витрине (секция 16 — каркас) +
выделенная секция 18 с тремя программами. На dev-странице покажет
skeleton-loading т.к. loadUserWallet не вызывался; на реальных страницах
после init процесса — настоящий баланс.

Со-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:00:57 +00:00
coopops 50b95de57b fix(desktop/_dev/ui): корректные подписи программ Благорост/Генератор в витрине WalletCard
Благорост — капитализация РИД (не «накопления на жильё»), Генератор —
генерация РИД (не «доходы от программы»). Кошелёк остаётся «свободный
остаток». Черновые ярлыки риском попадают в production через копипасту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 16:48:01 +00:00
coopops 345b226f84 feat(desktop/WalletCard): E5 Story 5.1 — props-only компонент с программными акцентами
Канонический .wallet с компактным (--row) и full-вариантами, программные
акценты через --prog-blagorost / --prog-wallet / --prog-generator (UX-DR20).
Витрина на /_dev/ui секция 17: три программы × оба варианта + loading +
empty + locked-line.

Compact (.wallet--row): 36px icon, 18px sum, 14px padding — для слота шапки.
Full (.wallet): 44px icon, 22px sum, 18px padding — для дашборда.

Connected-обёртка widgets/wallet-card-mini (Story 5.2) делается отдельным
коммитом, требует интеграции с реальным wallet-store и MARKETPLACE_ASSET_CONFIG.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 16:41:54 +00:00
coopops 8af8bf7512 revert(desktop/RailUserCard): убрать transition свёртки — baseline-skipping
Анимация max-height + opacity при свёртке давала видимый прыжок крупной
суммы баланса (`font-size: 22px` + `display: flex; align-items: baseline`):
во время промежуточных кадров baseline переcчитывался относительно
схлопывающегося контейнера → текст «прыгал в середину» и обратно.

Возвращаемся к canon `display: none`. Resize моментальный, без skipping.
Поведение rail (top опускается, bottom-chevron остаётся через flex-spacer)
работает и без анимации — пользователь это и подтвердил в фидбеке.

Chevron rotation через :deep(.q-icon) и balance-route не трогаем —
они независимы.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:33:04 +00:00
coopops 5ef499a75f feat(desktop/_dev/ui): TokenPalette + chevron full-width fix
1. **Палитра системы.** Новая секция «00» в шапке dev-страницы со всеми
   основными --p-* токенами:
   - Акцент (accent / hover / press)
   - Поверхности (canvas / canvas-2 / surface / 2 / 3)
   - Текст (ink / ink-on-accent)
   - Линии (line / 1 / 2) — текстовый input для rgba
   - Статусы (pos / neg / warn / info)
   - Программные тинты (blagorost / wallet / generator)

   Color-picker для hex-токенов, текстовый ввод для rgba. Live-override
   через `document.documentElement.style.setProperty()` — inline-style на
   :root перебивает CSS-правила, перекрашивание мгновенное. Подсветка
   переопределённых токенов + счётчик в шапке + кнопка «Сбросить» (через
   removeProperty). Watch на Quasar Dark — при смене темы пикеры
   обновляются на новые resolved-значения.

2. **Chevron в свёрнутом виде — full-width.** Из-за того что для плавной
   анимации primary остаётся в DOM (max-height:0 вместо display:none),
   у него сохранялся `flex: 1` и он съедал ширину карточки → chevron не
   мог растянуться через canon-правило `width: 100%`. В `.is-collapsed`
   добавили `flex: 0 0 0` + `width: 0` для primary — chevron теперь
   корректно занимает всю ширину.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:31:54 +00:00
coopops 79afee2b82 fix(desktop/RailUserCard): chevron rotation, плавная свёртка, balance-route
По фидбеку:

1. **Chevron не переворачивается.** Canon-правило
   `.rail__usercard__collapse svg { transform: rotate(180deg) }` рассчитано
   на `<svg>`, а Quasar `<q-icon>` рендерит `<i class="q-icon">`. Прокидываем
   transition + rotate через scoped `:deep(.q-icon)` — chevron теперь крутится
   при свёртке/развёртке.

2. **«Карточка улетает»** — резкий jump при `display:none` создаёт визуальный
   прыжок. Подменяем display:none на `max-height: 0` + `opacity: 0` с
   transition'ом. Логика canon-селектора `.is-collapsed` не нарушается,
   просто добавлена плавность. Если структурное поведение rail (top
   опускается, bottom-chevron остаётся на месте) всё равно неудобно —
   перевёрстаем во вторую итерацию.

3. **Клик по балансу → маршрут кошелька.** Новый prop `balanceRoute` —
   если задан, блок `.rail__balance` оборачивается в `<router-link :to>`.
   Параллельно эмитим `balance-click` — для аналитики или функционального
   handler'а. На витрине привязал к `/_dev/ui#wallet` чтобы проверить.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:28:31 +00:00
coopops 0a977dcd83 feat(desktop/domain): RailUserCard + фикс RouteMeta для dev-роута
1. `RouteMeta.icon` объявлен обязательным в `src/env.d.ts` — добавили
   `icon: 'fa-solid fa-flask'` для dev-роута, иначе vue-tsc валится.

2. `RailUserCard` (`shared/ui/domain/RailUserCard`) — доменный
   общеплатформенный компонент мини-кошелька в нижней части drawer'а.
   Это будущий E11 (Волна 2), но логично вытащить раньше, чтобы основной
   layout можно было заменить целиком уже сейчас.

   Реализован по canon строго: rail__usercard с usertop+balance+actions,
   collapse-chevron справа с поворотом arrow при свёртке (через
   `.is-collapsed` модификатор + canon CSS), отдельный `rail__signout`
   блок (выходит вне `.rail__usercard` — рендерится сразу после, в том
   же `aside.rail` через footer-slot).

   Контракт строго dumb по архитектуре: только props/emits/slots.
   - props: name, role, avatarSrc, balance, symbol, balanceLabel,
     lockedBalance, lockedLabel, primaryActionLabel, collapsed,
     showSignout, signoutLabel
   - emits: primary-action, update:collapsed (v-model), signout
   - slots: usertop-extra, actions

   Coнnected-обёртки (с инжектом store/auth) — на уровне страницы,
   не здесь.

3. Витрина `/_dev/ui`:
   - Секция 12 (AppDrawer): заменили временный огрызок rail-demo__user
     на полноценный `<RailUserCard show-signout>` через footer-slot.
   - Секция 16 (Каркас приложения): аналогично — теперь у rail полноценный
     footer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:19:09 +00:00
coopops 74539a6c46 feat(desktop/layout): E4 — AppDrawer + AppHeader + PageHead + PageTabs
E4 «Layout-каркас» Волны 1: 4 структурные обёртки в `src/shared/ui/layout/`.
По архитектуре — только props/emits/slots, без store/router/api, без знания
о данных. Cтраницы передают menu-items, breadcrumbs, табы и активные ключи
сверху.

Состав:
- `AppDrawer` — canon `.rail`: solid surface, активный пункт soft-accent
  пилюлей + 2-px рейл слева. Поддерживает плоский список + секции
  (RailSection с eyebrow-меткой), opt-in ⌘K-кнопку, slot `footer` для
  user-card / logout.
- `AppHeader` — canon `.topbar`: бургер · crumb · `topbar__actions` ·
  `topbar__right`. Поддерживает простой title и multi-level breadcrumb.
  Глобальные действия через слоты `notifications`/`theme`/`wallet`/`profile`
  или общий `right`.
- `PageHead` — canon `.page-head`: eyebrow + title + subtitle + slot
  `actions`. Используется когда страница БЕЗ подстраниц.
- `PageTabs` — canon `.tabbar` + `.tab`: вкладки подстраниц с tab__count,
  опциональный hairline-разделитель и `tabbar__actions` справа.

Также в составе коммита:
- Убрали AuthCard из base — он композит (cap + body + head + footer),
  переедет в E6 Auth flow рядом с SignUp/ResetKey/Invite.
- BaseForm: gap полей 16→10px (тесные формы как в каноне).
- Витрина `/_dev/ui`: 4 новые секции (12-15) на каждый layout-компонент +
  секция 16 «Каркас приложения» — AppDrawer + AppHeader + PageTabs в живой
  связке без legacy QLayout/QDrawer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 06:17:01 +00:00
coopops dc93704990 fix(desktop/mono-platform): убрать HTML-теги <style> из canon CSS
Скрипт извлечения из bundled HTML оставил `  <style>...` в начале и
`</style>` в конце обоих файлов. CSS-парсер ловил `<` как ошибочный
селектор и пропускал блок до ближайшей `}` — съедая весь первый блок
правил.

В tokens.css первым блоком был `:root, [data-theme="light"]` со светлой
палитрой — она целиком игнорировалась. `[data-theme="dark"]` ниже
парсился нормально, поэтому тёмная тема работала, а светлая — нет:
кнопки primary/ghost/danger оставались с transparent background и
сливались с canvas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 05:54:25 +00:00
coopops 02a4a2aa14 feat(desktop): MONO Platform v2 — базовые компоненты дизайн-системы
Заносим эталонную дизайн-систему в проект — только базовые элементы для
дальнейшей компоновки. Source — MONO Design System.html.

Архитектура (по `_bmad-output/planning-artifacts/architecture.md`):
- `src/css/mono-platform/{tokens.css,components.css}` — canon токены и стили
  компонентов 1:1 из эталона; импортируются первыми в quasar.config css[].
- `src/shared/ui/base/<Имя>/{Vue,types,index}` — 14 базовых обёрток (Vue 3 +
  TS, native HTML над canon BEM, no Quasar-deps кроме Dark API в ThemeToggle).
  FSD-граница: только props/emits/slots, никаких store/router/api.
- `src/boot/ui.ts` — глобальная регистрация 14 базовых компонентов.
- `src/boot/theme.ts` — sync `html[data-theme]` ↔ Quasar Dark.
- Inter + JetBrains Mono в index.html.

14 базовых:
BaseButton, BaseInput, BaseSelect, BaseCard, BaseTable, BaseChip, BaseBadge,
BaseDialog, BaseBanner, BaseForm, EmptyState, Avatar, ThemeToggle, AuthCard.

Витрина `/_dev/ui` (dev-only) — каждая обёртка отдельной секцией над чистым
canon-канвасом, без layout-обёрток. Маршрут смонтирован на корневом уровне,
ВНЕ DynamicLayoutWrapper, чтобы legacy default.vue не перебивал стили.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 05:30:28 +00:00
493 changed files with 24162 additions and 10669 deletions
+104 -160
View File
@@ -5,8 +5,12 @@ name: Release
# Порядок (через jobs.needs):
# 1) release — контракты + контейнеры + webhook деплоя (атомарно)
# 2) publish-packages — npm publish через lerna (если не -alpha)
# 3) publish-docs — mkdocs + standards-site → gh-pages (если не -alpha + ветка main)
# 4) trigger-coopenomics — repository_dispatch в coopenomics/coopenomics (тот же гейт, что docs)
# 3) trigger-coopenomics — workflow_dispatch сборки сайта C9S/coopenomics на
# Gitea (если не -alpha + ветка main); он сам тянет
# mono и пересобирает доки.
#
# publish-docs (gh-pages на github.com) удалён 2026-05-25 — после переезда на
# Gitea Pages-публикация невалидна; доки деплоятся через C9S/coopenomics.
#
# Зачем последовательно: пакеты/доки/внешний триггер не должны уезжать,
# если релиз контрактов или контейнеров провалился. Раньше четыре workflow'а
@@ -34,16 +38,7 @@ permissions:
contents: write
jobs:
# Гейт TS-типов перед сборкой образов. Reusable workflow `typecheck.yaml`
# запускает vue-tsc на desktop и tsc --noEmit на controller. Нужно потому,
# что `lerna run build` в корневом Dockerfile НЕ ловит TS-ошибки:
# quasar build идёт через esbuild с выключенным vueTsc, а у controller'а
# build-скрипта вообще нет (ts-node в рантайме).
typecheck:
uses: ./.github/workflows/typecheck.yaml
release:
needs: typecheck
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.resolve.outputs.tag_name }}
@@ -127,14 +122,41 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
# === Этап 1: контракты ===
# CDT ставим из .deb (C9S/cdt v4.2.0) прямо в окружение job'а и
# компилируем напрямую — БЕЗ вложенного docker. Почему не build-all.sh:
# тот монтирует $(pwd):/project в sibling-контейнер, а под Gitea
# act_runner сам job исполняется в контейнере → хостовый демон не видит
# этот путь, /project пуст, cmake падает "no CMakeLists.txt".
# build-all.sh остаётся для локальной сборки (оборачивает тот же
# build_contracts_cdt.sh в docker).
# CMakeLists хардкодит toolchain /cdt/build/...; .deb кладёт CDT в
# /usr/opt/cdt/4.2.0, а сам CDTWasmToolchain.cmake указывает на /usr
# абсолютно — поэтому симлинк /cdt/build → /usr/opt/cdt/4.2.0 сводит пути
# без правки CMakeLists.
- name: Pull CDT toolchain image
run: docker pull dicoop/blockchain_v5.1.1:dev
- name: Install CDT 4.2.0 toolchain (.deb)
run: |
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
$SUDO apt-get update
# build-essential — для host-компилятора (project() в CMakeLists);
# libz3-4/libtinfo6/libxml2/zlib1g — рантайм бинарей CDT (clang-9,
# ld.lld и т.д.). Пакет cdt объявляет только libcurl4-gnutls-dev,
# поэтому остальные .so ставим явно — в образе dicoop/blockchain они
# были из сборки исходников, из .deb не тянутся.
$SUDO apt-get install -y --no-install-recommends \
curl ca-certificates cmake make build-essential \
libz3-4 libtinfo6 libxml2 zlib1g
curl -fsSL -o /tmp/cdt.deb \
https://git.coopenomics.world/C9S/cdt/releases/download/v4.2.0/cdt_4.2.0-1_amd64.deb
$SUDO apt-get install -y /tmp/cdt.deb
$SUDO mkdir -p /cdt
$SUDO ln -sfn /usr/opt/cdt/4.2.0 /cdt/build
cdt-cpp --version || true
- name: Compile contracts
working-directory: components/contracts
run: |
./build-all.sh "$BUILD_MODE"
./build_contracts_cdt.sh "$BUILD_MODE"
echo "--- build/contracts ---"
ls -la build/contracts/
@@ -215,6 +237,17 @@ jobs:
- name: Build and push contracts image
working-directory: components/contracts
run: |
# Ретрай push'а: резолвинг registry-1.docker.io на runner'е изредка
# моргает (DNS-таймаут к 127.0.0.53) — 3 попытки с паузой.
dpush() {
local ref="$1" n=1
until docker push "$ref"; do
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
n=$((n+1)); sleep 10
done
}
IMAGE="dicoop/contracts"
SHORT_SHA="${SHA::7}"
docker build \
@@ -223,14 +256,14 @@ jobs:
--label "build.mode=$BUILD_MODE" \
-t "$IMAGE:$CONTRACTS_TAG" \
./docker/.context
docker push "$IMAGE:$CONTRACTS_TAG"
dpush "$IMAGE:$CONTRACTS_TAG"
docker tag "$IMAGE:$CONTRACTS_TAG" "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
docker push "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
dpush "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
if [ -n "$CONTRACTS_EXTRA" ]; then
docker tag "$IMAGE:$CONTRACTS_TAG" "$IMAGE:$CONTRACTS_EXTRA"
docker push "$IMAGE:$CONTRACTS_EXTRA"
dpush "$IMAGE:$CONTRACTS_EXTRA"
fi
- name: Verify pushed contracts image
@@ -243,15 +276,31 @@ jobs:
- name: Build and push base image
run: |
dpush() {
local ref="$1" n=1
until docker push "$ref"; do
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
n=$((n+1)); sleep 10
done
}
docker build --target runtime -t "dicoop/mono-base:$TAG_NAME" .
docker push "dicoop/mono-base:$TAG_NAME"
dpush "dicoop/mono-base:$TAG_NAME"
if [ "$IS_PROD" = "true" ]; then
docker tag "dicoop/mono-base:$TAG_NAME" dicoop/mono-base:latest
docker push dicoop/mono-base:latest
dpush dicoop/mono-base:latest
fi
- name: Build and push service images
run: |
dpush() {
local ref="$1" n=1
until docker push "$ref"; do
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
n=$((n+1)); sleep 10
done
}
build_service() {
local SVC="$1" PKG="$2" CMD="$3"
local DOCKERFILE="Dockerfile.$SVC"
@@ -260,10 +309,10 @@ jobs:
echo "CMD [\"pnpm\", \"-F\", \"$PKG\", \"run\", \"$CMD\"]"
} > "$DOCKERFILE"
docker build -t "dicoop/$SVC:$TAG_NAME" -f "$DOCKERFILE" .
docker push "dicoop/$SVC:$TAG_NAME"
dpush "dicoop/$SVC:$TAG_NAME"
if [ "$IS_PROD" = "true" ]; then
docker tag "dicoop/$SVC:$TAG_NAME" "dicoop/$SVC:latest"
docker push "dicoop/$SVC:latest"
dpush "dicoop/$SVC:latest"
fi
}
@@ -314,149 +363,44 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# ============================================================================
# publish-docs — mkdocs + standards-site → gh-pages (бывший publish-docs.yaml).
# trigger-coopenomics-docs — пересборка сайта C9S/coopenomics.
# Гейт: не-alpha + ветка main.
# ВНИМАНИЕ: gh-pages step пушит на github.com/coopenomics/mono — это GitHub
# Pages. При переезде на Gitea Actions нужно будет либо подложить PAT для
# внешнего push'а в GitHub, либо переключить публикацию на Gitea Pages.
# ============================================================================
publish-docs:
needs: release
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Версия pnpm берётся из `packageManager` корневого package.json,
# синхронно с publish-packages job'ом и Dockerfile'ами.
- name: Set up pnpm
uses: pnpm/action-setup@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Python requirements
run: |
python -m venv venv
source venv/bin/activate
pip install mkdocs-material mkdocs-macros-plugin mkdocs-section-index pymdown-extensions
working-directory: ./components/docs
- name: Install Node.js dependencies
run: pnpm install
working-directory: ./components/docs
- name: Patch spectaql-config.yml for CI
run: |
sed -i.bak "0,/url:.*/s|url:.*|url: 'https://testnet.coopenomics.world/backend/v1/graphql'|" spectaql-config.yml
working-directory: ./components/controller
- name: Show patched spectaql-config.yml
run: cat spectaql-config.yml
working-directory: ./components/controller
- name: Build cooptypes
run: pnpm run build
working-directory: ./components/cooptypes
- name: Generate controller docs
run: pnpm run docs
working-directory: ./components/controller
- name: Copy controller docs
run: |
mkdir -p ./components/docs/docs/graphql
cp -r ./components/controller/docs/* ./components/docs/docs/graphql/
- name: Generate sdk docs
run: pnpm run docs
working-directory: ./components/sdk
- name: Copy sdk docs
run: |
mkdir -p ./components/docs/docs/sdk
cp -r ./components/sdk/docs/* ./components/docs/docs/sdk/
- name: Generate cooptypes docs
run: pnpm run docs
working-directory: ./components/cooptypes
- name: Copy cooptypes docs
run: |
mkdir -p ./components/docs/docs/cooptypes
cp -r ./components/cooptypes/docs/* ./components/docs/docs/cooptypes/
# ── standards-site → /standards/ на docs.цифровой-кооператив.рф ──
# Отдельный Vue-сайт с BPMN-графом кооперативных стандартов,
# публикуется как поддиректория на том же домене. Vite собирает
# с base='/standards/' (см. vite.config.ts), относительные пути
# внутри dist/ и hash-router работают корректно.
# dist кладём в docs/standards/ ДО mkdocs build — так же, как
# graphql/sdk/cooptypes; mkdocs сам включит его в итоговый site/.
- name: Build standards-site
run: pnpm run build
working-directory: ./components/contracts/standards-site
- name: Copy standards-site into docs/standards
run: |
mkdir -p ./components/docs/docs/standards
cp -r ./components/contracts/standards-site/dist/* ./components/docs/docs/standards/
- name: Build docs (mkdocs)
run: |
source venv/bin/activate
mkdocs build
working-directory: ./components/docs
- name: Remove specific large file before publishing
run: |
rm -f ./components/docs/site/sdk/typedoc.json
if [ -f "./components/docs/site/sdk/typedoc.json" ]; then
echo "ERROR: typedoc.json still exists!"
exit 1
else
echo "SUCCESS: typedoc.json removed successfully"
fi
- name: Publish to GitHub Pages
run: npx gh-pages --nojekyll -d site --repo https://x-access-token:${GITHUB_TOKEN}@github.com/coopenomics/mono.git
working-directory: ./components/docs
env:
GIT_AUTHOR_NAME: github-actions
GIT_AUTHOR_EMAIL: github-actions@github.com
GIT_COMMITTER_NAME: github-actions
GIT_COMMITTER_EMAIL: github-actions@github.com
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger docs deployment webhook
if: ${{ success() }}
run: |
curl -X POST "${{ vars.DOCS_DEPLOY_WEBHOOK_URL }}" \
-H 'Content-Type: application/json' \
-d '{"ref":"${{ github.ref }}","sha":"${{ github.sha }}","branch":"${{ github.ref_name }}"}'
# ============================================================================
# trigger-coopenomics-docs — repository_dispatch в coopenomics/coopenomics
# (бывший build-contracts-docs.yaml). Гейт идентичен publish-docs.
#
# coopenomics переехал на Gitea (C9S/coopenomics); его publish-docs.yaml
# клонирует mono и пересобирает сайт. Gitea НЕ имеет API для
# repository_dispatch — только workflow_dispatch, поэтому дёргаем целевой
# workflow через Gitea API с PAT (secret DOCS_DISPATCH_TOKEN — префикс GITEA_
# у секретов зарезервирован Gitea, нельзя; права write на C9S/coopenomics).
# Целевой workflow слушает workflow_dispatch с входами
# mono_sha/mono_ref.
# ============================================================================
trigger-coopenomics-docs:
needs: release
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
runs-on: ubuntu-latest
steps:
- name: Trigger Coopenomics deployment
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.COOPENOMICS_PAT }}
repository: coopenomics/coopenomics
event-type: deploy_from_mono
client-payload: '{"repository": "${{ github.repository }}", "sha": "${{ github.sha }}", "ref": "${{ github.ref }}", "actor": "${{ github.actor }}"}'
- name: Trigger coopenomics website rebuild (Gitea workflow_dispatch)
run: |
curl -fsSL -X POST \
-H "Authorization: token ${{ secrets.DOCS_DISPATCH_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"ref":"master","inputs":{"mono_sha":"${{ github.sha }}","mono_ref":"${{ github.ref }}"}}' \
"${{ github.server_url }}/api/v1/repos/C9S/coopenomics/actions/workflows/publish-docs.yaml/dispatches"
# ============================================================================
# trigger-mono-docs — деплой ВТОРОЙ документации (сайт доков mono) через
# webhook DOCS_DEPLOY_WEBHOOK_URL. Отдельная от coopenomics публикация —
# обе доки уезжают синхронно по релизу. Извлечён из бывшего publish-docs:
# gh-pages на github.com выпилен как мёртвый, остался реальный deploy-webhook
# (приёмник деплоит доки на своей стороне). Гейт: не-alpha + ветка main.
# ============================================================================
trigger-mono-docs:
needs: release
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
runs-on: ubuntu-latest
steps:
- name: Trigger docs deployment webhook
run: |
curl -fsSL -X POST "${{ vars.DOCS_DEPLOY_WEBHOOK_URL }}" \
-H 'Content-Type: application/json' \
-d '{"ref":"${{ github.ref }}","sha":"${{ github.sha }}","branch":"${{ github.ref_name }}"}'
+4 -4
View File
@@ -13,11 +13,11 @@ name: Typecheck
#
# Триггеры:
# - pull_request на dev — гейт перед мерджем в основную ветку;
# - workflow_call — переиспользуется из release.yaml как `needs:` у release-job,
# чтобы битый тэг не уехал в DockerHub.
# - workflow_call — оставлен для переиспользования, но release.yaml его
# больше НЕ вызывает (гейт из релиза убран как избыточный — типы
# проверяются на PR в dev до того, как код доедет до тэга).
#
# Push в dev/testnet/main НЕ триггерит — намеренно (PR-гейт достаточен,
# а тэги покрывает workflow_call из release.yaml).
# Push в dev/testnet/main НЕ триггерит — намеренно (PR-гейт на dev достаточен).
on:
pull_request:
+1
View File
@@ -1,4 +1,5 @@
node_modules/
node_modules
lerna-debug.log
components/controller/graph.png
blockchain-data/
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/blago-cli",
"version": "2026.5.23-4",
"version": "2026.5.25-3",
"description": "CLI синхронизации артефактов Благорост с бэкендом через @coopenomics/sdk",
"type": "module",
"private": true,
@@ -9,12 +9,15 @@ export interface CommunicationCursorsFile {
messageLastTsByRoom: Record<string, number>
/** project_hash → ISO instant: транскрипции с endedAt ≤ этого момента уже выгружены */
transcriptionLastEndedExclusiveByProject: Record<string, string>
/** matrixRoomId → ISO instant: транскрипции непроектной комнаты с endedAt ≤ этого момента уже выгружены */
transcriptionLastEndedExclusiveByRoom: Record<string, string>
}
function empty(): CommunicationCursorsFile {
return {
messageLastTsByRoom: {},
transcriptionLastEndedExclusiveByProject: {},
transcriptionLastEndedExclusiveByRoom: {},
}
}
@@ -32,6 +35,11 @@ export async function loadCommunicationCursors(root: string): Promise<Communicat
&& typeof parsed.transcriptionLastEndedExclusiveByProject === 'object'
? { ...parsed.transcriptionLastEndedExclusiveByProject }
: {},
transcriptionLastEndedExclusiveByRoom:
parsed.transcriptionLastEndedExclusiveByRoom !== undefined
&& typeof parsed.transcriptionLastEndedExclusiveByRoom === 'object'
? { ...parsed.transcriptionLastEndedExclusiveByRoom }
: {},
}
}
catch {
@@ -25,7 +25,7 @@ import {
transcriptionMeetingFileStemUtc,
type CommunicationDayLine,
} from './communication-markdown.js'
import { workspaceBasePath, type ProjectPathModel } from './layout.js'
import { generateSlug, workspaceBasePath, type ProjectPathModel } from './layout.js'
import { syncEntityFile } from './sync-entity-file.js'
interface ProjectRowLite {
@@ -183,6 +183,19 @@ async function listRooms(ctx: AuthenticatedContext, projectHash: string) {
return q[Queries.ChatCoop.ListProjectCommunicationRooms.name] ?? []
}
/** Стабильная папка непроектной комнаты в `rooms/`. Системные — фиксированные, комнаты секретаря — slug + хвост id (уникальность). */
function nonProjectRoomFolder(kind: string, matrixRoomId: string, displayLabel: string): string {
if (kind === 'MEMBERS') {
return 'komnata-paishchikov'
}
if (kind === 'COUNCIL') {
return 'komnata-soveta'
}
const slug = generateSlug(displayLabel) || 'komnata'
const shortId = createHash('sha256').update(matrixRoomId, 'utf8').digest('hex').slice(0, 6)
return `${slug}-${shortId}`
}
export async function pullProjectCommunicationArtifacts(
ctx: AuthenticatedContext,
index: IndexFile,
@@ -422,3 +435,208 @@ export async function pullProjectCommunicationArtifacts(
warn(`Не удалось сохранить курсоры переписки: ${formatThrownValue(e)}`)
}
}
/**
* Pull переписки и транскрипций из комнат ВНЕ проектов Capital (пайщики, совет, комнаты секретаря).
* Раскладка — отдельная верхняя папка `rooms/<folder>/{messages,meetings}/`, чтобы не смешивать с
* проектными `meetings/`. Логика идентична проектной, но bucket = одна комната, курсор транскрипций — по matrixRoomId.
*/
export async function pullNonProjectCommunicationArtifacts(
ctx: AuthenticatedContext,
index: IndexFile,
): Promise<void> {
let rooms: { matrixRoomId: string, displayLabel: string, kind: string }[]
try {
const q = await ctx.client.Query(Queries.ChatCoop.ListNonProjectCommunicationRooms.query, {})
rooms = (q[Queries.ChatCoop.ListNonProjectCommunicationRooms.name] ?? []) as typeof rooms
}
catch (e) {
warn(`Список непроектных комнат (chatcoopListNonProjectCommunicationRooms): ${formatThrownValue(e)}`)
return
}
if (rooms.length === 0) {
return
}
let cursors: CommunicationCursorsFile
try {
cursors = await loadCommunicationCursors(ctx.root)
}
catch (e) {
warn(`Курсоры переписки (комнаты): не удалось прочитать, начинаем с пустых: ${formatThrownValue(e)}`)
cursors = {
messageLastTsByRoom: {},
transcriptionLastEndedExclusiveByProject: {},
transcriptionLastEndedExclusiveByRoom: {},
}
}
for (const room of rooms) {
const folder = nonProjectRoomFolder(room.kind, room.matrixRoomId, room.displayLabel)
const basePath = `rooms/${folder}`
const roomTitle = room.displayLabel || room.matrixRoomId
// Сообщения комнаты — по календарным суткам UTC новее курсора.
try {
const last = cursors.messageLastTsByRoom[room.matrixRoomId]
const afterTs = last ?? 0
const datesQ = await ctx.client.Query(Queries.ChatCoop.ListUtcDatesWithNewRoomMessages.query, {
variables: { data: { matrixRoomId: room.matrixRoomId, afterOriginServerTsExclusive: afterTs } },
})
const dates = (datesQ[Queries.ChatCoop.ListUtcDatesWithNewRoomMessages.name] ?? []).sort()
for (const utcDate of dates) {
const mq = await ctx.client.Query(Queries.ChatCoop.GetRoomMessagesForUtcDate.query, {
variables: { data: { matrixRoomId: room.matrixRoomId, utcDate } },
})
const linesRaw = mq[Queries.ChatCoop.GetRoomMessagesForUtcDate.name] ?? []
const lines: CommunicationDayLine[] = linesRaw.map(m => ({
originServerTs: m.originServerTs,
authorLabel: m.authorLabel,
coopUsername: m.coopUsername,
kind: String(m.kind),
bodyText: m.bodyText,
}))
if (lines.length === 0) {
continue
}
const content = projectCommunicationDayToMarkdown(roomTitle, room.matrixRoomId, utcDate, [
{ displayLabel: room.displayLabel, matrixRoomId: room.matrixRoomId, lines },
])
const rel = `${basePath}/messages/${utcDate}.md`
const entityHash = messageDayEntityHash(room.matrixRoomId, utcDate)
await syncEntityFile({
root: ctx.root,
index,
entityType: 'room_message_day',
entityHash,
relativePath: rel,
content,
remoteUpdatedAt: `${utcDate}T23:59:59.999Z`,
label: `переписка ${utcDate} (${room.matrixRoomId})`,
})
}
const maxQ = await ctx.client.Query(Queries.ChatCoop.GetMaxOriginServerTsForRoom.query, {
variables: { data: { matrixRoomId: room.matrixRoomId } },
})
const maxTs = maxQ[Queries.ChatCoop.GetMaxOriginServerTsForRoom.name] as number | null | undefined
if (maxTs !== undefined && maxTs !== null && Number.isFinite(maxTs)) {
cursors.messageLastTsByRoom[room.matrixRoomId] = maxTs
}
}
catch (e) {
warn(`Переписка Matrix, комната ${room.matrixRoomId} (${roomTitle}): ${formatThrownValue(e)}`)
}
// Транскрипции звонков комнаты + sibling memo.
try {
const tExIso = cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId]
const lowerBoundExclusive = tExIso === undefined ? new Date(0) : new Date(tExIso)
interface TranscriptionCandidate {
id: string
endedAt: Date
memo: string
updatedAt: Date | undefined
}
const byId = new Map<string, TranscriptionCandidate>()
const tq = await ctx.client.Query(Queries.ChatCoop.GetTranscriptions.query, {
variables: { data: { matrixRoomId: room.matrixRoomId, limit: CHATCOOP_TRANSCRIPTIONS_QUERY_LIMIT, offset: 0 } },
})
const list = tq[Queries.ChatCoop.GetTranscriptions.name] ?? []
for (const t of list) {
const end = dateFromUnknown(t.endedAt)
if (t.status !== Zeus.TranscriptionStatus.COMPLETED || !end) {
continue
}
const prev = byId.get(t.id)
if (!prev || end > prev.endedAt) {
byId.set(t.id, {
id: t.id,
endedAt: end,
memo: typeof t.memo === 'string' ? t.memo : '',
updatedAt: dateFromUnknown(t.updatedAt),
})
}
}
const allCompleted = [...byId.values()].sort((a, b) => a.endedAt.getTime() - b.endedAt.getTime())
const newMeetings = allCompleted.filter(c => c.endedAt.getTime() > lowerBoundExclusive.getTime())
let maxEnded: Date | null = null
for (const c of newMeetings) {
const packQ = await ctx.client.Query(Queries.ChatCoop.GetTranscription.query, {
variables: { data: { id: c.id } },
})
const pack = packQ[Queries.ChatCoop.GetTranscription.name]
if (!pack?.transcription || pack.transcription.status !== Zeus.TranscriptionStatus.COMPLETED) {
continue
}
const tr = pack.transcription
const startedAt: Date | string = dateFromUnknown(tr.startedAt) ?? (tr.startedAt as Date | string)
const endedAtTr: Date | string | null | undefined
= dateFromUnknown(tr.endedAt) ?? (tr.endedAt as Date | string | null | undefined)
const md = renderCallTranscriptionMarkdown(
{
matrixRoomId: String(tr.matrixRoomId),
roomId: String(tr.roomId),
startedAt,
endedAt: endedAtTr,
},
pack.segments.map(s => ({
speakerName: s.speakerName,
text: s.text,
startOffset: s.startOffset,
endOffset: s.endOffset,
})),
)
const stem = transcriptionMeetingFileStemUtc(c.endedAt)
const rel = `${basePath}/meetings/${stem}.md`
const entityHash = c.id.toLowerCase()
await syncEntityFile({
root: ctx.root,
index,
entityType: 'call_transcription',
entityHash,
relativePath: rel,
content: md,
remoteUpdatedAt: toUpdatedIso(c.endedAt),
label: `транскрипция ${c.id}`,
})
if (!maxEnded || c.endedAt > maxEnded) {
maxEnded = c.endedAt
}
}
for (const c of allCompleted) {
const stem = transcriptionMeetingFileStemUtc(c.endedAt)
const memoRel = `${basePath}/meetings/${stem}.memo.md`
const memoRemoteUpdatedAt = toUpdatedIso(c.updatedAt ?? c.endedAt)
await syncTranscriptionMemoFile({
root: ctx.root,
index,
transcriptionId: c.id,
relativePath: memoRel,
serverMemo: c.memo,
remoteUpdatedAtIso: memoRemoteUpdatedAt,
})
}
if (maxEnded) {
cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId] = maxEnded.toISOString()
}
else if (tExIso === undefined) {
cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId] = new Date().toISOString()
}
}
catch (e) {
warn(`Транскрипции звонков, комната ${room.matrixRoomId}: ${formatThrownValue(e)}`)
}
}
try {
await saveCommunicationCursors(ctx.root, cursors)
}
catch (e) {
warn(`Не удалось сохранить курсоры переписки (комнаты): ${formatThrownValue(e)}`)
}
}
+3 -1
View File
@@ -28,7 +28,7 @@ import {
storyFileRelativePath,
workspaceBasePath,
} from './layout.js'
import { pullProjectCommunicationArtifacts } from './pull-communication.js'
import { pullNonProjectCommunicationArtifacts, pullProjectCommunicationArtifacts } from './pull-communication.js'
import { scaffoldBmadWorkspacesAfterPull } from './scaffold-bmad-workspace.js'
import { syncEntityFile } from './sync-entity-file.js'
import { writeWorkspaceIndexMarkdown } from './workspace-index.js'
@@ -329,6 +329,8 @@ export async function runPull(ctx: AuthenticatedContext, options: RunPullOptions
await pullProjectCommunicationArtifacts(ctx, index, allProjects, projectByHash)
await pullNonProjectCommunicationArtifacts(ctx, index)
await scaffoldBmadWorkspacesAfterPull(ctx, allProjects, projectByHash)
await saveIndex(ctx.root, index)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/boot",
"type": "module",
"version": "2026.5.23-4",
"version": "2026.5.25-3",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "CLI-утилита инициализации блокчейна и кооператива",
+16 -10
View File
@@ -1,6 +1,7 @@
#!/bin/bash
# Загружаем per-instance конфиг из корня репо.
# Загружаем per-instance конфиг из корня репо (для CHAIN_URL/MONGODB_URL/API_URL,
# которые читают TS-код boot и шелл-скрипты networks.sh/preactivate.sh).
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
@@ -11,24 +12,29 @@ fi
# Останавливаем и удаляем контейнеры вместе с volumes
echo "Останавливаем и удаляем контейнеры с volumes..."
docker compose down -v mongo postgres cooparser || true
docker compose down -v mongo postgres monoredis cooparser coopback || true
# Останавливаем blockchain контейнер перед удалением данных
echo "Останавливаем blockchain контейнер..."
docker compose stop node || true
# Удаляем blockchain data
# Удаляем blockchain data.
# Контейнерный wipe (alpine под root) стирает данные независимо от их владельца —
# без sudo на любой ноде (на Pi нет passwordless sudo; на проде nodeos пишет
# данные под root). Единый способ с reboot.sh / extra_reboot.sh.
echo "Удаляем blockchain data..."
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
sudo rm -rf ../blockchain-data/
docker run --rm -v "$(cd .. && pwd)/blockchain-data:/d" alpine sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true'
# Пересоздаем и запускаем базы данных
# Пересоздаем и запускаем базы данных + Redis (monoredis).
# monoredis ОБЯЗАТЕЛЕН: без него coopback падает на старте с
# `getaddrinfo EAI_AGAIN monoredis` → MaxRetriesPerRequestError → nodemon crash.
echo "Пересоздаем и запускаем базы данных..."
docker compose up -d mongo postgres
docker compose up -d mongo postgres monoredis
# Ждем готовности MongoDB
# Ждем готовности MongoDB (standalone, ping вместо ожидания PRIMARY).
echo "Ждем готовности MongoDB..."
until docker compose exec -T mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
until docker compose exec -T mongo mongosh --quiet --eval "db.adminCommand({ping:1}).ok" > /dev/null 2>&1; do
echo "MongoDB еще не готов, ждем..."
sleep 2
done
@@ -42,7 +48,7 @@ until docker compose exec -T postgres pg_isready -U postgres -d voskhod > /dev/n
done
echo "PostgreSQL готов!"
# Запускаем boot процесс
# Запускаем boot процесс (clean: только программы Благорост/маркетплейс)
echo "Запускаем boot процесс..."
pnpm run boot:clean
@@ -51,6 +57,6 @@ echo "Запускаем parser..."
docker compose up -d cooparser
echo "Запускаем контроллер..."
docker compose restart coopback || true
docker compose up -d --force-recreate coopback || true
echo "Перезапуск завершен!"
+17 -14
View File
@@ -1,6 +1,7 @@
#!/bin/bash
# Загружаем per-instance конфиг из корня репо.
# Загружаем per-instance конфиг из корня репо (для CHAIN_URL/MONGODB_URL/API_URL,
# которые читают TS-код boot и шелл-скрипты networks.sh/preactivate.sh).
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
@@ -9,30 +10,32 @@ if [ -f "$ROOT_DIR/.env" ]; then
set +a
fi
# Останавливаем контроллер перед очисткой данных
echo "Останавливаем контроллер..."
docker compose down coopback || true
# Останавливаем и удаляем контейнеры вместе с volumes
echo "Останавливаем и удаляем контейнеры с volumes..."
docker compose down -v mongo postgres cooparser || true
docker compose down -v mongo postgres monoredis cooparser coopback || true
# Останавливаем blockchain контейнер перед удалением данных
echo "Останавливаем blockchain контейнер..."
docker compose stop node || true
# Удаляем blockchain data
# Удаляем blockchain data.
# Контейнерный wipe (alpine под root) стирает данные независимо от их владельца —
# без sudo на любой ноде (на Pi нет passwordless sudo; на проде nodeos пишет
# данные под root). Единый способ с reboot.sh / clean_reboot.sh.
echo "Удаляем blockchain data..."
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
sudo rm -rf ../blockchain-data/
docker run --rm -v "$(cd .. && pwd)/blockchain-data:/d" alpine sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true'
# Пересоздаем и запускаем базы данных
# Пересоздаем и запускаем базы данных + Redis (monoredis).
# monoredis ОБЯЗАТЕЛЕН: без него coopback падает на старте с
# `getaddrinfo EAI_AGAIN monoredis` → MaxRetriesPerRequestError → nodemon crash,
# и провайдер не может взять org-данные partner1 (PROVIDER_URL=coopback:2998).
echo "Пересоздаем и запускаем базы данных..."
docker compose up -d mongo postgres
docker compose up -d mongo postgres monoredis
# Ждем готовности MongoDB
# Ждем готовности MongoDB (standalone, ping вместо ожидания PRIMARY).
echo "Ждем готовности MongoDB..."
until docker compose exec -T mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
until docker compose exec -T mongo mongosh --quiet --eval "db.adminCommand({ping:1}).ok" > /dev/null 2>&1; do
echo "MongoDB еще не готов, ждем..."
sleep 2
done
@@ -46,7 +49,7 @@ until docker compose exec -T postgres pg_isready -U postgres -d voskhod > /dev/n
done
echo "PostgreSQL готов!"
# Запускаем boot процесс
# Запускаем boot процесс (расширенный: совет + пайщики; partner1 — при EXTRA_RENT=1)
echo "Запускаем boot процесс..."
pnpm run boot:extra
@@ -55,6 +58,6 @@ echo "Запускаем parser..."
docker compose up -d cooparser
echo "Запускаем контроллер..."
docker compose up -d coopback
docker compose up -d --force-recreate coopback || true
echo "Перезапуск завершен!"
+37 -22
View File
@@ -291,31 +291,46 @@ export default class Blockchain {
async activateFeature(feature: Feature) {
await this.update_pass_instance()
await this.api.transact(
{
actions: [
{
account: 'eosio',
name: 'activate',
authorization: [
{
actor: 'eosio',
permission: 'active',
try {
await this.api.transact(
{
actions: [
{
account: 'eosio',
name: 'activate',
authorization: [
{
actor: 'eosio',
permission: 'active',
},
],
data: {
feature_digest: feature.hash,
},
],
data: {
feature_digest: feature.hash,
},
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
console.log('Фича активирована: ', feature.name)
console.log('Фича активирована: ', feature.name)
}
catch (e: any) {
// На уже забутстрапленном (не обнулённом) чейне протокол-фича уже активна —
// nodeos отдаёт protocol_feature_exception (code 3250000). Это не повод
// ронять весь boot: фича на месте, нужный результат достигнут, продолжаем.
const errName = e?.json?.error?.name
const errCode = e?.json?.error?.code
const msg = e instanceof Error ? e.message : String(e)
if (errName === 'protocol_feature_exception' || errCode === 3250000 || /already activated/i.test(msg)) {
console.warn(`Фича уже активирована, пропускаю: ${feature.name}`)
return
}
throw e
}
}
async createToken(params: TokenContract.Interfaces.ICreate) {
+19 -9
View File
@@ -1,7 +1,6 @@
import config from '../configs'
import { initExtensionsInPostgres, initSystemStatus } from '../postgres-init'
import { installExtraData, installInitialData, startInfra } from './infra'
import { CooperativeClass, startCoop } from './cooperative'
import { startCoop } from './cooperative'
export async function boot() {
const blockchain = await startInfra()
@@ -14,18 +13,29 @@ export async function boot() {
}
export async function bootClean() {
const blockchain = await startInfra()
console.log('Создаём программы (Благорост и маркетплейс)')
const cooperative = new CooperativeClass(blockchain)
await cooperative.createPrograms(config.provider)
// Только инфраструктура: контракты, фичи, токен, системные параметры.
// Ни совета, ни программ — программы (createPrograms) требуют существующий
// совет и создаются в boot/bootExtra внутри installInitialData. В clean их
// не делаем (иначе soviet::createprog падает с «Совет не найден»).
await startInfra()
}
export async function bootExtra() {
const blockchain = await startInfra()
await installInitialData(blockchain, true) // Создать расширенный совет (устанавливает статус 'active' в MongoDB)
await installExtraData(blockchain) // Добавить дополнительных пайщиков
// installExtraData регистрирует partner1 как coop с auto-approve от провайдера —
// это и есть программная on-chain активация, которая триггерит аренду VM в
// провайдере (PENDING→RENT). boot:extra используется НЕ только для аренды
// сервера (например, просто пересев совета/чейна для других задач), поэтому
// провижининг partner1 включается ТОЛЬКО под флагом EXTRA_RENT=1.
if (process.env.EXTRA_RENT === '1') {
console.log('EXTRA_RENT=1 → installExtraData: провижининг partner1 (триггер аренды)')
await installExtraData(blockchain) // Регистрируем partner1 как coop (active)
}
else {
console.log('EXTRA_RENT не задан → пропускаем провижининг partner1 (аренда не запускается)')
}
console.log('Инициализируем статус системы в PostgreSQL')
await initSystemStatus() // Устанавливает статус 'active' в PostgreSQL
+14 -4
View File
@@ -15,6 +15,9 @@ import { sleep } from '../utils'
import { generateRandomSHA256 } from '../utils/randomHash'
import { initUsersInPostgres, initVaultInPostgres } from '../postgres-init'
import { CooperativeClass } from './cooperative'
import { signProgramAgreement } from './sign-program-agreement'
import { fakeDocument } from '../tests/shared/fakeDocument'
import { walletDraftId, walletProgramId } from '../tests/capital/consts'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
@@ -674,10 +677,17 @@ export async function installExtraData(blockchain: Blockchain) {
status: 'active',
})
// Ресурсы для партнёра: powerup CPU/NET. transfer токенов пропускаем —
// eosio.token::transfer падает на проверке membership в wallet program,
// которая для partner1 не настроена. Для provider sync + Hostkey-flow
// токены не нужны: tx от имени partner1 пойдут от soviet/admin.
// Записываем partner1 в ЦПП Кошелька оператора (voskhod, program_id=1).
// БЕЗ этого провайдерский performInitialTransfers (150 AXON на partner1 ДО
// перехода в ACTIVE, см. provider CLAUDE.md §13) падает ассертом
// eosio.token::is_can_transfer «Получатель не является участником целевой
// потребительской программы кошелька» — и инстанс навсегда застревает в
// INSTALL. wallet::signagree (auth voskhod@active) делает partner1 членом
// ЦПП Кошелька. Та же механика, что для обычных пайщиков в participant.ts.
console.log('Подписываем wallet-соглашение за partner1 (членство в ЦПП Кошелька)')
await signProgramAgreement(blockchain, config.provider, account.username, walletProgramId, walletDraftId, fakeDocument)
// Ресурсы для партнёра: powerup CPU/NET.
await blockchain.powerup({
payer: 'eosio',
receiver: account.username,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/cleos",
"version": "2026.5.23-4",
"version": "2026.5.25-3",
"private": true,
"description": "Обёртка над кошельком cleos для EOSIO блокчейна",
"scripts": {
+5 -7
View File
@@ -1,15 +1,13 @@
#!/usr/bin/env bash
# Локальная сборка всех контрактов: оборачивает build_contracts_cdt.sh в
# CDT-образ. Сам cmake/make живёт в build_contracts_cdt.sh — он же
# используется в CI (там CDT ставится из .deb, без docker), поэтому
# флаги сборки в одном месте и не разъезжаются.
mode="${1:-prod}"
if [ "$mode" = "test" ]; then
is_testnet="ON"
else
is_testnet="OFF"
fi
docker run --rm --name cdt \
--volume "$(pwd):/project" \
-w /project \
dicoop/blockchain:latest \
/bin/bash -c "mkdir -p build && cd build && cmake -DBUILD_TARGET= -DTEST_TARGET= -DVERBOSE=ON -DBUILD_TESTS=OFF -DIS_TESTNET=$is_testnet .. && make"
/bin/bash -c "./build_contracts_cdt.sh $mode"
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Компиляция всех контрактов кооперативной экономики через CDT.
#
# ВАЖНО: скрипт запускается УЖЕ внутри окружения с установленным CDT —
# тулчейн ожидается по пути /cdt/build/lib/cmake/cdt/CDTWasmToolchain.cmake
# (он захардкожен в CMakeLists.txt). Здесь НЕТ docker — только cmake/make.
#
# Два способа подготовить это окружение:
# - локально: build-all.sh оборачивает скрипт в `docker run dicoop/blockchain:latest`
# (в образе CDT уже лежит в /cdt/build);
# - в CI: CDT ставится из .deb (C9S/cdt), а /cdt/build симлинкуется на
# /usr/opt/cdt/<ver> (см. .github/workflows/release.yaml).
#
# Один источник cmake-флагов для обоих путей — чтобы локальная и CI-сборка
# не разъезжались.
set -euo pipefail
mode="${1:-prod}"
if [ "$mode" = "test" ]; then
is_testnet="ON"
else
is_testnet="OFF"
fi
mkdir -p build
cd build
cmake -DBUILD_TARGET= -DTEST_TARGET= -DVERBOSE=ON -DBUILD_TESTS=OFF -DIS_TESTNET="$is_testnet" ..
make
@@ -72,8 +72,8 @@ void capital::importcontrib(eosio::name coopname, eosio::name username, checksum
auto idx = user_wallets.get_index<"byuserwallet"_n>();
auto preimp_it = idx.find(combine_ids(ledger2_wallets::PREIMP_FUND.value, username.value));
if (preimp_it != idx.end() && preimp_it->available.amount > 0) {
eosio::check(preimp_it->blocked.amount == 0,
"preimp.blocked > 0 не поддерживается при импорте — обратиться в поддержку");
// Поле blocked упразднено (2026-05-24): резерв/блокировка на кошельках
// больше не используется, проверка preimp.blocked == 0 излишня.
Ledger2::apply(
_capital, coopname, operations::capital::DROP_PREIMP,
preimp_it->available, username, contributor_hash,
@@ -9,23 +9,26 @@ namespace Capital::Core {
* @brief Совокупный баланс программы Благорост на уровне кооператива.
*
* Источник — ledger2 L2 (`wallets[BLAGOROST_FUND]`), агрегированный по всем
* пайщикам. Σ L3.{available, blocked}[w.cap.blago] == L2.{available, blocked}
* (Эпик 3 invariant), поэтому L2 — авторитетная сумма для CRPS-знаменателя.
* Если кошелёк ещё не создан (никто не инвестировал) — нулевой баланс.
* пайщикам. Σ L3.available[w.cap.blago] == L2.available (Эпик 3 invariant),
* поэтому L2 — авторитетная сумма для CRPS-знаменателя. Если кошелёк ещё не
* создан (никто не инвестировал) — нулевой баланс.
*
* Поле blocked упразднено (2026-05-24): субсчёт «заблокировано» больше не
* используется, баланс кошелька = available.
*/
eosio::asset get_capital_program_share_balance(eosio::name coopname) {
wallets2_index wallets(_ledger2, coopname.value);
auto it = wallets.find(ledger2_wallets::BLAGOROST_FUND.value);
if (it == wallets.end()) return eosio::asset(0, _root_govern_symbol);
return it->available + it->blocked;
return it->available;
}
/**
* @brief Доля пайщика в Благоросте (available + blocked) из ledger2 L3.
* @brief Доля пайщика в Благоросте (available) из ledger2 L3.
*
* Источник — `userwallets[(w.cap.blago, username)]`. Запись существует
* только пока available + blocked > 0 (Эпик 3 §6: auto-delete на нуле),
* поэтому отсутствие = нулевой баланс.
* только пока available > 0 (Эпик 3 §6: auto-delete на нуле), поэтому
* отсутствие = нулевой баланс. Поле blocked упразднено (2026-05-24).
*/
eosio::asset get_capital_program_user_share_balance(eosio::name coopname, eosio::name username) {
userwallets_index user_wallets(_ledger2, coopname.value);
@@ -33,6 +36,6 @@ namespace Capital::Core {
auto key = combine_ids(ledger2_wallets::BLAGOROST_FUND.value, username.value);
auto it = idx.find(key);
if (it == idx.end()) return eosio::asset(0, _root_govern_symbol);
return it->available + it->blocked;
return it->available;
}
}
@@ -67,12 +67,12 @@ void ledger2::revert(eosio::name coopname,
eosio::check(original_operation_id != 0, "revert: original_operation_id обязателен");
// -------- validate mirror_wallet_op --------
eosio::check(mirror_wallet_op <= 5, "revert: неизвестный mirror_wallet_op");
// Не позволяем откатывать через BLOCK/UNBLOCK — они асимметричны и нет
// адекватного зеркала в одной операции (BLOCK + UNBLOCK — обратные сами по себе).
eosio::check(mirror_wallet_op != static_cast<uint8_t>(WalletOp::BLOCK) &&
mirror_wallet_op != static_cast<uint8_t>(WalletOp::UNBLOCK),
"revert: BLOCK/UNBLOCK не подлежат откату через revert (они симметричны сами себе)");
// Зеркало revert — только TRANSFER (обмен wallet_from/wallet_to) либо BURN
// (зеркало ISSUE: изъятие с wallet_from). Бывшие BLOCK/UNBLOCK упразднены
// (2026-05-24); ISSUE/NONE как зеркало смысла не имеют.
eosio::check(mirror_wallet_op == static_cast<uint8_t>(WalletOp::TRANSFER) ||
mirror_wallet_op == static_cast<uint8_t>(WalletOp::BURN),
"revert: mirror_wallet_op должен быть TRANSFER или BURN");
// -------- validate mirror wallets/accounts --------
if (mirror_wallet_from.value != 0) {
@@ -1,5 +1,5 @@
/**
* @brief Атомарная операция по кошельку (issue/transfer/block/unblock/burn/burn_blocked).
* @brief Атомарная операция по кошельку (issue/transfer/burn).
*
* Внутренний action ledger2 — вызывается только через inline из apply().
* Auth: только сам ledger2 (require_auth(get_self())).
@@ -53,13 +53,15 @@ void ledger2::walletop(eosio::name coopname,
eosio::check(amount.symbol == _root_govern_symbol,
"walletop: некорректный символ валюты");
eosio::check(memo.size() < 256, "walletop: memo > 255");
// op_code = 5 (NONE) намеренно не допускается: NONE-операции — это только
// бухпроводка без кошелькового движения, apply.cpp не диспатчит для них walletop.
// Прямой вызов с op_code=5 был бы no-op и сбил бы инвариант parity.
// Допустимы 0..4 (ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN) и 6 (BURN_BLOCKED).
eosio::check(op_code <= static_cast<uint8_t>(WalletOp::BURN_BLOCKED) &&
op_code != static_cast<uint8_t>(WalletOp::NONE),
"walletop: неизвестный op_code");
// Допустимы только ISSUE(0), TRANSFER(1), BURN(4). NONE(5) — это только
// бухпроводка без кошелькового движения, apply.cpp для неё walletop не диспатчит;
// прямой вызов был бы no-op и сбил бы parity. Бывшие BLOCK(2)/UNBLOCK(3)/
// BURN_BLOCKED(6) упразднены (2026-05-24): резерв возврата теперь TRANSFER на
// w.wal.wpend, их op_code больше не валиден.
eosio::check(op_code == static_cast<uint8_t>(WalletOp::ISSUE) ||
op_code == static_cast<uint8_t>(WalletOp::TRANSFER) ||
op_code == static_cast<uint8_t>(WalletOp::BURN),
"walletop: недопустимый op_code");
wallets2_index wallets(get_self(), coopname.value);
userwallets_index user_wallets(get_self(), coopname.value);
@@ -172,7 +174,7 @@ void ledger2::walletop(eosio::name coopname,
// миграций 048/049.
//
// walletop по построению применяет одно и то же `amount` к L2 и L3 (см.
// ниже case'ы ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN/BURN_BLOCKED), а sender-guard на
// ниже case'ы ISSUE/TRANSFER/BURN), а sender-guard на
// строке 46-47 запрещает обход. Поэтому инвариант сохраняется по
// конструкции; полную сверку выполняет бэкенд («стол бухгалтера»),
// вне транзакционного hot path.
@@ -227,60 +229,6 @@ void ledger2::walletop(eosio::name coopname,
cleanup_l3_if_empty(wallet_from);
break;
}
case WalletOp::BLOCK: {
eosio::check(wallet_from.value != 0, "walletop BLOCK: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop BLOCK: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->available >= amount,
std::string{"walletop BLOCK: недостаточно available на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) {
w.available -= amount;
w.blocked += amount;
});
if (is_user_shared_l3(wallet_from)) {
auto uw = find_l3(wallet_from);
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
uw->available >= amount,
std::string{"walletop BLOCK: недостаточно L3-available у пайщика "} +
username.to_string() + " на " + wallet_from.to_string());
auto uw_pri = user_wallets.find(uw->id);
user_wallets.modify(uw_pri, payer, [&](auto& r) {
r.available -= amount;
r.blocked += amount;
});
}
break;
}
case WalletOp::UNBLOCK: {
eosio::check(wallet_from.value != 0, "walletop UNBLOCK: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop UNBLOCK: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->blocked >= amount,
std::string{"walletop UNBLOCK: недостаточно blocked на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) {
w.blocked -= amount;
w.available += amount;
});
if (is_user_shared_l3(wallet_from)) {
auto uw = find_l3(wallet_from);
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
uw->blocked >= amount,
std::string{"walletop UNBLOCK: недостаточно L3-blocked у пайщика "} +
username.to_string() + " на " + wallet_from.to_string());
auto uw_pri = user_wallets.find(uw->id);
user_wallets.modify(uw_pri, payer, [&](auto& r) {
r.blocked -= amount;
r.available += amount;
});
}
break;
}
case WalletOp::BURN: {
eosio::check(wallet_from.value != 0, "walletop BURN: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop BURN: wallet_to должен быть пустым");
@@ -311,30 +259,6 @@ void ledger2::walletop(eosio::name coopname,
eosio::check(false, "walletop NONE: запрещённый op_code");
break;
}
case WalletOp::BURN_BLOCKED: {
eosio::check(wallet_from.value != 0, "walletop BURN_BLOCKED: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop BURN_BLOCKED: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->blocked >= amount,
std::string{"walletop BURN_BLOCKED: недостаточно blocked на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) { w.blocked -= amount; });
if (is_user_shared_l3(wallet_from)) {
auto uw = find_l3(wallet_from);
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
uw->blocked >= amount,
std::string{"walletop BURN_BLOCKED: недостаточно L3-blocked у пайщика "} +
username.to_string() + " на " + wallet_from.to_string());
auto uw_pri = user_wallets.find(uw->id);
user_wallets.modify(uw_pri, payer, [&](auto& r) { r.blocked -= amount; });
}
cleanup_l2_if_empty(wallet_from);
cleanup_l3_if_empty(wallet_from);
break;
}
}
// Post-mutation Σ L3 == L2: см. блок выше (строки 163-175). Снято
@@ -1,5 +1,3 @@
#include <vector>
/**
* @brief Универсальное миграционное действие контракта ledger2 — точка
* расширения для разовых исправлений состояния, которые можно провести
@@ -9,23 +7,33 @@
* после её прогона на проде тело очищается до пустого `require_auth(get_self())`
* (как в `capital::migrate`).
*
* Текущая задача (2026-05-21): чистка осиротевших L3-записей `w.cap.gen`
* на voskhod после смены WalletKind GENERATOR_FUND с USER_SHARED на
* COOPERATIVE (см. `lib/core/ledger2/wallets.hpp`).
* Текущая задача (2026-05-24): свёртка `blocked → available` по ВСЕМ коопам.
*
* Контекст: на voskhod `convertsegm` падал «недостаточно L3-средств у
* пайщика» в проектах, где CRPS перераспределял доли между сегментами.
* w.cap.gen был USER_SHARED, а CRPS в approvecmmt не делал per-user
* компенсирующих TRANSFER между сегментами: `Σ COMMIT_RID == Σ ACCEPT_RID`
* соблюдался только на проекте, не на пайщике.
* Контекст: механика «заблокированного» баланса упразднена (см.
* `lib/core/ledger2/operations.hpp` — удалены WalletOp BLOCK/UNBLOCK/BURN_BLOCKED;
* резерв возврата паевого теперь выражается переводом на кошелёк-резерв
* `w.wal.wpend`). Поле `blocked` остаётся в таблицах `wallets2`/`userwallets`
* как deprecated (физическое удаление поля = небезопасная смена layout таблицы
* на живых коопах, выносится в отдельный cleanup-деплой). Перед тем как поле
* перестанет поддерживаться кодом, накопленные `blocked`-остатки нужно вернуть
* в `available`, чтобы средства не «зависли» на упразднённом субсчёте.
*
* Архитектурный фикс: w.cap.gen — COOPERATIVE-пул без L3. Чтобы UI/бэкенд
* не врали остатками по «личным» w.cap.gen, удаляем осиротевшие L3-записи
* прямым `userwallets.erase`. L2-баланс `wallets2[w.cap.gen]` уже верен
* (синхронен с Σ старых userwallets), его не трогаем.
* Действие: пройти всех кооперативов (cooperatives2 в scope registrator) и для
* каждого свернуть `blocked → available` на уровнях L2 (`wallets2`) и L3
* (`userwallets`): `available += blocked; blocked = 0`. Сумма средств на кошельке
* не меняется — только субсчёт.
*
* Идемпотентно: повторный вызов на чистой БД — no-op (lower_bound пуст).
* Только voskhod: на остальных кооперативах w.cap.gen не использовался.
* Идемпотентно: после свёртки `blocked == 0`, повторный прогон — no-op.
*
* Сигнатура без аргументов — действие вызывается автоматически при деплое
* контракта (как и прочие задачи migrate); проходит по всем кооперативам сам.
*
* ПРЕДУСЛОВИЕ (операционное): на момент прогона не должно быть заявок на возврат
* «в полёте» (статусы pending/authorized в `wallet::withdraws`) — их `blocked`
* относится к старой механике и при свёртке в `available` вернётся пайщику как
* свободные средства, а последующий `completewthd` (BURN с `w.wal.wpend`) не
* найдёт резерва. Незавершённые заявки нужно довести (complete/decline) ДО
* деплоя с этой миграцией.
*
* @ingroup public_ledger2_actions
*
@@ -34,24 +42,31 @@
void ledger2::migrate() {
require_auth(get_self());
const eosio::name target_coop = "voskhod"_n;
cooperatives2_index coops(_registrator, _registrator.value);
userwallets_index user_wallets(get_self(), target_coop.value);
auto idx = user_wallets.get_index<"bywallet"_n>();
for (auto c = coops.begin(); c != coops.end(); ++c) {
const eosio::name coopname = c->username;
// Собираем primary id записей до erase (модификация контейнера при
// итерации через secondary index — небезопасна).
std::vector<uint64_t> ids_to_erase;
for (auto it = idx.lower_bound(ledger2_wallets::GENERATOR_FUND.value);
it != idx.end() && it->wallet_name == ledger2_wallets::GENERATOR_FUND;
++it) {
ids_to_erase.push_back(it->id);
}
// --- L3: userwallets[coopname] ---
// Модифицируем только не-ключевые поля (available/blocked) — итерация по
// первичному индексу с modify безопасна (порядок строк не меняется).
userwallets_index user_wallets(get_self(), coopname.value);
for (auto it = user_wallets.begin(); it != user_wallets.end(); ++it) {
if (it->blocked.amount <= 0) continue;
user_wallets.modify(it, get_self(), [&](auto& r) {
r.available += r.blocked;
r.blocked = eosio::asset(0, r.blocked.symbol);
});
}
for (uint64_t id : ids_to_erase) {
auto pri = user_wallets.find(id);
if (pri != user_wallets.end()) {
user_wallets.erase(pri);
// --- L2: wallets2[coopname] ---
wallets2_index wallets(get_self(), coopname.value);
for (auto it = wallets.begin(); it != wallets.end(); ++it) {
if (it->blocked.amount <= 0) continue;
wallets.modify(it, get_self(), [&](auto& w) {
w.available += w.blocked;
w.blocked = eosio::asset(0, w.blocked.symbol);
});
}
}
}
@@ -31,9 +31,9 @@
* - `WalletOp::WALLET_ONLY` удалён (ADR-003): «без бухпроводок» определяется
* парой `(debit_account_id == 0, credit_account_id == 0)` на уровне записи.
* Для `o.cap.invest` теперь TRANSFER без проводок (оба account_id == 0).
* - Добавлен `WalletOp::BURN` (ADR-003): `available -= amount` на `wallet_from`,
* без `wallet_to`. На текущем этапе в `OPERATION_REGISTRY` не используется —
* зарезервирован под будущие операции штатного сжигания.
* - `WalletOp::BURN` (ADR-003): `available -= amount` на `wallet_from`,
* без `wallet_to`. Используется в `o.wal.wthcpl` (сжигание резерва возврата
* с `w.wal.wpend`), `o.cap.drppre` и как зеркало ISSUE в `revert`.
* - Единые программные кошельки: `BLAGOROST_FUND` (`w.cap.blago`) и
* `GENERATOR_FUND` (`w.cap.gen`) — заменили ранее декомпозированные
* `bginv/bgprop/bgrid/bgmem` и `gncom/gnmem` (ADR-009).
@@ -61,9 +61,9 @@ namespace operations {
// wallet
namespace wallet {
inline constexpr eosio::name COMPLETE_DEPOSIT = "o.wal.depcpl"_n; ///< Завершение внесения паевого взноса (Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY).
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, BURN_BLOCKED SHARE_FUND_PAY — деньги уходят из системы, без wallet_to).
inline constexpr eosio::name REQUEST_WITHDRAW = "o.wal.wthreq"_n; ///< Запрос на возврат паевого: BLOCK на SHARE_FUND_PAY (без Dr/Cr).
inline constexpr eosio::name DECLINE_WITHDRAW = "o.wal.wthdec"_n; ///< Отклонение запроса на возврат: UNBLOCK на SHARE_FUND_PAY (без Dr/Cr).
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, BURN с WITHDRAW_PENDING — деньги уходят из системы, без wallet_to).
inline constexpr eosio::name REQUEST_WITHDRAW = "o.wal.wthreq"_n; ///< Запрос на возврат паевого: TRANSFER SHARE_FUND_PAY → WITHDRAW_PENDING (резерв, без Dr/Cr).
inline constexpr eosio::name DECLINE_WITHDRAW = "o.wal.wthdec"_n; ///< Отклонение запроса на возврат: TRANSFER WITHDRAW_PENDING → SHARE_FUND_PAY (снятие резерва, без Dr/Cr).
}
// capital
@@ -129,14 +129,23 @@ namespace operations {
* `(debit_account_id == 0, credit_account_id == 0)` на уровне записи реестра.
* Compile-time правило `(debit==0) ⇔ (credit==0)` ловит смешанные пары.
*/
//
// Удаление BLOCK/UNBLOCK/BURN_BLOCKED (2026-05-24): механика «заблокированного»
// баланса упразднена. Резерв средств под заявку на возврат паевого теперь
// выражается переводом на отдельный кошелёк-резерв `w.wal.wpend` (TRANSFER),
// возврат резерва — обратным TRANSFER, завершение — BURN с резерва. Поле
// `blocked` в таблицах wallets2/userwallets оставлено deprecated (всегда 0
// после ledger2::migrate-свёртки) — физическое удаление поля = небезопасная
// смена layout таблицы на живых коопах, выносится в отдельный cleanup-деплой.
//
// Числовые значения ISSUE/TRANSFER/BURN/NONE СОХРАНЕНЫ (не перенумерованы),
// чтобы исторические op_code в blockchain_actions читались бэкендом без сдвига
// смысла (2 и 3 — бывшие BLOCK/UNBLOCK — больше не выдаются и невалидны на входе).
enum class WalletOp : uint8_t {
ISSUE = 0, ///< первичный вход средств на кошелёк wallet_to (wallet_from = empty)
TRANSFER = 1, ///< перемещение wallet_from → wallet_to (с Dr/Cr ИЛИ без — по парам account_id)
BLOCK = 2, ///< available-=amount, blocked+=amount на wallet_from
UNBLOCK = 3, ///< blocked-=amount, available+=amount на wallet_from
BURN = 4, ///< изъятие amount с wallet_from->available, без wallet_to. Покрывает оба кейса: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY; (b) зеркало ISSUE при `ledger2::revert` (различие — через operation_code: `o.adj.rev` для adjustment-mirror).
NONE = 5, ///< только бухпроводка без перемещения средств (wallet_from = empty, wallet_to = empty, debit ≠ 0, credit ≠ 0). Покрывает кейсы внутрибалансовых проводок типа Dr 04 / Cr 08 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
BURN_BLOCKED = 6, ///< изъятие amount из wallet_from->blocked, без wallet_to. Используется при завершении возврата паевого (`o.wal.wthcpl`): средства предварительно заблокированы через BLOCK (REQUEST_WITHDRAW), на завершении сжигаются — выходят из системы (получателя на цепи нет). На L3 синхронно: from_uw->blocked -= amount.
};
/**
@@ -145,7 +154,7 @@ enum class WalletOp : uint8_t {
* Семантика полей по `wallet_op`:
* - ISSUE: wallet_from = eosio::name{}, wallet_to = required.
* - TRANSFER: wallet_from = required, wallet_to = required (≠ from).
* - BLOCK / UNBLOCK / BURN / BURN_BLOCKED: wallet_from = required, wallet_to = eosio::name{}.
* - BURN: wallet_from = required, wallet_to = eosio::name{}.
*
* Семантика бух.проводки:
* - Без проводок: debit_account_id == 0 И credit_account_id == 0.
@@ -157,7 +166,7 @@ struct OperationRegistryEntry {
eosio::name process_type; ///< тип процесса с префиксом `p.<contract>.<noun>`
WalletOp wallet_op;
eosio::name wallet_from; ///< пустое имя для ISSUE
eosio::name wallet_to; ///< пустое имя для BLOCK/UNBLOCK/BURN/BURN_BLOCKED
eosio::name wallet_to; ///< пустое имя для BURN
uint64_t debit_account_id; ///< 0 если без бухпроводки (тогда credit_account_id тоже == 0)
uint64_t credit_account_id; ///< 0 если без бухпроводки (тогда debit_account_id тоже == 0)
const char* human_name;
@@ -184,12 +193,12 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Внесение пайщиком паевого взноса" },
// 4. Возврат паевого взноса: Dr 80 / Cr 51, BURN_BLOCKED SHARE_FUND_PAY.
// Сжигание из заблокированной суммы пайщика (BLOCK был на REQUEST_WITHDRAW):
// 4. Возврат паевого взноса: Dr 80 / Cr 51, BURN WITHDRAW_PENDING.
// Сжигание из кошелька-резерва (TRANSFER в резерв был на REQUEST_WITHDRAW):
// деньги уходят из системы (банковский перевод пайщику), получателя на цепи нет.
// Бухгалтерия: паевой фонд уменьшается (Дт 80), расчётный счёт уменьшается (Кт 51).
{ operations::wallet::COMPLETE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BURN_BLOCKED,
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
{ operations::wallet::COMPLETE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BURN,
ledger2_wallets::WITHDRAW_PENDING, eosio::name{},
ledger2_accounts::SHARE_FUND, ledger2_accounts::BANK_ACCOUNT,
"Возврат паевого взноса пайщику" },
@@ -273,17 +282,19 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
ledger2_accounts::SHARE_FUND, ledger2_accounts::TARGET_RECEIPTS,
"Трансляция паевого взноса из ЦПП «Цифровой Кошелёк» в членский взнос за пользование инфраструктурой" },
// 15. Запрос на возврат паевого: BLOCK SHARE_FUND_PAY (без Dr/Cr — внутри одного бухсчёта 80).
{ operations::wallet::REQUEST_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BLOCK,
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
// 15. Запрос на возврат паевого: TRANSFER SHARE_FUND_PAY → WITHDRAW_PENDING
// (без Dr/Cr — оба кошелька на счёте 80; резерв средств на время рассмотрения).
{ operations::wallet::REQUEST_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::TRANSFER,
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::WITHDRAW_PENDING,
0, 0,
"Блокировка паевого под запрос на возврат" },
"Резервирование паевого под запрос на возврат" },
// 16. Отклонение запроса на возврат: UNBLOCK SHARE_FUND_PAY (без Dr/Cr — зеркало REQUEST_WITHDRAW).
{ operations::wallet::DECLINE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::UNBLOCK,
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
// 16. Отклонение запроса на возврат: TRANSFER WITHDRAW_PENDING → SHARE_FUND_PAY
// (без Dr/Cr — зеркало REQUEST_WITHDRAW; возврат резерва пайщику).
{ operations::wallet::DECLINE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::TRANSFER,
ledger2_wallets::WITHDRAW_PENDING, ledger2_wallets::SHARE_FUND_PAY,
0, 0,
"Разблокировка паевого после отклонения запроса на возврат" },
"Снятие резерва паевого после отклонения запроса на возврат" },
// 17. Возврат из ЦПП «Благорост» в Цифровой Кошелёк: TRANSFER BLAGOROST_FUND → SHARE_FUND_PAY (без Dr/Cr — оба счёта 80, зеркало INVEST).
{ operations::capital::WITHDRAW_FROM_CAPITAL, processes::capital::WTHCAP, WalletOp::TRANSFER,
@@ -340,7 +351,7 @@ static constexpr size_t OPERATION_REGISTRY_SIZE = sizeof(OPERATION_REGISTRY) / s
// оба существуют в `LEDGER2_ACCOUNT_MAP`.
// 4. Для TRANSFER: `wallet_from` ≠ `wallet_to`, оба ≠ 0.
// 5. Для ISSUE: `wallet_from` == 0 и `wallet_to` ≠ 0.
// 6. Для BLOCK / UNBLOCK / BURN / BURN_BLOCKED: `wallet_from` ≠ 0, `wallet_to` == 0.
// 6. Для BURN: `wallet_from` ≠ 0, `wallet_to` == 0.
// 7. Все id кошельков из записей существуют в `LEDGER2_WALLET_REGISTRY`.
namespace ledger2_registry_detail {
constexpr bool operation_codes_unique() {
@@ -384,11 +395,11 @@ namespace ledger2_registry_detail {
return true;
}
// Правило 6: BURN / BURN_BLOCKED — wallet_from required, wallet_to == 0 (ADR-003).
// Правило 6: BURN — wallet_from required, wallet_to == 0 (ADR-003).
constexpr bool burn_pattern_correct() {
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
const auto& e = OPERATION_REGISTRY[i];
if (e.wallet_op != WalletOp::BURN && e.wallet_op != WalletOp::BURN_BLOCKED) continue;
if (e.wallet_op != WalletOp::BURN) continue;
if (e.wallet_from.value == 0) return false;
if (e.wallet_to.value != 0) return false;
}
@@ -440,7 +451,7 @@ static_assert(ledger2_registry_detail::dr_ne_cr_when_posting(),
static_assert(ledger2_registry_detail::transfer_wallet_from_ne_to(),
"OPERATION_REGISTRY: TRANSFER с wallet_from == wallet_to или одним из них == 0");
static_assert(ledger2_registry_detail::burn_pattern_correct(),
"OPERATION_REGISTRY: BURN / BURN_BLOCKED требует wallet_from ≠ 0 и wallet_to == 0");
"OPERATION_REGISTRY: BURN требует wallet_from ≠ 0 и wallet_to == 0");
static_assert(ledger2_registry_detail::none_pattern_correct(),
"OPERATION_REGISTRY: NONE требует wallet_from == 0, wallet_to == 0 и обе проводки заполненными");
static_assert(ledger2_registry_detail::accounts_exist_in_map(),
@@ -27,7 +27,7 @@
* w.mkt.* — Маркетплейс (выплаты поставщикам)
*
* Sentinel `eosio::name{}` (пустое имя, value=0) — «кошелёк вне системы»
* для ISSUE (нет wallet_from) и для BURN/BLOCK/UNBLOCK (нет wallet_to).
* для ISSUE (нет wallet_from) и для BURN (нет wallet_to).
*
* При первом ISSUE/TRANSFER кошелёк создаётся автоматически по записи
* из WALLET_REGISTRY. При обнулении available+blocked запись удаляется.
@@ -46,7 +46,8 @@ struct ledger2_wallets {
// wallet — паевой фонд + возвраты + ЦК
static constexpr eosio::name SHARE_FUND_PAY = "w.wal.share"_n; ///< Паевой взнос пайщика (USER_SHARED)
static constexpr eosio::name CK_MEMBER = "w.wal.member"_n; ///< ЦК — членская часть пайщика (USER_SHARED)
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< DEPRECATED 2026-05-21: после переключения o.wal.wthcpl с TRANSFER на BURN_BLOCKED кошелёк больше не получает новых средств. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< DEPRECATED 2026-05-21: исторический sink возвратов. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
static constexpr eosio::name WITHDRAW_PENDING = "w.wal.wpend"_n; ///< Резерв паевого под заявку на возврат (COOPERATIVE-пул). o.wal.wthreq переводит сюда с w.wal.share, o.wal.wthdec возвращает обратно, o.wal.wthcpl сжигает отсюда. Заменил механику blocked/BLOCK/UNBLOCK 2026-05-24.
// registrator — минимальный паевой + вступительные
static constexpr eosio::name MIN_SHARE_FUND = "w.reg.minshr"_n; ///< Минимальный паевой взнос пайщика (USER_SHARED, без сверки соглашений)
@@ -94,7 +95,7 @@ struct Ledger2WalletMeta {
WalletKind kind;
};
inline constexpr std::array<Ledger2WalletMeta, 14> LEDGER2_WALLET_REGISTRY = {{
inline constexpr std::array<Ledger2WalletMeta, 15> LEDGER2_WALLET_REGISTRY = {{
// USER_SHARED (5) — L3-разрез по пайщику
{ ledger2_wallets::MIN_SHARE_FUND, "Минимальный паевой взнос", WalletKind::USER_SHARED },
{ ledger2_wallets::SHARE_FUND_PAY, "Паевой взнос пайщика", WalletKind::USER_SHARED },
@@ -102,7 +103,7 @@ inline constexpr std::array<Ledger2WalletMeta, 14> LEDGER2_WALLET_REGISTRY = {{
{ ledger2_wallets::BLAGOROST_FUND, "ЦПП «Благорост» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::PREIMP_FUND, "Первичный учёт РИД-взносов до перехода на электронный учёт", WalletKind::USER_SHARED },
// COOPERATIVE (9) — единый кооперативный баланс, без L3
// COOPERATIVE (10) — единый кооперативный баланс, без L3
// GENERATOR_FUND переведён сюда из USER_SHARED (см. wallets.hpp:64) —
// CRPS-распределение между сегментами проекта не поддерживает per-user
// компенсирующие TRANSFER на approvecmmt, поэтому L3-проверка walletop
@@ -110,6 +111,7 @@ inline constexpr std::array<Ledger2WalletMeta, 14> LEDGER2_WALLET_REGISTRY = {{
{ ledger2_wallets::GENERATOR_FUND, "ЦПП «Генератор» — единый кошелёк программы", WalletKind::COOPERATIVE },
{ ledger2_wallets::ENTRANCE_FEES, "Вступительные взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам (deprecated, не используется в новых операциях)", WalletKind::COOPERATIVE },
{ ledger2_wallets::WITHDRAW_PENDING, "Резерв паевого под заявку на возврат", WalletKind::COOPERATIVE },
{ ledger2_wallets::INFRA_FEES, "Членские взносы за инфраструктуру кооп. платформы", WalletKind::COOPERATIVE },
{ ledger2_wallets::DELEGATE_FEES, "Делегатские членские взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::SOV_EXPENSES, "Хозяйственные расходы из числа целевого финансирования", WalletKind::COOPERATIVE },
@@ -3,10 +3,11 @@
# программу «Цифровой Кошелёк» (SHARE_FUND_PAY).
#
# Четырёхактовый процесс с одной авторизацией советом. Запись заявки живёт в
# таблице `withdraws` от создания до выплаты или отказа. На закрывающем
# действии completewthd срабатывает одна операция o.wal.wthcpl — сжигание
# заблокированной суммы (BURN_BLOCKED w.wal.share): деньги уходят из системы
# пайщику банковским переводом, получателя на цепи нет.
# таблице `withdraws` от создания до выплаты или отказа. На подаче заявки сумма
# переводится в кошелёк-резерв (WITHDRAW_PENDING, w.wal.wpend). На закрывающем
# действии completewthd срабатывает операция o.wal.wthcpl — сжигание суммы из
# резерва (BURN w.wal.wpend): деньги уходят из системы пайщику банковским
# переводом, получателя на цепи нет.
#
# Источники правды в коде:
# • cpp/wallet/wallet.hpp — actions
@@ -43,8 +44,8 @@ actions:
actor: contributor
role: opener
purpose: >
Пайщик создаёт заявку на возврат паевого взноса. Контракт блокирует
сумму на кошельке SHARE_FUND_PAY (статус available → blocked) и
Пайщик создаёт заявку на возврат паевого взноса. Контракт переводит
сумму с кошелька SHARE_FUND_PAY в кошелёк-резерв WITHDRAW_PENDING и
создаёт повестку в совете о возврате с callback'ом авторизации.
- name: wallet::authwthd
human: Авторизовать выплату
@@ -60,16 +61,17 @@ actions:
role: closer
purpose: >
Gateway подтвердил списание средств в пользу пайщика. Это закрывающее
действие: применяется операция o.wal.wthcpl (BURN_BLOCKED w.wal.share)
заблокированная сумма сжигается, в бухгалтерии проходит обратная
действие: применяется операция o.wal.wthcpl (BURN w.wal.wpend)
— сумма из резерва сжигается, в бухгалтерии проходит обратная
проводка Дт 80 / Кт 51. Запись заявки удаляется.
- name: wallet::declinewthd
human: Отклонить
actor: soviet
role: reject
purpose: >
Отказ на любом из этапов до выплаты. Сумма разблокируется на кошельке
пайщика (UNBLOCK через o.wal.wthdec), запись заявки удаляется.
Отказ на любом из этапов до выплаты. Сумма возвращается из резерва на
кошелёк пайщика (o.wal.wthdec: WITHDRAW_PENDING → SHARE_FUND_PAY),
запись заявки удаляется.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
entity: wallet::withdraw
@@ -80,8 +82,8 @@ states:
- name: pending
human: Ожидает решения совета
description: >
Заявка создана, сумма заблокирована на кошельке пайщика (available → blocked),
повестка отправлена в совет.
Заявка создана, сумма переведена в кошелёк-резерв (SHARE_FUND_PAY →
WITHDRAW_PENDING), повестка отправлена в совет.
kind: normal
- name: authorized
human: Выплата отправлена
@@ -92,15 +94,15 @@ states:
- name: completed
human: Возврат выплачен
description: >
Gateway подтвердил списание, заблокированная сумма сжигается на кошельке
пайщика (BURN_BLOCKED w.wal.share). В бухгалтерии прошла обратная
Gateway подтвердил списание, сумма из резерва сжигается
(BURN w.wal.wpend). В бухгалтерии прошла обратная
проводка Дт 80 / Кт 51. Запись заявки удалена.
kind: final
- name: removed
human: Отклонено
description: >
Возврат не состоялся: сумма разблокирована (UNBLOCK через o.wal.wthdec),
запись удалена.
Возврат не состоялся: сумма возвращена из резерва на кошелёк пайщика
(o.wal.wthdec: WITHDRAW_PENDING → SHARE_FUND_PAY), запись удалена.
kind: virtual
virtual: true
@@ -172,7 +174,7 @@ scenario:
- Сумма возврата ≤ доступного остатка.
post:
- В таблице withdraws создана запись со статусом `pending`.
- На кошельке пайщика сумма переведена available → blocked.
- Сумма переведена с SHARE_FUND_PAY в кошелёк-резерв WITHDRAW_PENDING.
- Совет получил повестку о возврате.
- step: 2
@@ -197,13 +199,13 @@ scenario:
action: wallet::completewthd
description: >
Gateway подтверждает списание средств в пользу пайщика. Контракт
применяет ledger2-операцию o.wal.wthcpl (BURN_BLOCKED w.wal.share с
применяет ledger2-операцию o.wal.wthcpl (BURN w.wal.wpend с
обратной проводкой Дт 80 / Кт 51) и удаляет запись заявки.
pre:
- Заявка в статусе `authorized`.
- Gateway подтвердил списание.
post:
- Заблокированная сумма сожжена с SHARE_FUND_PAY (w.wal.share).
- Сумма из резерва сожжена (WITHDRAW_PENDING, w.wal.wpend).
- В ledger2 применена операция o.wal.wthcpl.
- Запись заявки удалена.
@@ -214,8 +216,8 @@ scenario:
actor: soviet
description: >
Совет принял отрицательное решение либо Gateway отклонил платёж.
Сумма разблокирована на кошельке пайщика (UNBLOCK через o.wal.wthdec),
запись удалена.
Сумма возвращена из резерва на кошелёк пайщика (o.wal.wthdec:
WITHDRAW_PENDING → SHARE_FUND_PAY), запись удалена.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
documents:
@@ -234,45 +236,45 @@ documents:
# ── Секция 6. Операции ──────────────────────────────────────────────────────
operations:
- ledger_code: o.wal.wthreq
human_name: Блокировка паевого под запрос на возврат
wallet_op: BLOCK
human_name: Резервирование паевого под запрос на возврат
wallet_op: TRANSFER
wallet_from: w.wal.share
wallet_to: ~
wallet_to: w.wal.wpend
debit: ~
credit: ~
amount_ref: withdraw.quantity
triggered_by: wallet::createwthd
description: >
На создании заявки контракт блокирует сумму на кошельке пайщика
(SHARE_FUND_PAY): available → blocked. Бухгалтерская проводка не
создаётся — движение внутри одного бухсчёта 80.
На создании заявки контракт переводит сумму с кошелька пайщика
(SHARE_FUND_PAY) в кошелёк-резерв (WITHDRAW_PENDING). Бухгалтерская
проводка не создаётся — движение внутри одного бухсчёта 80.
- ledger_code: o.wal.wthdec
human_name: Разблокировка паевого после отклонения запроса на возврат
wallet_op: UNBLOCK
wallet_from: w.wal.share
wallet_to: ~
human_name: Снятие резерва паевого после отклонения запроса на возврат
wallet_op: TRANSFER
wallet_from: w.wal.wpend
wallet_to: w.wal.share
debit: ~
credit: ~
amount_ref: withdraw.quantity
triggered_by: wallet::declinewthd
description: >
Зеркало REQUEST_WITHDRAW: blocked → available на кошельке пайщика.
Бухгалтерская проводка не создаётся.
Зеркало REQUEST_WITHDRAW: сумма возвращается из резерва (WITHDRAW_PENDING)
на кошелёк пайщика (SHARE_FUND_PAY). Бухгалтерская проводка не создаётся.
- ledger_code: o.wal.wthcpl
human_name: Возврат паевого взноса пайщику
wallet_op: BURN_BLOCKED
wallet_from: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
wallet_op: BURN
wallet_from: w.wal.wpend # кошелёк-резерв возвратов
wallet_to: ~ # деньги уходят из системы, получателя на цепи нет
debit: 80 # Паевой фонд (складочный капитал)
credit: 51 # Расчётный счёт
amount_ref: withdraw.quantity
triggered_by: wallet::completewthd
description: >
Обратная операция к o.wal.depcpl: заблокированная сумма сжигается с
кошелька пайщика «ЦПП Цифровой Кошелёк» (w.wal.share) — деньги уходят
из системы банковским переводом пайщику. На цепи получателя нет.
Обратная операция к o.wal.depcpl: зарезервированная сумма сжигается с
кошелька-резерва (w.wal.wpend) — деньги уходят из системы банковским
переводом пайщику. На цепи получателя нет.
В бухгалтерии прошла проводка Дт 80 / Кт 51 — паевой фонд уменьшился,
деньги ушли с расчётного счёта пайщику.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/contracts",
"version": "2026.5.23-4",
"version": "2026.5.25-3",
"private": true,
"type": "module",
"scripts": {
+3 -1
View File
@@ -1,4 +1,6 @@
{
"watch": ["./src"],
"ext": "ts"
"ext": "ts",
"delay": 2000,
"signal": "SIGTERM"
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "create-nodejs-express-app",
"version": "2026.5.23-4",
"version": "2026.5.25-3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "create-nodejs-express-app",
"version": "2026.5.23-4",
"version": "2026.5.25-3",
"license": "MIT",
"dependencies": {
"@a2seven/yoo-checkout": "^1.1.4",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/controller",
"version": "2026.5.23-4",
"version": "2026.5.25-3",
"description": "Бэкенд GraphQL API кооператива на NestJS",
"private": true,
"bin": "bin/createNodejsApp.js",
@@ -193,12 +193,12 @@
"@antfu/eslint-config": "^2.24.1",
"@graphql-codegen/cli": "^5.0.3",
"@graphql-codegen/typescript": "^4.1.1",
"@swc-node/register": "^1.11.1",
"@swc/core": "^1.15.33",
"@graphql-codegen/typescript-apollo-client-helpers": "^3.0.0",
"@graphql-codegen/typescript-operations": "^4.3.1",
"@nestjs/testing": "^11.1.19",
"@redocly/cli": "^1.18.0",
"@swc-node/register": "^1.11.1",
"@swc/core": "^1.15.33",
"@types/express": "^4.17.21",
"@types/node": "^20.11.26",
"@typescript-eslint/eslint-plugin": "^5.10.0",
File diff suppressed because it is too large Load Diff
@@ -17,7 +17,4 @@ export class Ledger2WalletDTO {
@Field(() => String, { description: 'Доступный баланс' })
available!: string;
@Field(() => String, { description: 'Заблокированный баланс' })
blocked!: string;
}
@@ -28,9 +28,6 @@ export class ProgramWalletDTO {
@Field(() => String, { description: 'Доступный баланс (формат: "100.0000 RUB")' })
available!: string;
@Field(() => String, { description: 'Заблокированный баланс (формат: "100.0000 RUB")' })
blocked!: string;
@Field(() => String, { description: 'Паевой взнос (формат: "100.0000 RUB")' })
membership_contribution!: string;
@@ -49,7 +46,6 @@ export class ProgramWalletDTO {
dto.agreement_id = entity.agreement_id;
dto.username = entity.username;
dto.available = entity.available;
dto.blocked = entity.blocked;
dto.membership_contribution = entity.membership_contribution;
dto.blockNum = entity.blockNum;
return dto;
@@ -256,12 +256,20 @@ export class DecisionExpiredNotificationService implements OnModuleInit, OnModul
}
}
// Обновляем дату последней проверки (только это поле, чтобы не перезаписать другие параметры конфига)
// Обновляем дату последней проверки. ВАЖНО: read-modify-write по СВЕЖЕМУ
// config из БД, а не по захваченному при initialize() снимку `plugin`.
// Cron-замыкание держит in-memory снимок с момента boot; за время между
// boot и тиком онбординг-флаги (`onboarding_*_done/_hash`) и прочие поля
// могли быть записаны в БД другими сервисами. Перезапись устаревшего
// снимка стёрла бы их (lost update). Поэтому берём актуальный config и
// трогаем только lastCheckDate.
const fresh = await this.extensionRepository.findByName('chairman');
const baseConfig = fresh?.config ?? plugin.config;
const updatedConfig = {
...plugin.config,
...baseConfig,
lastCheckDate: new Date().toISOString(),
};
await this.extensionRepository.update({ ...plugin, config: updatedConfig });
await this.extensionRepository.update({ name: 'chairman', config: updatedConfig });
this.logger.debug('Проверка истекших решений завершена');
} catch (error) {
@@ -0,0 +1,70 @@
/**
* Matrix: комната секретаря — создаётся вручную председателем/советом из desktop.
* Всегда незашифрована (E2EE секретарь транскрибировать не может). Тип (public/private)
* выбирается при создании, поэтому isPrivate здесь не фиксируем — задаёт сервис.
*/
import type { MatrixChatRoomPreset } from './matrix-chat-room-preset.types';
/**
* Те же права, что у комнаты проекта Capital: модераторы (50) почти по всем настройкам;
* 100 только у Matrix-админа для критичных state.
*/
function buildPowerLevels(adminUserId: string): Record<string, unknown> {
return {
users_default: 0,
invite: 50,
kick: 50,
ban: 50,
redact: 50,
state_default: 50,
events_default: 0,
users: {
[adminUserId]: 100,
},
events: {
'm.room.name': 50,
'm.room.topic': 50,
'm.room.avatar': 50,
'm.room.pinned_events': 50,
'm.room.canonical_alias': 50,
'm.room.aliases': 50,
'm.room.power_levels': 100,
'm.room.history_visibility': 50,
'm.room.encryption': 100,
'm.room.tombstone': 100,
'm.room.server_acl': 100,
'im.vector.modular.widgets': 50,
'm.widget': 50,
'org.matrix.msc2876.widgets': 50,
'io.element.widgets.layout': 50,
'io.element.call': 50,
'io.element.call.member': 0,
'org.matrix.msc3401.call': 50,
'org.matrix.msc3401.call.member': 0,
'm.call': 50,
'm.call.invite': 50,
'm.call.answer': 0,
'm.call.hangup': 0,
'm.call.candidates': 0,
'm.call.select_answer': 0,
'm.call.reject': 0,
'm.call.negotiate': 0,
},
};
}
/**
* Базовый пресет комнаты секретаря. `isPrivate` переопределяется сервисом по флагу публичности.
* `encrypt` всегда false — иначе секретарь не сможет читать сообщения и транскрибировать.
*/
export const SECRETARY_ROOM_MATRIX: MatrixChatRoomPreset = {
label: 'Комната секретаря',
isPrivate: true,
encrypt: false,
roomType: undefined,
initialState: [],
buildPowerLevels,
};
@@ -23,6 +23,32 @@ export class ChatcoopProjectCommunicationRoomDTO {
displayLabel!: string;
}
/**
* Тип непроектной комнаты: пайщики, совет или комната секретаря.
*/
export enum NonProjectRoomKindGql {
MEMBERS = 'MEMBERS',
COUNCIL = 'COUNCIL',
SECRETARY = 'SECRETARY',
}
registerEnumType(NonProjectRoomKindGql, {
name: 'NonProjectRoomKind',
description: 'Тип комнаты вне проекта: пайщики, совет, комната секретаря',
});
@ObjectType('ChatcoopNonProjectCommunicationRoom')
export class ChatcoopNonProjectCommunicationRoomDTO {
@Field({ description: 'Идентификатор комнаты Matrix' })
matrixRoomId!: string;
@Field({ description: 'Подпись для отображения комнаты' })
displayLabel!: string;
@Field(() => NonProjectRoomKindGql, { description: 'Тип комнаты (пайщики / совет / секретарь)' })
kind!: NonProjectRoomKindGql;
}
@ObjectType('ChatcoopRoomMessageLine')
export class ChatcoopRoomMessageLineDTO {
@Field(() => Float, { description: 'origin_server_ts из Matrix (мс)' })
@@ -0,0 +1,60 @@
import { Field, InputType, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IsBoolean, IsNotEmpty, IsString, MaxLength } from 'class-validator';
/**
* Тип комнаты в реестре ChatCoop. Редактировать (удалять) можно только `SECRETARY`;
* системные (`MEMBERS`/`COUNCIL`) и проектные (`CAPITAL_PROJECT`) защищены.
*/
export enum ManagedRoomKindGql {
MEMBERS = 'MEMBERS',
COUNCIL = 'COUNCIL',
CAPITAL_PROJECT = 'CAPITAL_PROJECT',
SECRETARY = 'SECRETARY',
}
registerEnumType(ManagedRoomKindGql, {
name: 'ManagedRoomKind',
description: 'Тип комнаты: пайщики, совет, проект Capital, комната секретаря',
});
@ObjectType('ChatcoopSecretaryRoom')
export class ChatcoopSecretaryRoomDTO {
@Field({ description: 'Идентификатор комнаты Matrix' })
matrixRoomId!: string;
@Field({ description: 'Название комнаты' })
displayLabel!: string;
@Field(() => ManagedRoomKindGql, { description: 'Тип комнаты' })
kind!: ManagedRoomKindGql;
@Field({ description: 'Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты' })
encrypted!: boolean;
@Field({ description: 'Секретарь присутствует в комнате' })
secretaryInRoom!: boolean;
@Field({ description: 'Можно ли удалить комнату из интерфейса (только комнаты секретаря)' })
editable!: boolean;
}
@InputType('CreateSecretaryRoomInput')
export class CreateSecretaryRoomInputDTO {
@Field({ description: 'Название комнаты' })
@IsString()
@IsNotEmpty()
@MaxLength(240)
displayName!: string;
@Field({ description: 'Публичная комната (любой может войти) либо приватная (вход по приглашению создателя)' })
@IsBoolean()
isPublic!: boolean;
}
@InputType('RemoveSecretaryRoomInput')
export class RemoveSecretaryRoomInputDTO {
@Field({ description: 'Идентификатор комнаты Matrix, которую нужно удалить' })
@IsString()
@IsNotEmpty()
matrixRoomId!: string;
}
@@ -11,19 +11,33 @@ import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
import {
ChatcoopNonProjectCommunicationRoomDTO,
ChatcoopProjectCommunicationRoomDTO,
ChatcoopRoomMessageLineDTO,
GetMaxOriginServerTsForRoomInputDTO,
GetProjectCommunicationRoomsInputDTO,
GetRoomMessagesForUtcDateInputDTO,
ListUtcDatesWithNewRoomMessagesInputDTO,
NonProjectRoomKindGql,
RoomMessageKindGql,
} from '../dto/project-communication.dto';
import type { InterNonProjectRoomKind } from '@coopenomics/inter';
function mapKind(kind: 'text' | 'audio'): RoomMessageKindGql {
return kind === 'text' ? RoomMessageKindGql.TEXT : RoomMessageKindGql.AUDIO;
}
function mapNonProjectKind(kind: InterNonProjectRoomKind): NonProjectRoomKindGql {
switch (kind) {
case 'members':
return NonProjectRoomKindGql.MEMBERS;
case 'council':
return NonProjectRoomKindGql.COUNCIL;
case 'secretary':
return NonProjectRoomKindGql.SECRETARY;
}
}
/**
* Доступ к данным переписки Capital↔Matrix для синхронизации (blago-cli, секретарь).
* Источник — порт `INTER_PROJECT_COMMUNICATION_ARTIFACTS` (пакет inter).
@@ -61,6 +75,24 @@ export class ProjectCommunicationResolver {
}));
}
@Query(() => [ChatcoopNonProjectCommunicationRoomDTO], {
name: 'chatcoopListNonProjectCommunicationRooms',
description: 'Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) — для синхронизации в blago',
})
@AuthRoles(['chairman', 'member', 'user'])
async listNonProjectCommunicationRooms(
@CurrentUser() user: MonoAccountDomainInterface
): Promise<ChatcoopNonProjectCommunicationRoomDTO[]> {
this.ensureComm();
this.logger.debug(`chatcoopListNonProjectCommunicationRooms user=${user.username}`);
const rooms = await this.comm!.listNonProjectCommunicationRooms();
return rooms.map((r) => ({
matrixRoomId: r.matrixRoomId,
displayLabel: r.displayLabel,
kind: mapNonProjectKind(r.kind),
}));
}
@Query(() => [String], {
name: 'chatcoopListUtcDatesWithNewRoomMessages',
description:
@@ -0,0 +1,98 @@
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Logger, UseGuards } from '@nestjs/common';
import { ActiveUserStatusGuard } from '~/application/auth/guards/active-user-status.guard';
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
import { RolesGuard } from '~/application/auth/guards/roles.guard';
import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
import { SecretaryRoomManagementService } from '../services/secretary-room-management.service';
import {
ChatcoopSecretaryRoomDTO,
CreateSecretaryRoomInputDTO,
ManagedRoomKindGql,
RemoveSecretaryRoomInputDTO,
} from '../dto/secretary-room.dto';
import type { ChatcoopManagedMatrixRoomKind } from '../../domain/entities/managed-matrix-room.entity';
import type { ManagedMatrixRoomDomainEntity } from '../../domain/entities/managed-matrix-room.entity';
function mapManagedKind(kind: ChatcoopManagedMatrixRoomKind): ManagedRoomKindGql {
switch (kind) {
case 'members':
return ManagedRoomKindGql.MEMBERS;
case 'council':
return ManagedRoomKindGql.COUNCIL;
case 'capital_project':
return ManagedRoomKindGql.CAPITAL_PROJECT;
case 'secretary':
return ManagedRoomKindGql.SECRETARY;
}
}
function toDto(room: ManagedMatrixRoomDomainEntity): ChatcoopSecretaryRoomDTO {
return {
matrixRoomId: room.matrixRoomId,
displayLabel: room.displayLabel || room.matrixRoomId,
kind: mapManagedKind(room.kind),
encrypted: room.encrypted,
secretaryInRoom: room.secretaryInRoom,
editable: room.kind === 'secretary',
};
}
/**
* Управление комнатами секретаря (стол связи desktop).
* Доступ — председатель и члены совета: они создают комнаты для звонков с секретарём и удаляют свои.
*/
@Resolver()
@UseGuards(GqlJwtAuthGuard, RolesGuard, ActiveUserStatusGuard)
export class SecretaryRoomsResolver {
private readonly logger = new Logger(SecretaryRoomsResolver.name);
constructor(private readonly service: SecretaryRoomManagementService) {}
@Query(() => [ChatcoopSecretaryRoomDTO], {
name: 'chatcoopListSecretaryRooms',
description: 'Все комнаты реестра ChatCoop (системные/проектные — read-only, комнаты секретаря — удаляемые)',
})
@AuthRoles(['chairman', 'member'])
async listSecretaryRooms(
@CurrentUser() user: MonoAccountDomainInterface
): Promise<ChatcoopSecretaryRoomDTO[]> {
this.logger.debug(`chatcoopListSecretaryRooms user=${user.username}`);
const rooms = await this.service.listRooms();
return rooms.map(toDto);
}
@Mutation(() => ChatcoopSecretaryRoomDTO, {
name: 'chatcoopCreateSecretaryRoom',
description: 'Создать комнату с секретарём (публичную или приватную); секретарь подключается сразу',
})
@AuthRoles(['chairman', 'member'])
async createSecretaryRoom(
@CurrentUser() user: MonoAccountDomainInterface,
@Args('data', { type: () => CreateSecretaryRoomInputDTO }) data: CreateSecretaryRoomInputDTO
): Promise<ChatcoopSecretaryRoomDTO> {
this.logger.log(`chatcoopCreateSecretaryRoom user=${user.username} isPublic=${data.isPublic}`);
const room = await this.service.createSecretaryRoom({
creatorUsername: user.username,
creatorRole: user.role,
displayName: data.displayName,
isPublic: data.isPublic,
});
return toDto(room);
}
@Mutation(() => String, {
name: 'chatcoopRemoveSecretaryRoom',
description: 'Удалить комнату секретаря: вывести секретаря и снять комнату с синхронизации (возвращает matrixRoomId)',
})
@AuthRoles(['chairman', 'member'])
async removeSecretaryRoom(
@CurrentUser() user: MonoAccountDomainInterface,
@Args('data', { type: () => RemoveSecretaryRoomInputDTO }) data: RemoveSecretaryRoomInputDTO
): Promise<string> {
this.logger.log(`chatcoopRemoveSecretaryRoom user=${user.username} room=${data.matrixRoomId}`);
return this.service.removeSecretaryRoom(data.matrixRoomId);
}
}
@@ -818,6 +818,48 @@ export class MatrixApiService {
}
}
/**
* Приглашает пользователя в комнату (от имени Matrix-админа, который состоит в комнате).
* Пользователь увидит приглашение и войдёт сам — в отличие от {@link joinRoom} (force-join).
*/
async inviteUser(userId: string, roomId: string): Promise<void> {
try {
const adminToken = await this.loginAdmin();
await this.httpClient.post(
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/invite`,
{ user_id: userId },
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
this.logger.log(`Пользователь ${userId} приглашён в комнату ${roomId}`);
} catch (error: any) {
this.logger.error(
`Не удалось пригласить пользователя ${userId} в комнату ${roomId}: ${JSON.stringify(error?.response?.data)}`
);
throw new Error('Не удалось пригласить пользователя в комнату');
}
}
/**
* Исключает пользователя из комнаты (от имени Matrix-админа с power 100).
* Используется для вывода секретаря из комнаты по требованию председателя/совета.
*/
async kickUser(userId: string, roomId: string, reason?: string): Promise<void> {
try {
const adminToken = await this.loginAdmin();
await this.httpClient.post(
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/kick`,
reason ? { user_id: userId, reason } : { user_id: userId },
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
this.logger.log(`Пользователь ${userId} исключён из комнаты ${roomId}`);
} catch (error: any) {
this.logger.error(
`Не удалось исключить пользователя ${userId} из комнаты ${roomId}: ${JSON.stringify(error?.response?.data)}`
);
throw new Error('Не удалось исключить пользователя из комнаты');
}
}
/**
* Получает текущие права пользователей в комнате
*/
@@ -0,0 +1,148 @@
import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { MatrixApiService } from './matrix-api.service';
import { ChatCoopApplicationService } from './chatcoop-application.service';
import { MatrixUserManagementService } from '../../domain/services/matrix-user-management.service';
import { SECRETARY_ROOM_MATRIX } from '../config/matrix-secretary-room.config';
import {
CHATCOOP_MANAGED_MATRIX_ROOM_REPOSITORY,
type ChatcoopManagedMatrixRoomRepository,
} from '../../domain/repositories/managed-matrix-room.repository';
import {
CHATCOOP_STATE_REPOSITORY,
type ChatcoopStateRepository,
} from '../../domain/repositories/chatcoop-state.repository';
import type { ManagedMatrixRoomDomainEntity } from '../../domain/entities/managed-matrix-room.entity';
export interface CreateSecretaryRoomInput {
/** Логин пайщика-создателя (председатель или член совета) */
creatorUsername: string;
/** Роль создателя в кооперативе — для прав в комнате (chairman / member) */
creatorRole: string;
/** Название комнаты */
displayName: string;
/** true — публичная (любой может войти), false — приватная (вход по приглашению) */
isPublic: boolean;
}
/**
* Управление комнатами секретаря: создание/удаление и список всех комнат реестра.
*
* Принцип безопасности: секретарь присутствует ТОЛЬКО в комнатах, созданных нашим бэкендом
* (kind `secretary`, а также системные members/council и проектные capital_project). Произвольные
* «чужие» Matrix-комнаты сюда не подключаются — Synapse общий на все кооперативы, и force-join в
* чужую комнату был бы дырой.
*/
@Injectable()
export class SecretaryRoomManagementService {
private readonly logger = new Logger(SecretaryRoomManagementService.name);
constructor(
private readonly matrixApi: MatrixApiService,
private readonly chatCoopApplicationService: ChatCoopApplicationService,
private readonly matrixUserManagement: MatrixUserManagementService,
@Inject(CHATCOOP_MANAGED_MATRIX_ROOM_REPOSITORY)
private readonly managedRooms: ChatcoopManagedMatrixRoomRepository,
@Inject(CHATCOOP_STATE_REPOSITORY)
private readonly chatcoopState: ChatcoopStateRepository
) {}
async listRooms(): Promise<ManagedMatrixRoomDomainEntity[]> {
return this.managedRooms.findAll();
}
async createSecretaryRoom(input: CreateSecretaryRoomInput): Promise<ManagedMatrixRoomDomainEntity> {
const st = await this.chatcoopState.getSingleton();
if (!st.isInitialized || !st.spaceId || st.spaceId.trim().length === 0) {
throw new BadRequestException('ChatCoop не инициализирован — нельзя создать комнату секретаря');
}
const secretaryId = st.secretaryMatrixUserId;
if (typeof secretaryId !== 'string' || secretaryId.trim().length === 0) {
throw new BadRequestException('Секретарь не инициализирован — нельзя создать комнату секретаря');
}
const displayName = input.displayName.trim();
if (displayName.length === 0) {
throw new BadRequestException('Название комнаты не может быть пустым');
}
const adminUserId = this.matrixApi.getAdminUserId();
const powerLevels = SECRETARY_ROOM_MATRIX.buildPowerLevels(adminUserId);
const isPrivate = !input.isPublic;
const roomId = await this.matrixApi.createRoom(
displayName.slice(0, 240),
`Комната секретаря — создал ${input.creatorUsername}`,
isPrivate,
SECRETARY_ROOM_MATRIX.roomType,
SECRETARY_ROOM_MATRIX.initialState.length > 0 ? SECRETARY_ROOM_MATRIX.initialState : undefined,
// Комната секретаря всегда незашифрована — иначе секретарь не читает сообщения и не транскрибирует.
false,
powerLevels as Record<string, unknown>
);
await this.matrixApi.addRoomToSpace(st.spaceId.trim(), roomId);
// Force-join только секретарь.
await this.matrixApi.joinRoom(secretaryId.trim(), roomId);
// Создатель сразу входит в свою комнату с правами модератора.
const creatorMatrix = await this.matrixUserManagement.getMatrixUserByCoopUsername(input.creatorUsername);
if (creatorMatrix) {
try {
await this.matrixApi.joinRoom(creatorMatrix.matrixUserId, roomId);
await this.chatCoopApplicationService.applyMembersRoomStylePowerForUser(
creatorMatrix.matrixUserId,
roomId,
input.creatorRole
);
} catch (err) {
this.logger.warn(`Не удалось ввести создателя ${input.creatorUsername} в комнату ${roomId}: ${String(err)}`);
}
} else {
this.logger.warn(`Нет Matrix-аккаунта для создателя ${input.creatorUsername} — он войдёт позже сам`);
}
const room = await this.managedRooms.upsertRoom({
matrixRoomId: roomId,
encrypted: false,
kind: 'secretary',
displayLabel: displayName,
projectHash: null,
secretaryInRoom: true,
});
this.logger.log(
`Создана комната секретаря ${roomId} (${isPrivate ? 'приватная' : 'публичная'}) создателем ${input.creatorUsername}`
);
return room;
}
/**
* Удаляет комнату секретаря: выводит секретаря из Matrix-комнаты и убирает запись из реестра ChatCoop.
* Сама Matrix-комната не уничтожается — её участники сохраняют доступ и историю, но транскрипция/синхронизация прекращаются.
* Разрешено только для kind `secretary` — системные и проектные комнаты так удалять нельзя.
*/
async removeSecretaryRoom(matrixRoomId: string): Promise<string> {
const room = await this.managedRooms.findByMatrixRoomId(matrixRoomId);
if (!room) {
throw new NotFoundException('Комната не найдена в реестре ChatCoop');
}
if (room.kind !== 'secretary') {
throw new BadRequestException('Удалять можно только комнаты секретаря (системные и проектные защищены)');
}
const st = await this.chatcoopState.getSingleton();
const secretaryId = st.secretaryMatrixUserId;
if (typeof secretaryId === 'string' && secretaryId.trim().length > 0) {
try {
await this.matrixApi.kickUser(secretaryId.trim(), matrixRoomId, 'Комната секретаря удалена');
} catch (err) {
this.logger.warn(`Не удалось вывести секретаря из ${matrixRoomId}: ${String(err)}`);
}
}
await this.managedRooms.setSecretaryInRoom(matrixRoomId, false);
await this.managedRooms.deleteByMatrixRoomId(matrixRoomId);
this.logger.log(`Комната секретаря ${matrixRoomId} удалена из реестра`);
return matrixRoomId;
}
}
@@ -13,6 +13,8 @@ import { ChatCoopCalendarResolver } from './application/resolvers/chatcoop-calen
import { ChatCoopCalendarFeedController } from './application/controllers/chatcoop-calendar-feed.controller';
import { TranscriptionResolver } from './application/resolvers/transcription.resolver';
import { ProjectCommunicationResolver } from './application/resolvers/project-communication.resolver';
import { SecretaryRoomsResolver } from './application/resolvers/secretary-rooms.resolver';
import { SecretaryRoomManagementService } from './application/services/secretary-room-management.service';
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
import { ConfigModule } from '@nestjs/config';
import { z } from 'zod';
@@ -527,6 +529,7 @@ export class ChatCoopPlugin extends BaseExtModule {
// Application Services
ChatCoopApplicationService,
CapitalProjectMatrixSyncService,
SecretaryRoomManagementService,
MatrixApiService,
ChatCoopSecretaryMatrixTokenService,
SecretaryAgentService,
@@ -604,6 +607,7 @@ export class ChatCoopPlugin extends BaseExtModule {
TranscriptionResolver,
ActiveUserStatusGuard,
ProjectCommunicationResolver,
SecretaryRoomsResolver,
],
exports: [
ChatCoopPlugin,
@@ -1,7 +1,7 @@
/**
* Тип комнаты Matrix, зарегистрированной в реестре ChatCoop (централизованное хранение).
*/
export type ChatcoopManagedMatrixRoomKind = 'members' | 'council' | 'capital_project';
export type ChatcoopManagedMatrixRoomKind = 'members' | 'council' | 'capital_project' | 'secretary';
/**
* Запись о Matrix-комнате кооператива: источник правды для LiveKit/секретаря; совет при миграции v3 из legacy-конфига.
@@ -24,6 +24,10 @@ export interface ChatcoopManagedMatrixRoomRepository {
findByKind(kind: ChatcoopManagedMatrixRoomKind): Promise<ManagedMatrixRoomDomainEntity[]>;
/** Комнаты проекта Capital (kind capital_project, projectHash задан). */
findByProjectHash(projectHash: string): Promise<ManagedMatrixRoomDomainEntity[]>;
/** Все комнаты реестра (для интерфейса управления присутствием секретаря). */
findAll(): Promise<ManagedMatrixRoomDomainEntity[]>;
/** Комнаты, не привязанные к проекту Capital (members/council/secretary) — для синхронизации в blago. */
findNonProjectCommunicationRooms(): Promise<ManagedMatrixRoomDomainEntity[]>;
/** Комнаты, в которых секретарь может участвовать в звонке и писать plaintext в Matrix */
findEligibleForSecretaryTranscription(): Promise<ManagedMatrixRoomDomainEntity[]>;
/** Обновить флаг членства секретаря (после успешного join или проверки Matrix) */
@@ -1,6 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import type {
InterCompletedCallTranscriptionHead,
InterNonProjectCommunicationRoomRef,
InterNonProjectRoomKind,
InterProjectCommunicationArtifactsPort,
InterProjectCommunicationRoomRef,
InterRoomMessageLine,
@@ -48,6 +50,19 @@ export class ChatcoopInterProjectCommunicationArtifactsAdapter implements InterP
}));
}
async listNonProjectCommunicationRooms(): Promise<InterNonProjectCommunicationRoomRef[]> {
const rooms = await this.managedRooms.findNonProjectCommunicationRooms();
return rooms
.filter((r): r is typeof r & { kind: InterNonProjectRoomKind } =>
r.kind === 'members' || r.kind === 'council' || r.kind === 'secretary'
)
.map((r) => ({
matrixRoomId: r.matrixRoomId,
displayLabel: r.displayLabel || r.matrixRoomId,
kind: r.kind,
}));
}
async listUtcDatesWithNewMessages(
matrixRoomId: string,
afterOriginServerTsExclusive: number
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Not, Repository } from 'typeorm';
import type {
ChatcoopManagedMatrixRoomRepository,
UpsertManagedMatrixRoomInput,
@@ -56,6 +56,16 @@ export class ManagedMatrixRoomTypeormRepository implements ChatcoopManagedMatrix
return rows.map(ManagedMatrixRoomMapper.toDomain);
}
async findAll(): Promise<ManagedMatrixRoomDomainEntity[]> {
const rows = await this.repository.find();
return rows.map(ManagedMatrixRoomMapper.toDomain);
}
async findNonProjectCommunicationRooms(): Promise<ManagedMatrixRoomDomainEntity[]> {
const rows = await this.repository.find({ where: { roomKind: Not('capital_project') } });
return rows.map(ManagedMatrixRoomMapper.toDomain);
}
async findEligibleForSecretaryTranscription(): Promise<ManagedMatrixRoomDomainEntity[]> {
const rows = await this.repository.find({ where: { encrypted: false } });
return rows.map(ManagedMatrixRoomMapper.toDomain);
@@ -64,6 +64,29 @@ export class MeetTrackerService {
this.logger.info('MeetTrackerService инициализирован');
}
/**
* Сохраняет состояние трекинга собраний read-modify-write по СВЕЖЕМУ config.
*
* `this.pluginConfig` — снимок, захваченный при initialize() (boot), а
* cron-проверка крутится часами; update() заменяет весь config JSONB целиком.
* Если писать захваченный снимок, затрутся поля, записанные в БД после boot
* другими сервисами (онбординг-флаги и т.п.) — это и есть наблюдавшийся
* сброс онбординга. Поэтому берём актуальный config из БД и накладываем
* только поля, которыми владеет meet-tracker.
*/
private async persistTrackedState(): Promise<void> {
const fresh = await this.extensionRepository.findByName(this.extensionName);
const baseConfig = fresh?.config ?? this.pluginConfig.config;
const nextConfig: IConfig = {
...baseConfig,
trackedMeets: this.pluginConfig.config.trackedMeets,
closedMeetIds: this.pluginConfig.config.closedMeetIds,
lastCheckTimestamp: this.pluginConfig.config.lastCheckTimestamp,
};
await this.extensionRepository.update({ name: this.extensionName, config: nextConfig });
this.pluginConfig = { ...this.pluginConfig, config: nextConfig };
}
// Приватная функция для обновления trackedMeet на основе свежих данных
private getUpdatedTrackedMeet(trackedMeet: TrackedMeet, meetData: any, extendedStatus: string): TrackedMeet {
return {
@@ -145,7 +168,7 @@ export class MeetTrackerService {
this.pluginConfig.config.trackedMeets[trackedMeetIndex].notifications.endNotification = true;
if (!closedMeetIds.includes(meetID)) {
closedMeetIds.push(meetID);
await this.extensionRepository.update(this.pluginConfig);
await this.persistTrackedState();
}
}
this.logger.info(`Удаление закрытого собрания ${meetHash} (№${meetID}) из списка отслеживаемых`);
@@ -310,7 +333,7 @@ export class MeetTrackerService {
this.pluginConfig.config.lastCheckTimestamp = now.toISOString();
// Сохраняем изменения в конфигурации
await this.extensionRepository.update(this.pluginConfig);
await this.persistTrackedState();
} catch (error: any) {
this.logger.error(`Ошибка при проверке собраний: ${error.message}`, error.stack);
}
@@ -215,8 +215,18 @@ export class PowerupPlugin extends BaseExtModule implements OnModuleDestroy {
await this.blockchainPort.powerUp(username, quantity);
this.plugin.config.lastDailyReplenishmentDate = new Date().toISOString();
await this.extensionRepository.update(this.plugin);
// read-modify-write по СВЕЖЕМУ config: daily-cron держит in-memory снимок
// `this.plugin` с момента boot, а update() заменяет весь config JSONB
// целиком. Перезапись устаревшего снимка стёрла бы поля, записанные за
// сутки другими сервисами (онбординг и т.п.). Берём актуальный config и
// трогаем только lastDailyReplenishmentDate.
const fresh = await this.extensionRepository.findByName(this.name);
const nextConfig = {
...(fresh?.config ?? this.plugin.config),
lastDailyReplenishmentDate: new Date().toISOString(),
};
await this.extensionRepository.update({ name: this.name, config: nextConfig });
this.plugin = { ...this.plugin, config: nextConfig };
await this.log({
type: 'daily',
+31 -2
View File
@@ -341,6 +341,9 @@ export const AllTypesProps: Record<string,any> = {
},
CreateProjectPropertyInput:{
},
CreateSecretaryRoomInput:{
},
CreateSovietIndividualDataInput:{
passport:"PassportInput"
@@ -617,6 +620,7 @@ export const AllTypesProps: Record<string,any> = {
MakeClearanceInput:{
document:"SignedDigitalDocumentInput"
},
ManagedRoomKind: "enum" as const,
MarkReportPeriodInput:{
mark:"ReportSubmissionMark",
reportType:"ReportType"
@@ -899,8 +903,14 @@ export const AllTypesProps: Record<string,any> = {
chatcoopCreateCalendarEvent:{
data:"CreateChatCoopCalendarEventInput"
},
chatcoopCreateSecretaryRoom:{
data:"CreateSecretaryRoomInput"
},
chatcoopDeleteCalendarEvent:{
},
chatcoopRemoveSecretaryRoom:{
data:"RemoveSecretaryRoomInput"
},
chatcoopUpdateCalendarEvent:{
data:"UpdateChatCoopCalendarEventInput"
@@ -1214,6 +1224,7 @@ export const AllTypesProps: Record<string,any> = {
input:"WalmoveInput"
}
},
NonProjectRoomKind: "enum" as const,
NotificationWorkflowRecipientInput:{
},
@@ -1643,6 +1654,9 @@ export const AllTypesProps: Record<string,any> = {
statement:"ParticipantApplicationSignedDocumentInput",
user_agreement:"SignedDigitalDocumentInput",
wallet_agreement:"SignedDigitalDocumentInput"
},
RemoveSecretaryRoomInput:{
},
ReportHistoryFilterInput:{
reportType:"ReportType"
@@ -2920,6 +2934,11 @@ export const ReturnTypes: Record<string,any> = {
displayLabel:"String",
matrixRoomId:"String"
},
ChatcoopNonProjectCommunicationRoom:{
displayLabel:"String",
kind:"NonProjectRoomKind",
matrixRoomId:"String"
},
ChatcoopProjectCommunicationRoom:{
displayLabel:"String",
matrixRoomId:"String"
@@ -2931,6 +2950,14 @@ export const ReturnTypes: Record<string,any> = {
kind:"RoomMessageKind",
originServerTs:"Float"
},
ChatcoopSecretaryRoom:{
displayLabel:"String",
editable:"Boolean",
encrypted:"Boolean",
kind:"ManagedRoomKind",
matrixRoomId:"String",
secretaryInRoom:"Boolean"
},
ContactsDTO:{
chairman:"PublicChairman",
details:"OrganizationDetails",
@@ -3343,7 +3370,6 @@ export const ReturnTypes: Record<string,any> = {
},
Ledger2Wallet:{
available:"String",
blocked:"String",
id:"String",
name:"String"
},
@@ -3560,7 +3586,9 @@ export const ReturnTypes: Record<string,any> = {
chatcoopCreateAccount:"Boolean",
chatcoopCreateCalendarEvent:"ChatCoopCalendarEvent",
chatcoopCreateCalendarIcsSubscription:"ChatCoopCalendarIcsUrlResponse",
chatcoopCreateSecretaryRoom:"ChatcoopSecretaryRoom",
chatcoopDeleteCalendarEvent:"Boolean",
chatcoopRemoveSecretaryRoom:"String",
chatcoopUpdateCalendarEvent:"ChatCoopCalendarEvent",
chatcoopUpdateTranscriptionMemo:"CallTranscription",
completeCapitalOnboardingStep:"CapitalOnboardingState",
@@ -4041,7 +4069,6 @@ export const ReturnTypes: Record<string,any> = {
agreement_id:"ID",
available:"String",
blockNum:"Float",
blocked:"String",
coopname:"String",
id:"ID",
membership_contribution:"String",
@@ -4130,7 +4157,9 @@ export const ReturnTypes: Record<string,any> = {
chatcoopGetTranscriptions:"CallTranscription",
chatcoopListCalendarEvents:"ChatCoopCalendarEvent",
chatcoopListCalendarRooms:"ChatCoopCalendarRoomOption",
chatcoopListNonProjectCommunicationRooms:"ChatcoopNonProjectCommunicationRoom",
chatcoopListProjectCommunicationRooms:"ChatcoopProjectCommunicationRoom",
chatcoopListSecretaryRooms:"ChatcoopSecretaryRoom",
chatcoopListUtcDatesWithNewRoomMessages:"String",
checkReportReadiness:"ReportReadinessView",
cooperativeAgreements:"CoopAgreement",
+237 -23
View File
@@ -3851,6 +3851,16 @@ export type ValueTypes = {
matrixRoomId?:boolean | `@${string}`,
__typename?: boolean | `@${string}`,
['...on ChatCoopCalendarRoomOption']?: Omit<ValueTypes["ChatCoopCalendarRoomOption"], "...on ChatCoopCalendarRoomOption">
}>;
["ChatcoopNonProjectCommunicationRoom"]: AliasType<{
/** Подпись для отображения комнаты */
displayLabel?:boolean | `@${string}`,
/** Тип комнаты (пайщики / совет / секретарь) */
kind?:boolean | `@${string}`,
/** Идентификатор комнаты Matrix */
matrixRoomId?:boolean | `@${string}`,
__typename?: boolean | `@${string}`,
['...on ChatcoopNonProjectCommunicationRoom']?: Omit<ValueTypes["ChatcoopNonProjectCommunicationRoom"], "...on ChatcoopNonProjectCommunicationRoom">
}>;
["ChatcoopProjectCommunicationRoom"]: AliasType<{
/** Подпись для отображения (комната / проект Capital) */
@@ -3872,6 +3882,22 @@ export type ValueTypes = {
originServerTs?:boolean | `@${string}`,
__typename?: boolean | `@${string}`,
['...on ChatcoopRoomMessageLine']?: Omit<ValueTypes["ChatcoopRoomMessageLine"], "...on ChatcoopRoomMessageLine">
}>;
["ChatcoopSecretaryRoom"]: AliasType<{
/** Название комнаты */
displayLabel?:boolean | `@${string}`,
/** Можно ли удалить комнату из интерфейса (только комнаты секретаря) */
editable?:boolean | `@${string}`,
/** Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты */
encrypted?:boolean | `@${string}`,
/** Тип комнаты */
kind?:boolean | `@${string}`,
/** Идентификатор комнаты Matrix */
matrixRoomId?:boolean | `@${string}`,
/** Секретарь присутствует в комнате */
secretaryInRoom?:boolean | `@${string}`,
__typename?: boolean | `@${string}`,
['...on ChatcoopSecretaryRoom']?: Omit<ValueTypes["ChatcoopSecretaryRoom"], "...on ChatcoopSecretaryRoom">
}>;
["CheckMatrixUsernameInput"]: {
username: string | Variable<any, string>
@@ -4604,6 +4630,12 @@ export type ValueTypes = {
property_hash: string | Variable<any, string>,
/** Имя пользователя */
username: string | Variable<any, string>
};
["CreateSecretaryRoomInput"]: {
/** Название комнаты */
displayName: string | Variable<any, string>,
/** Публичная комната (любой может войти) либо приватная (вход по приглашению создателя) */
isPublic: boolean | Variable<any, string>
};
["CreateSovietIndividualDataInput"]: {
/** Дата рождения */
@@ -5921,6 +5953,8 @@ export type ValueTypes = {
['...on IndividualCertificate']?: Omit<ValueTypes["IndividualCertificate"], "...on IndividualCertificate">
}>;
["Init"]: {
/** Признак того, что инициализация выполняется со стороны провайдера. При true coopback ставит init_by_server=true (org_data становится readonly для пользовательского визарда). Поле передаёт provider в callInitSystemMutation. */
is_server_init?: boolean | undefined | null | Variable<any, string>,
/** Объект организации кооператива, которая обслуживает данный экземпляр программного обеспечения MONO */
organization_data: ValueTypes["CreateInitOrganizationDataInput"] | Variable<any, string>
};
@@ -6052,8 +6086,6 @@ export type ValueTypes = {
["Ledger2Wallet"]: AliasType<{
/** Доступный баланс */
available?:boolean | `@${string}`,
/** Заблокированный баланс */
blocked?:boolean | `@${string}`,
/** eosio::name-идентификатор кошелька (w.<contract>.<waltype>) */
id?:boolean | `@${string}`,
/** Название кошелька */
@@ -6143,6 +6175,8 @@ export type ValueTypes = {
/** Имя пользователя */
username: string | Variable<any, string>
};
/** Тип комнаты: пайщики, совет, проект Capital, комната секретаря */
["ManagedRoomKind"]:ManagedRoomKind;
["MarkReportPeriodInput"]: {
mark?: ValueTypes["ReportSubmissionMark"] | undefined | null | Variable<any, string>,
period?: number | undefined | null | Variable<any, string>,
@@ -6466,7 +6500,9 @@ chatcoopCreateCalendarEvent?: [{ data: ValueTypes["CreateChatCoopCalendarEventIn
Требуемые роли: chairman, member, user. */
chatcoopCreateCalendarIcsSubscription?:ValueTypes["ChatCoopCalendarIcsUrlResponse"],
chatcoopCreateSecretaryRoom?: [{ data: ValueTypes["CreateSecretaryRoomInput"] | Variable<any, string>},ValueTypes["ChatcoopSecretaryRoom"]],
chatcoopDeleteCalendarEvent?: [{ id: string | Variable<any, string>},boolean | `@${string}`],
chatcoopRemoveSecretaryRoom?: [{ data: ValueTypes["RemoveSecretaryRoomInput"] | Variable<any, string>},boolean | `@${string}`],
chatcoopUpdateCalendarEvent?: [{ data: ValueTypes["UpdateChatCoopCalendarEventInput"] | Variable<any, string>},ValueTypes["ChatCoopCalendarEvent"]],
chatcoopUpdateTranscriptionMemo?: [{ data: ValueTypes["UpdateCallTranscriptionMemoInput"] | Variable<any, string>},ValueTypes["CallTranscription"]],
completeCapitalOnboardingStep?: [{ data: ValueTypes["CapitalOnboardingStepInput"] | Variable<any, string>},ValueTypes["CapitalOnboardingState"]],
@@ -6566,6 +6602,8 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable<any, string>},V
__typename?: boolean | `@${string}`,
['...on Mutation']?: Omit<ValueTypes["Mutation"], "...on Mutation">
}>;
/** Тип комнаты вне проекта: пайщики, совет, комната секретаря */
["NonProjectRoomKind"]:NonProjectRoomKind;
["NotificationWorkflowRecipientInput"]: {
/** Username получателя */
username: string | Variable<any, string>
@@ -7503,8 +7541,6 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable<any, string>},V
available?:boolean | `@${string}`,
/** Номер блока последнего обновления */
blockNum?:boolean | `@${string}`,
/** Заблокированный баланс (формат: "100.0000 RUB") */
blocked?:boolean | `@${string}`,
/** Имя кооператива */
coopname?:boolean | `@${string}`,
/** Уникальный идентификатор кошелька в блокчейне */
@@ -7776,7 +7812,15 @@ chatcoopGetTranscriptions?: [{ data?: ValueTypes["GetTranscriptionsInput"] | und
Требуемые роли: chairman, member. */
chatcoopListCalendarRooms?:ValueTypes["ChatCoopCalendarRoomOption"],
/** Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) для синхронизации в blago
Требуемые роли: chairman, member, user. */
chatcoopListNonProjectCommunicationRooms?:ValueTypes["ChatcoopNonProjectCommunicationRoom"],
chatcoopListProjectCommunicationRooms?: [{ data: ValueTypes["GetProjectCommunicationRoomsInput"] | Variable<any, string>},ValueTypes["ChatcoopProjectCommunicationRoom"]],
/** Все комнаты реестра ChatCoop (системные/проектные read-only, комнаты секретаря удаляемые)
Требуемые роли: chairman, member. */
chatcoopListSecretaryRooms?:ValueTypes["ChatcoopSecretaryRoom"],
chatcoopListUtcDatesWithNewRoomMessages?: [{ data: ValueTypes["ListUtcDatesWithNewRoomMessagesInput"] | Variable<any, string>},boolean | `@${string}`],
checkReportReadiness?: [{ reportType: ValueTypes["ReportType"] | Variable<any, string>},ValueTypes["ReportReadinessView"]],
cooperativeAgreements?: [{ coopname: string | Variable<any, string>},ValueTypes["CoopAgreement"]],
@@ -8049,6 +8093,10 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
__typename?: boolean | `@${string}`,
['...on RegistrationProgram']?: Omit<ValueTypes["RegistrationProgram"], "...on RegistrationProgram">
}>;
["RemoveSecretaryRoomInput"]: {
/** Идентификатор комнаты Matrix, которую нужно удалить */
matrixRoomId: string | Variable<any, string>
};
["ReportCalendarPeriodEntry"]: AliasType<{
dueDate?:boolean | `@${string}`,
dueMonth?:boolean | `@${string}`,
@@ -9118,7 +9166,7 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
};
["UpdateCallTranscriptionMemoInput"]: {
id: string | Variable<any, string>,
/** Текст заметки (до 4000 символов) */
/** Текст заметки */
memo: string | Variable<any, string>
};
["UpdateChatCoopCalendarEventInput"]: {
@@ -12196,6 +12244,15 @@ export type ResolverInputTypes = {
displayLabel?:boolean | `@${string}`,
matrixRoomId?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["ChatcoopNonProjectCommunicationRoom"]: AliasType<{
/** Подпись для отображения комнаты */
displayLabel?:boolean | `@${string}`,
/** Тип комнаты (пайщики / совет / секретарь) */
kind?:boolean | `@${string}`,
/** Идентификатор комнаты Matrix */
matrixRoomId?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["ChatcoopProjectCommunicationRoom"]: AliasType<{
/** Подпись для отображения (комната / проект Capital) */
@@ -12215,6 +12272,21 @@ export type ResolverInputTypes = {
/** origin_server_ts из Matrix (мс) */
originServerTs?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["ChatcoopSecretaryRoom"]: AliasType<{
/** Название комнаты */
displayLabel?:boolean | `@${string}`,
/** Можно ли удалить комнату из интерфейса (только комнаты секретаря) */
editable?:boolean | `@${string}`,
/** Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты */
encrypted?:boolean | `@${string}`,
/** Тип комнаты */
kind?:boolean | `@${string}`,
/** Идентификатор комнаты Matrix */
matrixRoomId?:boolean | `@${string}`,
/** Секретарь присутствует в комнате */
secretaryInRoom?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["CheckMatrixUsernameInput"]: {
username: string
@@ -12942,6 +13014,12 @@ export type ResolverInputTypes = {
property_hash: string,
/** Имя пользователя */
username: string
};
["CreateSecretaryRoomInput"]: {
/** Название комнаты */
displayName: string,
/** Публичная комната (любой может войти) либо приватная (вход по приглашению создателя) */
isPublic: boolean
};
["CreateSovietIndividualDataInput"]: {
/** Дата рождения */
@@ -14227,6 +14305,8 @@ export type ResolverInputTypes = {
__typename?: boolean | `@${string}`
}>;
["Init"]: {
/** Признак того, что инициализация выполняется со стороны провайдера. При true coopback ставит init_by_server=true (org_data становится readonly для пользовательского визарда). Поле передаёт provider в callInitSystemMutation. */
is_server_init?: boolean | undefined | null,
/** Объект организации кооператива, которая обслуживает данный экземпляр программного обеспечения MONO */
organization_data: ResolverInputTypes["CreateInitOrganizationDataInput"]
};
@@ -14350,8 +14430,6 @@ export type ResolverInputTypes = {
["Ledger2Wallet"]: AliasType<{
/** Доступный баланс */
available?:boolean | `@${string}`,
/** Заблокированный баланс */
blocked?:boolean | `@${string}`,
/** eosio::name-идентификатор кошелька (w.<contract>.<waltype>) */
id?:boolean | `@${string}`,
/** Название кошелька */
@@ -14437,6 +14515,8 @@ export type ResolverInputTypes = {
/** Имя пользователя */
username: string
};
/** Тип комнаты: пайщики, совет, проект Capital, комната секретаря */
["ManagedRoomKind"]:ManagedRoomKind;
["MarkReportPeriodInput"]: {
mark?: ResolverInputTypes["ReportSubmissionMark"] | undefined | null,
period?: number | undefined | null,
@@ -14751,7 +14831,9 @@ chatcoopCreateCalendarEvent?: [{ data: ResolverInputTypes["CreateChatCoopCalenda
Требуемые роли: chairman, member, user. */
chatcoopCreateCalendarIcsSubscription?:ResolverInputTypes["ChatCoopCalendarIcsUrlResponse"],
chatcoopCreateSecretaryRoom?: [{ data: ResolverInputTypes["CreateSecretaryRoomInput"]},ResolverInputTypes["ChatcoopSecretaryRoom"]],
chatcoopDeleteCalendarEvent?: [{ id: string},boolean | `@${string}`],
chatcoopRemoveSecretaryRoom?: [{ data: ResolverInputTypes["RemoveSecretaryRoomInput"]},boolean | `@${string}`],
chatcoopUpdateCalendarEvent?: [{ data: ResolverInputTypes["UpdateChatCoopCalendarEventInput"]},ResolverInputTypes["ChatCoopCalendarEvent"]],
chatcoopUpdateTranscriptionMemo?: [{ data: ResolverInputTypes["UpdateCallTranscriptionMemoInput"]},ResolverInputTypes["CallTranscription"]],
completeCapitalOnboardingStep?: [{ data: ResolverInputTypes["CapitalOnboardingStepInput"]},ResolverInputTypes["CapitalOnboardingState"]],
@@ -14850,6 +14932,8 @@ voteOnAnnualGeneralMeet?: [{ data: ResolverInputTypes["VoteOnAnnualGeneralMeetIn
walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputTypes["Ledger2AdjustmentResult"]],
__typename?: boolean | `@${string}`
}>;
/** Тип комнаты вне проекта: пайщики, совет, комната секретаря */
["NonProjectRoomKind"]:NonProjectRoomKind;
["NotificationWorkflowRecipientInput"]: {
/** Username получателя */
username: string
@@ -15737,8 +15821,6 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType
available?:boolean | `@${string}`,
/** Номер блока последнего обновления */
blockNum?:boolean | `@${string}`,
/** Заблокированный баланс (формат: "100.0000 RUB") */
blocked?:boolean | `@${string}`,
/** Имя кооператива */
coopname?:boolean | `@${string}`,
/** Уникальный идентификатор кошелька в блокчейне */
@@ -16006,7 +16088,15 @@ chatcoopGetTranscriptions?: [{ data?: ResolverInputTypes["GetTranscriptionsInput
Требуемые роли: chairman, member. */
chatcoopListCalendarRooms?:ResolverInputTypes["ChatCoopCalendarRoomOption"],
/** Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) для синхронизации в blago
Требуемые роли: chairman, member, user. */
chatcoopListNonProjectCommunicationRooms?:ResolverInputTypes["ChatcoopNonProjectCommunicationRoom"],
chatcoopListProjectCommunicationRooms?: [{ data: ResolverInputTypes["GetProjectCommunicationRoomsInput"]},ResolverInputTypes["ChatcoopProjectCommunicationRoom"]],
/** Все комнаты реестра ChatCoop (системные/проектные read-only, комнаты секретаря удаляемые)
Требуемые роли: chairman, member. */
chatcoopListSecretaryRooms?:ResolverInputTypes["ChatcoopSecretaryRoom"],
chatcoopListUtcDatesWithNewRoomMessages?: [{ data: ResolverInputTypes["ListUtcDatesWithNewRoomMessagesInput"]},boolean | `@${string}`],
checkReportReadiness?: [{ reportType: ResolverInputTypes["ReportType"]},ResolverInputTypes["ReportReadinessView"]],
cooperativeAgreements?: [{ coopname: string},ResolverInputTypes["CoopAgreement"]],
@@ -16272,6 +16362,10 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
title?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["RemoveSecretaryRoomInput"]: {
/** Идентификатор комнаты Matrix, которую нужно удалить */
matrixRoomId: string
};
["ReportCalendarPeriodEntry"]: AliasType<{
dueDate?:boolean | `@${string}`,
dueMonth?:boolean | `@${string}`,
@@ -17311,7 +17405,7 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
};
["UpdateCallTranscriptionMemoInput"]: {
id: string,
/** Текст заметки (до 4000 символов) */
/** Текст заметки */
memo: string
};
["UpdateChatCoopCalendarEventInput"]: {
@@ -20307,6 +20401,14 @@ export type ModelTypes = {
["ChatCoopCalendarRoomOption"]: {
displayLabel: string,
matrixRoomId: string
};
["ChatcoopNonProjectCommunicationRoom"]: {
/** Подпись для отображения комнаты */
displayLabel: string,
/** Тип комнаты (пайщики / совет / секретарь) */
kind: ModelTypes["NonProjectRoomKind"],
/** Идентификатор комнаты Matrix */
matrixRoomId: string
};
["ChatcoopProjectCommunicationRoom"]: {
/** Подпись для отображения (комната / проект Capital) */
@@ -20324,6 +20426,20 @@ export type ModelTypes = {
kind: ModelTypes["RoomMessageKind"],
/** origin_server_ts из Matrix (мс) */
originServerTs: number
};
["ChatcoopSecretaryRoom"]: {
/** Название комнаты */
displayLabel: string,
/** Можно ли удалить комнату из интерфейса (только комнаты секретаря) */
editable: boolean,
/** Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты */
encrypted: boolean,
/** Тип комнаты */
kind: ModelTypes["ManagedRoomKind"],
/** Идентификатор комнаты Matrix */
matrixRoomId: string,
/** Секретарь присутствует в комнате */
secretaryInRoom: boolean
};
["CheckMatrixUsernameInput"]: {
username: string
@@ -21043,6 +21159,12 @@ export type ModelTypes = {
property_hash: string,
/** Имя пользователя */
username: string
};
["CreateSecretaryRoomInput"]: {
/** Название комнаты */
displayName: string,
/** Публичная комната (любой может войти) либо приватная (вход по приглашению создателя) */
isPublic: boolean
};
["CreateSovietIndividualDataInput"]: {
/** Дата рождения */
@@ -22291,6 +22413,8 @@ export type ModelTypes = {
username: string
};
["Init"]: {
/** Признак того, что инициализация выполняется со стороны провайдера. При true coopback ставит init_by_server=true (org_data становится readonly для пользовательского визарда). Поле передаёт provider в callInitSystemMutation. */
is_server_init?: boolean | undefined | null,
/** Объект организации кооператива, которая обслуживает данный экземпляр программного обеспечения MONO */
organization_data: ModelTypes["CreateInitOrganizationDataInput"]
};
@@ -22402,8 +22526,6 @@ export type ModelTypes = {
["Ledger2Wallet"]: {
/** Доступный баланс */
available: string,
/** Заблокированный баланс */
blocked: string,
/** eosio::name-идентификатор кошелька (w.<contract>.<waltype>) */
id: string,
/** Название кошелька */
@@ -22483,6 +22605,7 @@ export type ModelTypes = {
/** Имя пользователя */
username: string
};
["ManagedRoomKind"]:ManagedRoomKind;
["MarkReportPeriodInput"]: {
mark?: ModelTypes["ReportSubmissionMark"] | undefined | null,
period?: number | undefined | null,
@@ -23027,17 +23150,25 @@ export type ModelTypes = {
Требуемые роли: chairman, member, user. */
chatcoopCreateCalendarIcsSubscription: ModelTypes["ChatCoopCalendarIcsUrlResponse"],
/** Создать комнату с секретарём (публичную или приватную); секретарь подключается сразу
Требуемые роли: chairman, member. */
chatcoopCreateSecretaryRoom: ModelTypes["ChatcoopSecretaryRoom"],
/** Удалить событие календаря
Требуемые роли: chairman, member. */
chatcoopDeleteCalendarEvent: boolean,
/** Удалить комнату секретаря: вывести секретаря и снять комнату с синхронизации (возвращает matrixRoomId)
Требуемые роли: chairman, member. */
chatcoopRemoveSecretaryRoom: string,
/** Обновить событие календаря
Требуемые роли: chairman, member. */
chatcoopUpdateCalendarEvent: ModelTypes["ChatCoopCalendarEvent"],
/** Обновить заметку (memo) к транскрипции звонка
Требуемые роли: chairman, member, user. */
Требуемые роли: chairman, member. */
chatcoopUpdateTranscriptionMemo: ModelTypes["CallTranscription"],
/** Выполнить шаг онбординга capital (создание предложения повестки)
@@ -23356,6 +23487,7 @@ export type ModelTypes = {
Требуемые роли: chairman. */
walmoveWallets: ModelTypes["Ledger2AdjustmentResult"]
};
["NonProjectRoomKind"]:NonProjectRoomKind;
["NotificationWorkflowRecipientInput"]: {
/** Username получателя */
username: string
@@ -24176,8 +24308,6 @@ export type ModelTypes = {
available: string,
/** Номер блока последнего обновления */
blockNum?: number | undefined | null,
/** Заблокированный баланс (формат: "100.0000 RUB") */
blocked: string,
/** Имя кооператива */
coopname: string,
/** Уникальный идентификатор кошелька в блокчейне */
@@ -24520,10 +24650,18 @@ export type ModelTypes = {
Требуемые роли: chairman, member. */
chatcoopListCalendarRooms: Array<ModelTypes["ChatCoopCalendarRoomOption"]>,
/** Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) для синхронизации в blago
Требуемые роли: chairman, member, user. */
chatcoopListNonProjectCommunicationRooms: Array<ModelTypes["ChatcoopNonProjectCommunicationRoom"]>,
/** Комнаты Matrix, привязанные к проекту Capital (реестр ChatCoop)
Требуемые роли: chairman, member, user. */
chatcoopListProjectCommunicationRooms: Array<ModelTypes["ChatcoopProjectCommunicationRoom"]>,
/** Все комнаты реестра ChatCoop (системные/проектные read-only, комнаты секретаря удаляемые)
Требуемые роли: chairman, member. */
chatcoopListSecretaryRooms: Array<ModelTypes["ChatcoopSecretaryRoom"]>,
/** UTC-даты (YYYY-MM-DD), в которых есть сообщения новее afterOriginServerTsExclusive, для комнаты Matrix
Требуемые роли: chairman, member, user. */
@@ -24896,6 +25034,10 @@ export type ModelTypes = {
requirements?: string | undefined | null,
/** Название программы для отображения */
title: string
};
["RemoveSecretaryRoomInput"]: {
/** Идентификатор комнаты Matrix, которую нужно удалить */
matrixRoomId: string
};
["ReportCalendarPeriodEntry"]: {
dueDate: string,
@@ -25899,7 +26041,7 @@ export type ModelTypes = {
};
["UpdateCallTranscriptionMemoInput"]: {
id: string,
/** Текст заметки (до 4000 символов) */
/** Текст заметки */
memo: string
};
["UpdateChatCoopCalendarEventInput"]: {
@@ -29036,6 +29178,16 @@ export type GraphQLTypes = {
displayLabel: string,
matrixRoomId: string,
['...on ChatCoopCalendarRoomOption']: Omit<GraphQLTypes["ChatCoopCalendarRoomOption"], "...on ChatCoopCalendarRoomOption">
};
["ChatcoopNonProjectCommunicationRoom"]: {
__typename: "ChatcoopNonProjectCommunicationRoom",
/** Подпись для отображения комнаты */
displayLabel: string,
/** Тип комнаты (пайщики / совет / секретарь) */
kind: GraphQLTypes["NonProjectRoomKind"],
/** Идентификатор комнаты Matrix */
matrixRoomId: string,
['...on ChatcoopNonProjectCommunicationRoom']: Omit<GraphQLTypes["ChatcoopNonProjectCommunicationRoom"], "...on ChatcoopNonProjectCommunicationRoom">
};
["ChatcoopProjectCommunicationRoom"]: {
__typename: "ChatcoopProjectCommunicationRoom",
@@ -29057,6 +29209,22 @@ export type GraphQLTypes = {
/** origin_server_ts из Matrix (мс) */
originServerTs: number,
['...on ChatcoopRoomMessageLine']: Omit<GraphQLTypes["ChatcoopRoomMessageLine"], "...on ChatcoopRoomMessageLine">
};
["ChatcoopSecretaryRoom"]: {
__typename: "ChatcoopSecretaryRoom",
/** Название комнаты */
displayLabel: string,
/** Можно ли удалить комнату из интерфейса (только комнаты секретаря) */
editable: boolean,
/** Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты */
encrypted: boolean,
/** Тип комнаты */
kind: GraphQLTypes["ManagedRoomKind"],
/** Идентификатор комнаты Matrix */
matrixRoomId: string,
/** Секретарь присутствует в комнате */
secretaryInRoom: boolean,
['...on ChatcoopSecretaryRoom']: Omit<GraphQLTypes["ChatcoopSecretaryRoom"], "...on ChatcoopSecretaryRoom">
};
["CheckMatrixUsernameInput"]: {
username: string
@@ -29789,6 +29957,12 @@ export type GraphQLTypes = {
property_hash: string,
/** Имя пользователя */
username: string
};
["CreateSecretaryRoomInput"]: {
/** Название комнаты */
displayName: string,
/** Публичная комната (любой может войти) либо приватная (вход по приглашению создателя) */
isPublic: boolean
};
["CreateSovietIndividualDataInput"]: {
/** Дата рождения */
@@ -31106,7 +31280,9 @@ export type GraphQLTypes = {
['...on IndividualCertificate']: Omit<GraphQLTypes["IndividualCertificate"], "...on IndividualCertificate">
};
["Init"]: {
/** Объект организации кооператива, которая обслуживает данный экземпляр программного обеспечения MONO */
/** Признак того, что инициализация выполняется со стороны провайдера. При true coopback ставит init_by_server=true (org_data становится readonly для пользовательского визарда). Поле передаёт provider в callInitSystemMutation. */
is_server_init?: boolean | undefined | null,
/** Объект организации кооператива, которая обслуживает данный экземпляр программного обеспечения MONO */
organization_data: GraphQLTypes["CreateInitOrganizationDataInput"]
};
["Install"]: {
@@ -31238,8 +31414,6 @@ export type GraphQLTypes = {
__typename: "Ledger2Wallet",
/** Доступный баланс */
available: string,
/** Заблокированный баланс */
blocked: string,
/** eosio::name-идентификатор кошелька (w.<contract>.<waltype>) */
id: string,
/** Название кошелька */
@@ -31328,6 +31502,8 @@ export type GraphQLTypes = {
/** Имя пользователя */
username: string
};
/** Тип комнаты: пайщики, совет, проект Capital, комната секретаря */
["ManagedRoomKind"]: ManagedRoomKind;
["MarkReportPeriodInput"]: {
mark?: GraphQLTypes["ReportSubmissionMark"] | undefined | null,
period?: number | undefined | null,
@@ -31891,17 +32067,25 @@ export type GraphQLTypes = {
Требуемые роли: chairman, member, user. */
chatcoopCreateCalendarIcsSubscription: GraphQLTypes["ChatCoopCalendarIcsUrlResponse"],
/** Создать комнату с секретарём (публичную или приватную); секретарь подключается сразу
Требуемые роли: chairman, member. */
chatcoopCreateSecretaryRoom: GraphQLTypes["ChatcoopSecretaryRoom"],
/** Удалить событие календаря
Требуемые роли: chairman, member. */
chatcoopDeleteCalendarEvent: boolean,
/** Удалить комнату секретаря: вывести секретаря и снять комнату с синхронизации (возвращает matrixRoomId)
Требуемые роли: chairman, member. */
chatcoopRemoveSecretaryRoom: string,
/** Обновить событие календаря
Требуемые роли: chairman, member. */
chatcoopUpdateCalendarEvent: GraphQLTypes["ChatCoopCalendarEvent"],
/** Обновить заметку (memo) к транскрипции звонка
Требуемые роли: chairman, member, user. */
Требуемые роли: chairman, member. */
chatcoopUpdateTranscriptionMemo: GraphQLTypes["CallTranscription"],
/** Выполнить шаг онбординга capital (создание предложения повестки)
@@ -32221,6 +32405,8 @@ export type GraphQLTypes = {
walmoveWallets: GraphQLTypes["Ledger2AdjustmentResult"],
['...on Mutation']: Omit<GraphQLTypes["Mutation"], "...on Mutation">
};
/** Тип комнаты вне проекта: пайщики, совет, комната секретаря */
["NonProjectRoomKind"]: NonProjectRoomKind;
["NotificationWorkflowRecipientInput"]: {
/** Username получателя */
username: string
@@ -33161,8 +33347,6 @@ export type GraphQLTypes = {
available: string,
/** Номер блока последнего обновления */
blockNum?: number | undefined | null,
/** Заблокированный баланс (формат: "100.0000 RUB") */
blocked: string,
/** Имя кооператива */
coopname: string,
/** Уникальный идентификатор кошелька в блокчейне */
@@ -33514,10 +33698,18 @@ export type GraphQLTypes = {
Требуемые роли: chairman, member. */
chatcoopListCalendarRooms: Array<GraphQLTypes["ChatCoopCalendarRoomOption"]>,
/** Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) для синхронизации в blago
Требуемые роли: chairman, member, user. */
chatcoopListNonProjectCommunicationRooms: Array<GraphQLTypes["ChatcoopNonProjectCommunicationRoom"]>,
/** Комнаты Matrix, привязанные к проекту Capital (реестр ChatCoop)
Требуемые роли: chairman, member, user. */
chatcoopListProjectCommunicationRooms: Array<GraphQLTypes["ChatcoopProjectCommunicationRoom"]>,
/** Все комнаты реестра ChatCoop (системные/проектные read-only, комнаты секретаря удаляемые)
Требуемые роли: chairman, member. */
chatcoopListSecretaryRooms: Array<GraphQLTypes["ChatcoopSecretaryRoom"]>,
/** UTC-даты (YYYY-MM-DD), в которых есть сообщения новее afterOriginServerTsExclusive, для комнаты Matrix
Требуемые роли: chairman, member, user. */
@@ -33903,6 +34095,10 @@ export type GraphQLTypes = {
/** Название программы для отображения */
title: string,
['...on RegistrationProgram']: Omit<GraphQLTypes["RegistrationProgram"], "...on RegistrationProgram">
};
["RemoveSecretaryRoomInput"]: {
/** Идентификатор комнаты Matrix, которую нужно удалить */
matrixRoomId: string
};
["ReportCalendarPeriodEntry"]: {
__typename: "ReportCalendarPeriodEntry",
@@ -34973,7 +35169,7 @@ export type GraphQLTypes = {
};
["UpdateCallTranscriptionMemoInput"]: {
id: string,
/** Текст заметки (до 4000 символов) */
/** Текст заметки */
memo: string
};
["UpdateChatCoopCalendarEventInput"]: {
@@ -35597,6 +35793,19 @@ export enum LogEventType {
VOTING_COMPLETED = "VOTING_COMPLETED",
VOTING_STARTED = "VOTING_STARTED"
}
/** Тип комнаты: пайщики, совет, проект Capital, комната секретаря */
export enum ManagedRoomKind {
CAPITAL_PROJECT = "CAPITAL_PROJECT",
COUNCIL = "COUNCIL",
MEMBERS = "MEMBERS",
SECRETARY = "SECRETARY"
}
/** Тип комнаты вне проекта: пайщики, совет, комната секретаря */
export enum NonProjectRoomKind {
COUNCIL = "COUNCIL",
MEMBERS = "MEMBERS",
SECRETARY = "SECRETARY"
}
/** Тип юридического лица */
export enum OrganizationType {
AO = "AO",
@@ -35614,6 +35823,7 @@ export enum PaymentDirection {
}
/** Статус платежа */
export enum PaymentStatus {
AWAITING_AUTHORIZATION = "AWAITING_AUTHORIZATION",
CANCELLED = "CANCELLED",
COMPLETED = "COMPLETED",
EXPIRED = "EXPIRED",
@@ -35863,6 +36073,7 @@ type ZEUS_VARIABLES = {
["CreateProjectInput"]: ValueTypes["CreateProjectInput"];
["CreateProjectInvestInput"]: ValueTypes["CreateProjectInvestInput"];
["CreateProjectPropertyInput"]: ValueTypes["CreateProjectPropertyInput"];
["CreateSecretaryRoomInput"]: ValueTypes["CreateSecretaryRoomInput"];
["CreateSovietIndividualDataInput"]: ValueTypes["CreateSovietIndividualDataInput"];
["CreateStoryInput"]: ValueTypes["CreateStoryInput"];
["CreateSubscriptionInput"]: ValueTypes["CreateSubscriptionInput"];
@@ -35960,9 +36171,11 @@ type ZEUS_VARIABLES = {
["LoginInput"]: ValueTypes["LoginInput"];
["LogoutInput"]: ValueTypes["LogoutInput"];
["MakeClearanceInput"]: ValueTypes["MakeClearanceInput"];
["ManagedRoomKind"]: ValueTypes["ManagedRoomKind"];
["MarkReportPeriodInput"]: ValueTypes["MarkReportPeriodInput"];
["ModerateRequestInput"]: ValueTypes["ModerateRequestInput"];
["MoveCapitalIssueToComponentInput"]: ValueTypes["MoveCapitalIssueToComponentInput"];
["NonProjectRoomKind"]: ValueTypes["NonProjectRoomKind"];
["NotificationWorkflowRecipientInput"]: ValueTypes["NotificationWorkflowRecipientInput"];
["NotifyOnAnnualGeneralMeetInput"]: ValueTypes["NotifyOnAnnualGeneralMeetInput"];
["OpenProjectInput"]: ValueTypes["OpenProjectInput"];
@@ -36008,6 +36221,7 @@ type ZEUS_VARIABLES = {
["RegisterAccountInput"]: ValueTypes["RegisterAccountInput"];
["RegisterContributorInput"]: ValueTypes["RegisterContributorInput"];
["RegisterParticipantInput"]: ValueTypes["RegisterParticipantInput"];
["RemoveSecretaryRoomInput"]: ValueTypes["RemoveSecretaryRoomInput"];
["ReportHistoryFilterInput"]: ValueTypes["ReportHistoryFilterInput"];
["ReportPreviewInput"]: ValueTypes["ReportPreviewInput"];
["ReportSubmissionMark"]: ValueTypes["ReportSubmissionMark"];
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "cooptypes",
"type": "module",
"version": "2026.5.23-4",
"version": "2026.5.25-3",
"description": "Shared TypeScript типы и интерфейсы экосистемы кооперативов",
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
"license": "MIT",
@@ -11,11 +11,11 @@
*
* Идентификаторы кошельков (`wallet_from`/`wallet_to`) eosio::name с
* префиксом `w.<contract>.<waltype>` (см. `./wallets.ts`). Пустая строка
* (`""`) sentinel «кошелёк вне системы» для ISSUE, BURN и BURN_BLOCKED.
* (`""`) sentinel «кошелёк вне системы» для ISSUE и BURN.
*/
import type { IName } from '../interfaces/ledger2'
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BLOCK' | 'UNBLOCK' | 'BURN' | 'BURN_BLOCKED' | 'NONE'
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BURN' | 'NONE'
export interface OperationMeta {
/** Машинный идентификатор — eosio::name в контракте. */
@@ -34,7 +34,7 @@ export interface OperationMeta {
wallet_op: WalletOp | null
/** Кошелёк-источник (null для ISSUE и для adjustment-операций). */
wallet_from: IName | null
/** Кошелёк-приёмник (null для BLOCK/UNBLOCK/BURN/BURN_BLOCKED и для adjustment-операций). */
/** Кошелёк-приёмник (null для BURN и для adjustment-операций). */
wallet_to: IName | null
/** Код счёта Дт (null без бухпроводки, ADR-003: ⇔ credit == null). */
debit: number | null
@@ -74,17 +74,17 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [
human_name: 'Внесение пайщиком паевого взноса' },
{ code: 'o.wal.wthreq', process_type: 'p.wal.wthdrw', contract: 'wallet',
name: 'REQUEST_WITHDRAW', wallet_op: 'BLOCK', wallet_from: 'w.wal.share', wallet_to: null,
name: 'REQUEST_WITHDRAW', wallet_op: 'TRANSFER', wallet_from: 'w.wal.share', wallet_to: 'w.wal.wpend',
debit: null, credit: null,
human_name: 'Блокировка паевого под запрос на возврат' },
human_name: 'Резервирование паевого под запрос на возврат' },
{ code: 'o.wal.wthdec', process_type: 'p.wal.wthdrw', contract: 'wallet',
name: 'DECLINE_WITHDRAW', wallet_op: 'UNBLOCK', wallet_from: 'w.wal.share', wallet_to: null,
name: 'DECLINE_WITHDRAW', wallet_op: 'TRANSFER', wallet_from: 'w.wal.wpend', wallet_to: 'w.wal.share',
debit: null, credit: null,
human_name: 'Разблокировка паевого после отклонения запроса на возврат' },
human_name: 'Снятие резерва паевого после отклонения запроса на возврат' },
{ code: 'o.wal.wthcpl', process_type: 'p.wal.wthdrw', contract: 'wallet',
name: 'COMPLETE_WITHDRAW', wallet_op: 'BURN_BLOCKED', wallet_from: 'w.wal.share', wallet_to: null,
name: 'COMPLETE_WITHDRAW', wallet_op: 'BURN', wallet_from: 'w.wal.wpend', wallet_to: null,
debit: 80, credit: 51,
human_name: 'Возврат паевого взноса пайщику' },
@@ -27,6 +27,7 @@ export const LEDGER2_WALLET_REGISTRY: readonly WalletMeta[] = [
{ name: "w.cap.gen", human_name: "ЦПП «Генератор» — единый кошелёк программы", kind: "COOPERATIVE" },
{ name: "w.reg.entry", human_name: "Вступительные взносы", kind: "COOPERATIVE" },
{ name: "w.wal.wthdrw", human_name: "Возвраты паевых взносов пайщикам (deprecated, не используется в новых операциях)", kind: "COOPERATIVE" },
{ name: "w.wal.wpend", human_name: "Резерв паевого под заявку на возврат", kind: "COOPERATIVE" },
{ name: "w.sov.infra", human_name: "Членские взносы за инфраструктуру кооп. платформы", kind: "COOPERATIVE" },
{ name: "w.sov.delgte", human_name: "Делегатские членские взносы", kind: "COOPERATIVE" },
{ name: "w.sov.expns", human_name: "Хозяйственные расходы из числа целевого финансирования", kind: "COOPERATIVE" },
@@ -33,7 +33,6 @@ import { computed } from 'vue';
import { useWalletStore } from 'src/entities/Wallet/model';
import { ColorCard } from 'src/shared/ui';
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
import { addAssets } from 'src/shared/lib/utils/addAssets';
import { Zeus } from '@coopenomics/sdk';
import { CreateProgramInvestButton } from 'app/extensions/capital/features/ProgramInvest/CreateProgramInvest/ui';
@@ -47,8 +46,7 @@ const generatorWallet = computed(() => {
return {
available: wallet?.available || '0.00',
blocked: wallet?.blocked || '0.00',
total: addAssets(wallet?.available || '0.00', wallet?.blocked || '0.00'),
total: wallet?.available || '0.00',
};
});
@@ -61,8 +59,7 @@ const blagorostWallet = computed(() => {
return {
available: wallet?.available || '0.00',
blocked: wallet?.blocked || '0.00',
total: addAssets(wallet?.available || '0.00', wallet?.blocked || '0.00'),
total: wallet?.available || '0.00',
};
});
@@ -244,45 +244,30 @@ const calculateInvestorTotal = (contributor: IContributor) => {
// Функции для расчета сумм кошельков
const calculateMainWalletTotal = (contributor: IContributor) => {
const availableStr = contributor?.main_wallet?.available || '0.0000';
const blockedStr = contributor?.main_wallet?.blocked || '0.0000';
const total = Number(availableStr.split(' ')[0] || '0');
const available = Number(availableStr.split(' ')[0] || '0');
const blocked = Number(blockedStr.split(' ')[0] || '0');
const total = available + blocked;
// Определяем валюту из любого поля, которое ее содержит, или используем системный символ
const currency = availableStr.split(' ')[1] || blockedStr.split(' ')[1] || governSymbol.value;
// Определяем валюту из поля available или используем системный символ
const currency = availableStr.split(' ')[1] || governSymbol.value;
return currency ? `${total} ${currency}` : total.toString();
};
const calculateGenerationWalletTotal = (contributor: IContributor) => {
const availableStr = contributor?.generation_wallet?.available || '0.0000';
const blockedStr = contributor?.generation_wallet?.blocked || '0.0000';
const total = Number(availableStr.split(' ')[0] || '0');
const available = Number(availableStr.split(' ')[0] || '0');
const blocked = Number(blockedStr.split(' ')[0] || '0');
const total = available + blocked;
// Определяем валюту из любого поля, которое ее содержит, или используем системный символ
const currency = availableStr.split(' ')[1] || blockedStr.split(' ')[1] || governSymbol.value;
// Определяем валюту из поля available или используем системный символ
const currency = availableStr.split(' ')[1] || governSymbol.value;
return currency ? `${total} ${currency}` : total.toString();
};
const calculateBlagorostWalletTotal = (contributor: IContributor) => {
const availableStr = contributor?.blagorost_wallet?.available || '0.0000';
const blockedStr = contributor?.blagorost_wallet?.blocked || '0.0000';
const total = Number(availableStr.split(' ')[0] || '0');
const available = Number(availableStr.split(' ')[0] || '0');
const blocked = Number(blockedStr.split(' ')[0] || '0');
const total = available + blocked;
// Определяем валюту из любого поля, которое ее содержит, или используем системный символ
const currency = availableStr.split(' ')[1] || blockedStr.split(' ')[1] || governSymbol.value;
// Определяем валюту из поля available или используем системный символ
const currency = availableStr.split(' ')[1] || governSymbol.value;
return currency ? `${total} ${currency}` : total.toString();
};
@@ -106,8 +106,8 @@ export const useOnboardingFlow = () => {
const importStep: AgendaStep = {
key: 'import_participants',
type: 'import',
title: 'Импорт пайщиков',
description: 'Загрузите пайщиков через CSV-файл.',
title: 'Импорт пайщиков (опционально)',
description: 'Необязательный шаг. Если у вас уже есть пайщики кооператива — загрузите их списком через CSV-файл. Если нет — просто двигайтесь дальше.',
done: false,
pending: false,
};
@@ -11,92 +11,122 @@ OnboardingCompletionCelebration(
)
// Показываем шаги если онбординг не завершен и данные загружены
q-card(v-else flat)
q-card-section.row.items-center.justify-between
div
div.text-h5 Адаптируйте кооператив к работе на платформе
q-chip(
.onboarding(v-else)
.onboarding__head
h2.onboarding__title Адаптируйте кооператив к работе на платформе
p.onboarding__lead
| Чтобы кооператив начал работать в цифровом контуре платформы, совет
| принимает несколько решений в электронной форме по одному на каждый
| шаг ниже. Сначала утверждаются положения о цифровых сервисах и формы
| документов кооператива, затем оформляется вступление в Потребительский
| кооператив «Восход». После этого вы импортируете пайщиков и объявляете
| общее собрание. Для каждого шага платформа сформирует проект повестки и
| зафиксирует принятое решение пройдите все шаги до окончания срока
| адаптации.
.onboarding__deadline
q-icon(name="fa-regular fa-clock" size="15px")
span {{ countdownLabel ? `Срок адаптации: ${countdownLabel}` : 'Отсчёт появится после получения данных' }}
color="primary"
text-color="white"
icon="schedule"
) {{ countdownLabel || 'Отсчёт появится после получения данных' }}
q-separator
q-card-section
q-list(separator)
q-item(v-for="(step, index) in steps" :key="step.key")
q-item-section
div.row.items-center.q-gutter-sm
q-icon(
:name="getIcon(step)"
:color="getIconColor(step)"
size="22px"
q-separator.onboarding__sep
.stepper.stepper--v
.step(
v-for="(step, index) in steps"
:key="step.key"
:class="stepClass(index)"
)
.step__rail
.step__num
q-icon(v-if="step.done" name="fa-solid fa-check")
template(v-else) {{ index + 1 }}
.step__line
.step__body
.step__label {{ step.title }}
.step__sublabel {{ step.description }}
.step__meta(v-if="!step.done && step.pending")
q-icon(name="fa-solid fa-hourglass-half" size="12px")
span {{ getPendingText(step) }}
.step__action(v-if="showAction(index)")
BaseButton(
v-if="step.type === 'agenda'"
variant="primary"
size="sm"
:loading="loadingAction"
@click="openAgendaDialog(step)"
)
div.text-subtitle1 {{ index + 1 }}. {{ step.title }}
div.text-caption.text-grey-7 {{ step.description }}
q-chip(
v-if="!step.done && step.pending"
dense
color="amber"
text-color="black"
icon="hourglass_top"
class="q-mt-xs"
) {{ getPendingText(step) }}
q-item-section(side)
q-btn(
v-if="step.type === 'agenda' && showAction(index)"
:disable="loadingAction"
color="primary"
label="Объявить собрание совета"
@click="openAgendaDialog(step)"
)
template(#icon-left)
q-icon(name="fa-solid fa-bullhorn" size="14px")
| Объявить собрание совета
q-btn(
v-else-if="step.type === 'import' && showAction(index)"
:disable="loadingAction || isActionDisabled(index)"
color="primary"
icon="upload_file"
push
label="Импорт пайщиков"
@click="importDialog = true"
)
q-tooltip(
v-if="isActionDisabled(index)"
anchor="top middle"
self="bottom middle"
) Для продолжения примите решения советом
q-btn(
v-else-if="step.type === 'meet' && showAction(index)"
:disable="loadingAction || isActionDisabled(index)"
color="primary"
label="Объявить общее собрание"
@click="openMeetDialog"
)
q-tooltip(
v-if="isActionDisabled(index)"
anchor="top middle"
self="bottom middle"
) Для продолжения примите решения советом
BaseButton(
v-else-if="step.type === 'import'"
variant="primary"
size="sm"
:disabled="loadingAction || isActionDisabled(index)"
@click="importDialog = true"
)
template(#icon-left)
q-icon(name="fa-solid fa-file-arrow-up" size="14px")
| Импорт пайщиков
q-tooltip(
v-if="isActionDisabled(index)"
anchor="top middle"
self="bottom middle"
) Для продолжения примите решения советом
q-dialog(v-model="agendaDialog.open" persistent)
ModalBase(title="Предложение повестки" @close="closeAgendaDialog" style="min-width: 640px")
template(#default)
q-card-section
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
q-icon(name="help_outline" size="18px" class="text-primary")
span Вопрос на повестке
div.q-mt-sm.q-pa-sm.text-body1.rounded-borders {{ agendaDialog.question }}
BaseButton(
v-else-if="step.type === 'meet'"
variant="primary"
size="sm"
:disabled="loadingAction || isActionDisabled(index)"
@click="openMeetDialog"
)
template(#icon-left)
q-icon(name="fa-solid fa-users" size="14px")
| Объявить общее собрание
q-tooltip(
v-if="isActionDisabled(index)"
anchor="top middle"
self="bottom middle"
) Для продолжения примите решения советом
q-separator.q-my-md
BaseDialog(
:model-value="agendaDialog.open"
size="lg"
:title="agendaDialog.title || 'Предложение повестки'"
:close-on-backdrop="!loadingAction"
:close-on-escape="!loadingAction"
@update:model-value="onAgendaDialogToggle"
)
.agenda-dialog
.agenda-dialog__section
.agenda-dialog__label
q-icon(name="fa-regular fa-circle-question" size="16px")
span Вопрос на повестке
.agenda-dialog__question {{ agendaDialog.question }}
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
q-icon(name="gavel" size="18px" class="text-primary")
span Проект решения
div.q-mt-sm.q-pa-sm.rounded-borders
DocumentHtmlReader(:html="agendaDialog.decision" :sanitize="false")
div.q-pb-lg
q-btn(flat label="Отмена" @click="closeAgendaDialog" :disable="loadingAction")
q-btn(color="primary" label="Объявить" :loading="loadingAction" @click="submitAgenda")
.agenda-dialog__section
.agenda-dialog__label
q-icon(name="fa-solid fa-gavel" size="16px")
span Проект решения
.agenda-dialog__decision
DocumentHtmlReader(:html="agendaDialog.decision" :sanitize="false")
template(#footer)
BaseButton(
variant="ghost"
:disabled="loadingAction"
@click="closeAgendaDialog"
) Отмена
BaseButton(
variant="primary"
:loading="loadingAction"
@click="submitAgenda"
)
template(#icon-left)
q-icon(name="fa-solid fa-bullhorn" size="15px")
| Объявить
CreateMeetForm(
v-model="meetDialog"
@@ -114,8 +144,9 @@ import { onMounted, ref } from 'vue';
import { CreateMeetForm } from 'src/features/Meet/CreateMeet/ui';
import { ParticipantsImportDialog } from 'src/features/User/ImportParticipants/ui';
import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { WindowLoader } from 'src/shared/ui/Loader';
import { BaseButton } from 'src/shared/ui/base/BaseButton';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import OnboardingCompletionCelebration from './OnboardingCompletionCelebration.vue';
import { useOnboardingFlow } from '../model';
@@ -138,18 +169,11 @@ const {
const importDialog = ref(false);
const getIcon = (step: { done: boolean; pending: boolean; type: string }) => {
if (step.done) return 'task_alt';
if (step.pending) return 'hourglass_top';
if (step.type === 'import') return 'arrow_forward';
return 'radio_button_unchecked';
};
const getIconColor = (step: { done: boolean; pending: boolean; type: string }) => {
if (step.done) return 'green-6';
if (step.pending) return 'orange-6';
if (step.type === 'import') return '';
return 'grey-6';
// Закрытие диалога повестки (крестик / клик по фону / Escape) сбрасывает
// его состояние через closeAgendaDialog. Открытие идёт только через
// кнопку шага (openAgendaDialog), поэтому здесь обрабатываем лишь закрытие.
const onAgendaDialogToggle = (open: boolean) => {
if (!open) closeAgendaDialog();
};
const getPendingText = (step: { type: string }) => {
@@ -191,5 +215,104 @@ const isActionDisabled = (index: number) => {
return false;
};
// Канон-степпер: пройденный шаг is-done (галочка, заливка primary),
// текущий доступный к действию is-active (подсветка номера). Ожидающие
// решения и ещё недоступные шаги нейтральные.
const stepClass = (index: number) => {
const step = steps.value[index];
if (!step) return '';
if (step.done) return 'is-done';
if (showAction(index)) return 'is-active';
return '';
};
onMounted(init);
</script>
<style scoped lang="scss">
.onboarding {
display: flex;
flex-direction: column;
}
.onboarding__head {
display: flex;
flex-direction: column;
gap: var(--p-3, 12px);
}
.onboarding__title {
margin: 0;
font-size: var(--p-fs-h2);
font-weight: 600;
letter-spacing: -0.01em;
color: var(--p-ink);
}
.onboarding__lead {
margin: 0;
font-size: var(--p-fs-body);
line-height: 1.55;
color: var(--p-ink-2);
}
// Разделитель между шапкой и степпером канон-воздух сверху/снизу.
.onboarding__sep {
margin: var(--p-5, 20px) 0;
}
// Действие шага без обрамляющей рамки, просто отступ сверху.
.step__action {
margin-top: var(--p-3, 12px);
}
// Срок адаптации спокойная плашка-предупреждение, отсчёт виден сразу.
.onboarding__deadline {
align-self: flex-start;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: var(--p-r-pill, 999px);
background: var(--p-warn-soft);
color: var(--p-warn);
font-size: var(--p-fs-body-sm);
font-weight: 500;
}
// Диалог повестки.
.agenda-dialog {
display: flex;
flex-direction: column;
gap: var(--p-5, 20px);
}
.agenda-dialog__section {
display: flex;
flex-direction: column;
gap: var(--p-2, 8px);
}
// Подписи секций надстрочные метки (eyebrow): отдельный типографический
// ярус ниже заголовка окна и заметно отличный от него, чтобы не казаться
// конкурирующим заголовком на фоне крупного заголовка самого документа.
.agenda-dialog__label {
display: flex;
align-items: center;
gap: 7px;
font-size: var(--p-fs-body-sm);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--p-ink-3);
.q-icon {
color: var(--p-primary);
}
}
.agenda-dialog__question {
font-size: var(--p-fs-body);
line-height: 1.5;
color: var(--p-ink);
}
.agenda-dialog__decision {
padding: var(--p-4, 16px);
background: var(--p-surface-2);
border: 1px solid var(--p-line);
border-radius: var(--p-r-sm, 8px);
max-height: 50vh;
overflow-y: auto;
}
</style>
@@ -1,43 +1,41 @@
<template lang="pug">
form.full-width(@submit.prevent='submit')
.q-gutter-md
// Провайдер платежей по умолчанию
.row.q-gutter-md
.col-md-6.col-sm-12
q-select(
v-model='formData.provider_name'
:options='providerOptions'
label='Провайдер входящих платежей'
placeholder='Выберите провайдера'
dense
standout="bg-teal text-white"
emit-value
map-options
option-value='value'
option-label='label'
:loading='loading'
)
form.pp-form(@submit.prevent='submit')
q-select.pp-form__select(
v-model='formData.provider_name'
:options='providerOptions'
label='Провайдер входящих платежей'
placeholder='Выберите провайдера'
dense
outlined
color='primary'
emit-value
map-options
option-value='value'
option-label='label'
:loading='loading'
)
.col-md-12(v-if='selectedProviderDescription')
.q-mt-sm.text-caption.text-grey-7 {{ selectedProviderDescription }}
.pp-form__hint(v-if='selectedProviderDescription') {{ selectedProviderDescription }}
.q-mt-md
.row.justify-start.q-gutter-sm
q-btn(
flat
label='Отменить'
color='grey'
@click='resetForm'
:loading='loading'
:disable='!hasChanges'
)
q-btn(
type='submit'
label='Сохранить'
color='primary'
:loading='loading'
:disable='!hasChanges'
)
.pp-form__actions
q-btn(
flat
no-caps
label='Отменить'
color='grey-7'
@click='resetForm'
:loading='loading'
:disable='!hasChanges'
)
q-btn(
type='submit'
no-caps
unelevated
label='Сохранить'
color='primary'
:loading='loading'
:disable='!hasChanges'
)
</template>
<script lang="ts" setup>
@@ -133,8 +131,24 @@ const submit = async () => {
}
</script>
<style scoped>
.full-width {
<style scoped lang="scss">
.pp-form {
display: flex;
flex-direction: column;
gap: var(--p-4, 16px);
width: 100%;
}
.pp-form__select {
max-width: 480px;
}
.pp-form__hint {
max-width: 640px;
font-size: var(--p-fs-body-sm);
line-height: 1.5;
color: var(--p-ink-2);
}
.pp-form__actions {
display: flex;
gap: var(--p-2, 8px);
}
</style>
@@ -10,7 +10,8 @@ form.full-width(@submit.prevent='submit')
label='Рабочий стол для авторизованных пользователей'
placeholder='Выберите рабочий стол'
dense
standout="bg-teal text-white"
outlined
color="primary"
emit-value
map-options
option-value='value'
@@ -26,7 +27,8 @@ form.full-width(@submit.prevent='submit')
label='Страница для авторизованных пользователей'
placeholder='Выберите страницу'
dense
standout="bg-teal text-white"
outlined
color="primary"
emit-value
map-options
option-value='value'
@@ -45,7 +47,8 @@ form.full-width(@submit.prevent='submit')
label='Рабочий стол для неавторизованных пользователей'
placeholder='Выберите рабочий стол'
dense
standout="bg-teal text-white"
outlined
color="primary"
emit-value
map-options
option-value='value'
@@ -61,7 +64,8 @@ form.full-width(@submit.prevent='submit')
label='Страница для неавторизованных пользователей'
placeholder='Выберите страницу'
dense
standout="bg-teal text-white"
outlined
color="primary"
emit-value
map-options
option-value='value'
@@ -77,7 +77,7 @@ export default async function (): Promise<IWorkspaceConfig[]> {
name: 'extensions',
component: markRaw(ExtensionsManagement),
meta: {
title: 'Магазин приложений',
title: 'Каталог приложений',
icon: 'fa-solid fa-puzzle-piece',
roles: ['chairman'],
agreements: agreementsBase,
@@ -1,5 +1,5 @@
<template lang="pug">
div
.approvals-page
q-card(flat)
// Фильтры
@@ -11,7 +11,8 @@ div
label='Статус'
placeholder='без фильтра'
dense
standout="bg-teal text-white"
outlined
color="primary"
@update:model-value='onFiltersChange'
style="width: 250px"
)
@@ -128,3 +129,12 @@ watch(() => route.params.coopname, () => {
loadApprovals();
});
</script>
<style scoped lang="scss">
.approvals-page {
padding: var(--p-6, 24px);
@media (max-width: 768px) {
padding: var(--p-4, 16px);
}
}
</style>
@@ -1,8 +1,18 @@
<template lang="pug">
q-page.padding
q-page.onboarding-page
OnboardingStepsCard
</template>
<script setup lang="ts">
import { OnboardingStepsCard } from 'app/extensions/chairman/features/Onboarding/ui';
</script>
<style scoped lang="scss">
// Канон-отступы страницы (как у прочих столов): контент не липнет к краям.
.onboarding-page {
padding: var(--p-6, 24px);
@media (max-width: 768px) {
padding: var(--p-4, 16px);
}
}
</style>
@@ -1,17 +1,19 @@
<template lang="pug">
div.page-shell
q-card.hero-card(flat)
.hero-title Провайдер платежей
.hero-subtitle
.payment-provider-page
.banner
q-icon.banner__icon(name='fa-solid fa-circle-info' size='18px')
.banner__body
| Выберите провайдера входящих платежей по умолчанию для вашего кооператива.
| Этот провайдер будет использоваться для создания и обработки платежей пайщиков.
PaymentProviderForm(
:loading='saving'
@submit='onSubmit'
@success='onSuccess'
@error='onError'
)
q-card(flat)
q-card-section
PaymentProviderForm(
:loading='saving'
@submit='onSubmit'
@success='onSuccess'
@error='onError'
)
</template>
<script lang="ts" setup>
@@ -43,31 +45,14 @@ const onError = (error: Error) => {
</script>
<style scoped lang="scss">
.page-shell {
.payment-provider-page {
width: 100%;
padding: 24px 12px 32px;
display: flex;
flex-direction: column;
gap: 16px;
}
.hero-card {
border-radius: 18px;
padding: 18px 20px;
}
.hero-title {
font-size: 22px;
font-weight: 600;
}
.hero-subtitle {
line-height: 1.55;
max-width: 820px;
}
.surface-card {
border-radius: 16px;
padding: 16px 18px;
gap: var(--p-5, 20px);
padding: var(--p-6, 24px);
@media (max-width: 768px) {
padding: var(--p-4, 16px);
}
}
</style>
@@ -1,19 +1,21 @@
<template lang="pug">
div.page-shell
q-card.hero-card(flat)
.hero-title Стартовые страницы
.hero-subtitle
| Настройте рабочие столы и страницы, которые будут открываться по умолчанию
| для новых пользователей при входе на сайт. Единый стиль помогает быстрее
| понять, что важно сделать в первую очередь.
.settings-page
//- Заголовок страницы не дублируем он уже показан крошкой в шапке.
//- Пояснение размещаем на канон-поверхности (.banner), а не на голом фоне.
.banner
q-icon.banner__icon(name='fa-solid fa-circle-info' size='18px')
.banner__body
| Настройте рабочие столы и страницы, которые будут открываться по
| умолчанию для новых пользователей при входе на сайт.
q-card.surface-card(flat)
DefaultPagesForm(
:loading='saving'
@submit='onSubmit'
@success='onSuccess'
@error='onError'
)
q-card(flat)
q-card-section
DefaultPagesForm(
:loading='saving'
@submit='onSubmit'
@success='onSuccess'
@error='onError'
)
</template>
<script lang="ts" setup>
@@ -45,31 +47,14 @@ const onError = (error: Error) => {
</script>
<style scoped lang="scss">
.page-shell {
width: 100%;
padding: 24px 12px 32px;
// Канон-отступы страницы (как на прочих столах).
.settings-page {
display: flex;
flex-direction: column;
gap: 16px;
}
.hero-card {
border-radius: 18px;
padding: 18px 20px;
}
.hero-title {
font-size: 22px;
font-weight: 600;
}
.hero-subtitle {
line-height: 1.55;
max-width: 820px;
}
.surface-card {
border-radius: 16px;
padding: 16px 18px;
gap: var(--p-5, 20px);
padding: var(--p-6, 24px);
@media (max-width: 768px) {
padding: var(--p-4, 16px);
}
}
</style>
@@ -0,0 +1,33 @@
import { client } from 'src/shared/api/client';
import { Queries, Mutations } from '@coopenomics/sdk';
import type { ISecretaryRoom, ICreateSecretaryRoomInput } from '../model/types';
async function listSecretaryRooms(): Promise<ISecretaryRoom[]> {
const { [Queries.ChatCoop.ListSecretaryRooms.name]: rows } = await client.Query(
Queries.ChatCoop.ListSecretaryRooms.query,
{},
);
return rows ?? [];
}
async function createSecretaryRoom(data: ICreateSecretaryRoomInput): Promise<ISecretaryRoom> {
const { [Mutations.ChatCoop.CreateSecretaryRoom.name]: row } = await client.Mutation(
Mutations.ChatCoop.CreateSecretaryRoom.mutation,
{ variables: { data } },
);
return row;
}
async function removeSecretaryRoom(matrixRoomId: string): Promise<string> {
const { [Mutations.ChatCoop.RemoveSecretaryRoom.name]: result } = await client.Mutation(
Mutations.ChatCoop.RemoveSecretaryRoom.mutation,
{ variables: { data: { matrixRoomId } } },
);
return result;
}
export const api = {
listSecretaryRooms,
createSecretaryRoom,
removeSecretaryRoom,
};
@@ -0,0 +1,2 @@
export { useSecretaryRoomStore } from './model';
export * from './model/types';
@@ -0,0 +1,2 @@
export * from './store';
export * from './types';
@@ -0,0 +1,78 @@
import { defineStore } from 'pinia';
import { ref, Ref } from 'vue';
import { api } from '../api';
import type { ISecretaryRoom, ICreateSecretaryRoomInput } from './types';
const namespace = 'secretaryRoomStore';
interface ISecretaryRoomStore {
rooms: Ref<ISecretaryRoom[]>;
isLoading: Ref<boolean>;
isMutating: Ref<boolean>;
error: Ref<string | null>;
loadRooms: () => Promise<ISecretaryRoom[]>;
createRoom: (data: ICreateSecretaryRoomInput) => Promise<ISecretaryRoom>;
removeRoom: (matrixRoomId: string) => Promise<void>;
clearError: () => void;
}
export const useSecretaryRoomStore = defineStore(namespace, (): ISecretaryRoomStore => {
const rooms = ref<ISecretaryRoom[]>([]);
const isLoading = ref(false);
const isMutating = ref(false);
const error = ref<string | null>(null);
const loadRooms = async (): Promise<ISecretaryRoom[]> => {
isLoading.value = true;
error.value = null;
try {
const result = await api.listSecretaryRooms();
rooms.value = result;
return result;
} catch (err) {
console.error('Failed to load secretary rooms:', err);
error.value = 'Не удалось загрузить список комнат. Попробуйте обновить страницу.';
return [];
} finally {
isLoading.value = false;
}
};
const createRoom = async (data: ICreateSecretaryRoomInput): Promise<ISecretaryRoom> => {
isMutating.value = true;
error.value = null;
try {
const room = await api.createSecretaryRoom(data);
await loadRooms();
return room;
} finally {
isMutating.value = false;
}
};
const removeRoom = async (matrixRoomId: string): Promise<void> => {
isMutating.value = true;
error.value = null;
try {
await api.removeSecretaryRoom(matrixRoomId);
await loadRooms();
} finally {
isMutating.value = false;
}
};
const clearError = () => {
error.value = null;
};
return {
rooms,
isLoading,
isMutating,
error,
loadRooms,
createRoom,
removeRoom,
clearError,
};
});
@@ -0,0 +1,8 @@
import type { Queries, Mutations } from '@coopenomics/sdk';
/** Комната из реестра ChatCoop (системная / проектная / секретаря) */
export type ISecretaryRoom =
Queries.ChatCoop.ListSecretaryRooms.IOutput[typeof Queries.ChatCoop.ListSecretaryRooms.name][number];
/** Вход для создания комнаты секретаря */
export type ICreateSecretaryRoomInput = Mutations.ChatCoop.CreateSecretaryRoom.IInput['data'];
@@ -1,3 +1,4 @@
export * as ChatCoopCalendar from './ChatCoopCalendar';
export * as ChatCoopChat from './ChatCoopChat';
export * as SecretaryRoom from './SecretaryRoom';
export * as Transcription from './Transcription';
@@ -1,5 +1,5 @@
import { markRaw } from 'vue';
import { CalendarPage, ChatCoopPage, MobileClientPage, TranscriptionsPage, TranscriptionDetailPage } from './pages';
import { CalendarPage, ChatCoopPage, MobileClientPage, SecretaryRoomsPage, TranscriptionsPage, TranscriptionDetailPage } from './pages';
import { agreementsBase } from 'src/shared/lib/consts/workspaces';
import type { IWorkspaceConfig } from 'src/shared/lib/types/workspace';
@@ -73,6 +73,19 @@ export default async function (): Promise<IWorkspaceConfig[]> {
},
children: [],
},
{
path: 'secretary-rooms',
name: 'chatcoop-secretary-rooms',
component: markRaw(SecretaryRoomsPage),
meta: {
title: 'Комнаты секретаря',
icon: 'fa-solid fa-user-shield',
roles: ['chairman', 'member'],
agreements: agreementsBase,
requiresAuth: true,
},
children: [],
},
{
path: 'transcriptions/:id',
name: 'chatcoop-transcription-detail',
@@ -0,0 +1 @@
export * from './ui'
@@ -0,0 +1,282 @@
<template lang="pug">
q-page.secretary-rooms-page(padding)
header.sr-head
div.sr-head__text
h1.sr-head__title Комнаты секретаря
p.sr-head__subtitle
| Создавайте комнаты для звонков с секретарём он подключается сразу и ведёт транскрипцию.
| Системные и проектные комнаты показаны для справки и не удаляются.
.sr-head__actions
q-btn(
unelevated
no-caps
color="primary"
icon="fa-solid fa-plus"
label="Создать комнату"
@click="openCreateDialog"
)
q-btn.sr-head__refresh(
flat
round
dense
icon="fa-solid fa-rotate-right"
@click="handleRefresh"
:loading="store.isLoading"
aria-label="Обновить список"
)
q-tooltip Обновить
q-banner.sr-error(v-if="store.error" dense rounded class="bg-red-1 text-red-9 q-mb-md")
| {{ store.error }}
q-table(
flat
bordered
:rows="store.rooms"
:columns="columns"
row-key="matrixRoomId"
:loading="store.isLoading"
:rows-per-page-options="[0]"
hide-pagination
no-data-label="Комнат пока нет"
)
template(#body-cell-displayLabel="props")
q-td(:props="props")
.sr-name {{ props.row.displayLabel }}
.sr-room-id {{ props.row.matrixRoomId }}
template(#body-cell-kind="props")
q-td(:props="props")
q-badge(:color="kindColor(props.row.kind)" :label="kindLabel(props.row.kind)" outline)
template(#body-cell-secretary="props")
q-td(:props="props")
q-icon(
:name="props.row.secretaryInRoom ? 'fa-solid fa-circle-check' : 'fa-solid fa-circle-minus'"
:color="props.row.secretaryInRoom ? 'positive' : 'grey-5'"
size="18px"
)
q-tooltip {{ props.row.secretaryInRoom ? 'Секретарь в комнате' : 'Секретаря нет' }}
q-icon.q-ml-sm(
v-if="props.row.encrypted"
name="fa-solid fa-lock"
color="orange-8"
size="16px"
)
q-tooltip Зашифрованная комната транскрипция недоступна
template(#body-cell-actions="props")
q-td(:props="props" align="right")
q-btn(
v-if="props.row.editable"
flat
dense
no-caps
color="negative"
icon="fa-solid fa-trash"
label="Удалить"
@click="confirmRemove(props.row)"
)
span.text-grey-5(v-else)
//- Диалог создания комнаты
q-dialog(v-model="createDialog")
q-card.sr-dialog
q-card-section
.text-h6 Новая комната секретаря
q-card-section.q-pt-none
q-input(
v-model="form.displayName"
label="Название комнаты"
autofocus
:rules="[(v) => !!v && v.trim().length > 0 || 'Введите название']"
maxlength="240"
)
.sr-type.q-mt-md
.text-subtitle2.q-mb-xs Тип комнаты
q-option-group(
v-model="form.isPublic"
:options="typeOptions"
color="primary"
type="radio"
)
.text-caption.text-grey-7.q-mt-xs
| {{ form.isPublic ? 'Публичная: войти может любой пайщик.' : 'Приватная: участников приглашаете вы (в клиенте мессенджера).' }}
q-card-actions(align="right")
q-btn(flat no-caps label="Отмена" v-close-popup :disable="store.isMutating")
q-btn(
unelevated
no-caps
color="primary"
label="Создать"
:loading="store.isMutating"
@click="submitCreate"
)
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { useQuasar } from 'quasar';
import { useSecretaryRoomStore } from '../../../entities/SecretaryRoom';
import type { ISecretaryRoom } from '../../../entities/SecretaryRoom';
const $q = useQuasar();
const store = useSecretaryRoomStore();
const createDialog = ref(false);
const form = ref<{ displayName: string; isPublic: boolean }>({
displayName: '',
isPublic: false,
});
const typeOptions = [
{ label: 'Приватная (по приглашению)', value: false },
{ label: 'Публичная (открытый вход)', value: true },
];
const columns = [
{
name: 'displayLabel',
label: 'Комната',
field: 'displayLabel',
align: 'left' as const,
style: 'min-width: 320px;',
},
{ name: 'kind', label: 'Тип', field: 'kind', align: 'left' as const },
{ name: 'secretary', label: 'Секретарь', field: 'secretaryInRoom', align: 'left' as const },
{ name: 'actions', label: '', field: 'actions', align: 'right' as const },
];
function kindLabel(kind: ISecretaryRoom['kind']): string {
switch (kind) {
case 'MEMBERS':
return 'Пайщики';
case 'COUNCIL':
return 'Совет';
case 'CAPITAL_PROJECT':
return 'Проект';
case 'SECRETARY':
return 'Секретарь';
default:
return String(kind);
}
}
function kindColor(kind: ISecretaryRoom['kind']): string {
switch (kind) {
case 'SECRETARY':
return 'primary';
case 'CAPITAL_PROJECT':
return 'teal';
default:
return 'grey-7';
}
}
function openCreateDialog(): void {
form.value = { displayName: '', isPublic: false };
store.clearError();
createDialog.value = true;
}
async function submitCreate(): Promise<void> {
const name = form.value.displayName.trim();
if (name.length === 0) {
return;
}
try {
await store.createRoom({ displayName: name, isPublic: form.value.isPublic });
createDialog.value = false;
$q.notify({ type: 'positive', message: 'Комната создана, секретарь подключён' });
} catch (err) {
$q.notify({ type: 'negative', message: extractError(err) });
}
}
function confirmRemove(room: ISecretaryRoom): void {
$q.dialog({
title: 'Удалить комнату секретаря?',
message: `Секретарь выйдет из «${room.displayLabel}», комната перестанет транскрибироваться и синхронизироваться.`,
cancel: { label: 'Отмена', flat: true, noCaps: true },
ok: { label: 'Удалить', color: 'negative', noCaps: true, unelevated: true },
persistent: true,
}).onOk(async () => {
try {
await store.removeRoom(room.matrixRoomId);
$q.notify({ type: 'positive', message: 'Комната удалена' });
} catch (err) {
$q.notify({ type: 'negative', message: extractError(err) });
}
});
}
function extractError(err: unknown): string {
if (err instanceof Error) {
return err.message;
}
return 'Не удалось выполнить операцию';
}
async function handleRefresh(): Promise<void> {
await store.loadRooms();
}
onMounted(async () => {
await store.loadRooms();
});
</script>
<style scoped>
.sr-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.sr-head__title {
margin: 0;
font-size: 1.35rem;
font-weight: 600;
letter-spacing: -0.02em;
line-height: 1.25;
}
.sr-head__subtitle {
margin: 6px 0 0;
font-size: 0.8125rem;
line-height: 1.45;
color: rgba(0, 0, 0, 0.55);
max-width: 48rem;
}
.body--dark .sr-head__subtitle {
color: rgba(255, 255, 255, 0.55);
}
.sr-head__actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.sr-name {
font-weight: 500;
line-height: 1.35;
}
.sr-room-id {
margin-top: 2px;
font-size: 0.75rem;
color: rgba(0, 0, 0, 0.45);
word-break: break-all;
}
.body--dark .sr-room-id {
color: rgba(255, 255, 255, 0.45);
}
.sr-dialog {
width: 460px;
max-width: 90vw;
}
</style>
@@ -0,0 +1 @@
export { default as SecretaryRoomsPage } from './SecretaryRoomsPage.vue'
@@ -1,5 +1,6 @@
export { CalendarPage } from './CalendarPage';
export { ChatCoopPage } from './ChatCoopPage';
export { MobileClientPage } from './MobileClientPage';
export { SecretaryRoomsPage } from './SecretaryRoomsPage';
export { TranscriptionsPage } from './TranscriptionsPage';
export { TranscriptionDetailPage } from './TranscriptionDetailPage';
@@ -12,6 +12,14 @@ import { PowerupLogItem } from '../../../widgets/PowerupLogItem'
<style lang="scss" scoped>
.logs-page {
height: 100%;
padding: var(--p-6, 24px);
max-width: 1200px;
margin: 0 auto;
}
@media (max-width: 768px) {
.logs-page {
padding: var(--p-4, 16px);
}
}
</style>
@@ -1,62 +1,73 @@
<template lang="pug">
div.monitor-page
// Ресурсные карточки в ряд
.resources-row
.resources-container
.resource-col
CpuResourceWidget
.resource-col
NetResourceWidget
.resource-col
RamResourceWidget
.monitor-page
//- Предупреждение об исчерпании квот баннером на всю ширину сверху.
BaseBanner.monitor-page__alert(variant="warn")
template(#icon)
q-icon(name="warning", size="20px")
| Не допускайте исчерпание квот ресурсов это заблокирует обработку новых документов в кооперативе.
// Кошелек AXON
div(style="margin-top: 50px;")
.row.justify-center
.col-xs-12.col-sm-6.col-md-5.q-pa-md
AxonWalletCard
.col-xs-12.col-sm-6.col-md-5.q-pa-md
ResourceInfoWidget
//- Верхний ряд: слева (2/3) «как это работает», справа (1/3) кошелёк AXON.
//- На узком экране перестраивается в одну колонку: сначала описание, затем кошелёк.
.monitor-page__top
.monitor-page__info
ResourceInfoWidget
.monitor-page__wallet
AxonWalletCard
//- Ресурсы в одну строку (CPU / NET / RAM); на узком экране в колонку.
.monitor-page__resources
CpuResourceWidget
NetResourceWidget
RamResourceWidget
</template>
<style lang="scss" scoped>
.monitor-page {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.resources-row {
.resources-container {
display: flex;
justify-content: space-around;
gap: 2rem;
flex-wrap: wrap;
@media (max-width: 1024px) {
flex-direction: column;
align-items: center;
gap: 1.5rem;
}
}
.resource-col {
flex: 0 0 auto;
max-width: 320px;
width: 100%;
@media (max-width: 1024px) {
max-width: 100%;
}
}
}
</style>
<script lang="ts" setup>
import { BaseBanner } from 'src/shared/ui/base/BaseBanner'
import CpuResourceWidget from '../../../widgets/CpuResourceWidget'
import NetResourceWidget from '../../../widgets/NetResourceWidget'
import RamResourceWidget from '../../../widgets/RamResourceWidget'
import ResourceInfoWidget from '../../../widgets/ResourceInfoWidget'
import AxonWalletCard from '../../../widgets/AxonWalletCard'
</script>
<style lang="scss" scoped>
.monitor-page {
display: flex;
flex-direction: column;
gap: var(--p-5, 20px);
padding: var(--p-6, 24px);
max-width: 1200px;
margin: 0 auto;
}
.monitor-page__alert {
width: 100%;
}
.monitor-page__top {
display: grid;
grid-template-columns: 2fr 1fr;
gap: var(--p-5, 20px);
align-items: stretch;
}
.monitor-page__resources {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--p-5, 20px);
align-items: stretch;
}
@media (max-width: 1024px) {
.monitor-page__top,
.monitor-page__resources {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.monitor-page {
padding: var(--p-4, 16px);
}
}
</style>
@@ -2,7 +2,7 @@
.settings-page
.settings-content(v-if='extension')
.settings-container.q-pa-md
.settings-container
ExtensionSettings(
:schema='extension.schema',
:config='extension.config',
@@ -112,7 +112,7 @@ const saveSettings = async () => {
<style lang="scss" scoped>
.settings-page {
padding: 1rem;
padding: var(--p-6, 24px);
max-width: 1200px;
margin: 0 auto;
}
@@ -123,4 +123,10 @@ const saveSettings = async () => {
margin: 0 auto;
}
}
@media (max-width: 768px) {
.settings-page {
padding: var(--p-4, 16px);
}
}
</style>
@@ -0,0 +1,127 @@
/* Канон-стиль ресурсной карточки монитора (CPU / NET / RAM).
Общий для трёх виджетов каждый переопределяет лишь акцент иконки.
Плоская surface-карточка вместо «переворачивающейся» 3D-карты:
детали раскрываются inline через q-slide-transition, ничего не прыгает. */
.resource-card {
display: flex;
flex-direction: column;
gap: var(--p-4, 16px);
background: var(--p-surface);
border: 1px solid var(--p-line);
border-radius: var(--p-r-lg, 16px);
padding: var(--p-5, 20px);
height: 100%;
}
.resource-card__head {
display: flex;
align-items: center;
gap: var(--p-3, 12px);
}
.resource-card__icon {
width: 40px;
height: 40px;
flex: 0 0 40px;
border-radius: var(--p-r-md, 12px);
display: flex;
align-items: center;
justify-content: center;
i {
font-size: 1.1rem;
}
}
.resource-card__title {
font-size: var(--p-fs-h3, 15px);
font-weight: 600;
color: var(--p-ink);
}
.resource-card__gauge {
display: flex;
justify-content: center;
padding: var(--p-2, 8px) 0;
}
.gauge-inner {
display: flex;
flex-direction: column;
align-items: center;
.gauge-value {
font-size: 1.1rem;
font-weight: 700;
line-height: 1.2;
color: var(--p-ink);
}
.gauge-label {
font-size: var(--p-fs-meta, 12px);
color: var(--p-ink-3);
margin-top: 2px;
}
}
.resource-card__note {
background: var(--p-surface-2);
border-radius: var(--p-r-md, 12px);
padding: var(--p-3, 12px) var(--p-4, 16px);
color: var(--p-ink-2);
font-size: var(--p-fs-body-sm, 13px);
line-height: 1.5;
}
.resource-card__toggle {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--p-1, 4px);
align-self: center;
margin-top: auto;
padding: var(--p-1, 4px) var(--p-2, 8px);
background: none;
border: 0;
cursor: pointer;
color: var(--p-primary);
font-size: var(--p-fs-body-sm, 13px);
font-weight: 500;
border-radius: var(--p-r-sm, 8px);
transition: background var(--p-dur-fast, 120ms) var(--p-ease-standard);
&:hover {
background: var(--p-primary-soft);
}
}
.resource-card__details {
border-top: 1px solid var(--p-line);
padding-top: var(--p-2, 8px);
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--p-3, 12px);
padding: var(--p-2, 8px) 0;
border-bottom: 1px solid var(--p-line);
&:last-child {
border-bottom: none;
}
}
.detail-label {
font-size: var(--p-fs-body-sm, 13px);
color: var(--p-ink-2);
}
.detail-value {
font-size: var(--p-fs-mono, 13px);
font-weight: 600;
color: var(--p-ink);
font-family: var(--p-mono);
text-align: right;
}
}
@@ -1,34 +1,29 @@
<template lang="pug">
div.axon-wallet-card
.widget-header
.widget-icon
.axon-wallet-card
.axon-wallet-card__head
.axon-wallet-card__icon
i.fas.fa-wallet
.widget-title Кошелек AXON
.axon-wallet-card__title Кошелёк AXON
.wallet-balance
.balance-value {{ formattedBalance }}
.balance-label Доступно
.axon-wallet-card__balance
.axon-wallet-card__balance-value {{ formattedBalance }}
.axon-wallet-card__balance-label Доступно
.axon-details
.axon-wallet-card__details
.detail-row
.detail-label Минимальных квот:
.detail-value {{ minQuotasDays }} {{ minQuotasDaysText }}
span.detail-label Минимальных квот
span.detail-value {{ minQuotasDays }} {{ minQuotasDaysText }}
.detail-row
.detail-label Новых пайщиков:
.detail-value {{ maxNewMembers }} {{ maxNewMembersText }}
span.detail-label Новых пайщиков
span.detail-value {{ maxNewMembers }} {{ maxNewMembersText }}
ColorCard(color='purple')
.axon-wallet-card__note
| AXON используется для аренды вычислительных ресурсов (минимум 5 AXON/день) и регистрации пайщиков (1 AXON/аккаунт). Курс: 1 AXON = 10 RUB.
q-btn.wallet-btn(
flat
no-caps
size="md"
color="primary"
icon="open_in_new"
label="Пополнить у оператора"
@click="goToReplenishment"
)
BaseButton.axon-wallet-card__action(variant="primary", size="sm", block, @click="goToReplenishment")
template(#icon-left)
q-icon(name="open_in_new", size="16px")
| Пополнить у оператора
</template>
<script setup lang="ts">
@@ -36,7 +31,7 @@ import { computed } from 'vue';
import { useSystemStore } from 'src/entities/System/model';
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
import { pluralizeDays, pluralizeAccounts } from 'src/shared/lib/utils';
import { ColorCard } from 'src/shared/ui';
import { BaseButton } from 'src/shared/ui/base/BaseButton';
const systemStore = useSystemStore();
@@ -79,95 +74,102 @@ const goToReplenishment = () => {
</script>
<style lang="scss" scoped>
/* Канон-карточка кошелька AXON: плоская surface, без градиента/теней. */
.axon-wallet-card {
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #e0e6ed;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
gap: var(--p-4, 16px);
background: var(--p-surface);
border: 1px solid var(--p-line);
border-radius: var(--p-r-lg, 16px);
padding: var(--p-5, 20px);
height: 100%;
min-width: 320px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
}
}
.widget-header {
.axon-wallet-card__head {
display: flex;
align-items: center;
margin-bottom: 1.5rem;
gap: var(--p-3, 12px);
}
.widget-icon {
width: 40px;
height: 40px;
border-radius: 12px;
background: linear-gradient(135deg, #8e44ad 0%, #9b59b6 100%);
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
.axon-wallet-card__icon {
width: 40px;
height: 40px;
flex: 0 0 40px;
border-radius: var(--p-r-md, 12px);
display: flex;
align-items: center;
justify-content: center;
background: var(--prog-generator-soft);
color: var(--prog-generator);
i {
color: white;
font-size: 1.2rem;
}
}
.widget-title {
i {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
}
.wallet-balance {
text-align: center;
margin-bottom: 1rem;
.axon-wallet-card__title {
font-size: var(--p-fs-h3, 15px);
font-weight: 600;
color: var(--p-ink);
}
.balance-value {
font-size: 2rem;
.axon-wallet-card__balance {
text-align: center;
padding: var(--p-2, 8px) 0;
.axon-wallet-card__balance-value {
font-size: 1.9rem;
font-weight: 700;
line-height: 1.2;
margin-bottom: 0.5rem;
color: var(--p-ink);
}
.balance-label {
font-size: 0.8rem;
opacity: 0.7;
.axon-wallet-card__balance-label {
font-size: var(--p-fs-meta, 12px);
color: var(--p-ink-3);
margin-top: var(--p-1, 4px);
}
}
.axon-details {
margin-bottom: 1rem;
.axon-wallet-card__details {
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #f1f3f4;
gap: var(--p-3, 12px);
padding: var(--p-2, 8px) 0;
border-bottom: 1px solid var(--p-line);
&:last-child {
border-bottom: none;
}
}
.detail-label {
font-size: 0.85rem;
font-weight: 500;
}
.detail-label {
font-size: var(--p-fs-body-sm, 13px);
color: var(--p-ink-2);
}
.detail-value {
font-size: 0.85rem;
font-weight: 600;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
}
.detail-value {
font-size: var(--p-fs-mono, 13px);
font-weight: 600;
color: var(--p-ink);
font-family: var(--p-mono);
text-align: right;
}
}
.wallet-btn {
margin-top: 1rem;
width: 100%;
.axon-wallet-card__note {
background: var(--p-surface-2);
border-radius: var(--p-r-md, 12px);
padding: var(--p-3, 12px) var(--p-4, 16px);
color: var(--p-ink-2);
font-size: var(--p-fs-body-sm, 13px);
line-height: 1.5;
}
.axon-wallet-card__action {
margin-top: auto;
}
</style>
@@ -1,231 +1,49 @@
<template lang="pug">
div.cpu-resource-widget
.card-flipper(:class="{ flipped: showDetails }")
.card-front.front
.card-content
.widget-header
.widget-icon
i.fas.fa-microchip
.widget-title Время процессора (CPU)
.resource-card
.resource-card__head
.resource-card__icon
i.fas.fa-microchip
.resource-card__title Время процессора (CPU)
.widget-content
q-circular-progress(
:value="usagePercent"
size="180px"
:thickness="0.35"
color="primary"
track-color="grey-3"
show-value
:class="progressClass"
)
.progress-content
.usage-value {{ usagePercent.toFixed(2) }}%
.usage-label Использовано
.resource-card__gauge
q-circular-progress(
:value="usagePercent"
size="148px"
:thickness="0.14"
:color="gaugeColor"
track-color="grey-3"
rounded
show-value
)
.gauge-inner
.gauge-value {{ usagePercent.toFixed(2) }}%
.gauge-label Использовано
ColorCard(color='blue')
| CPU расходуется для вычислений при обработке пакетов документов.
.resource-card__note CPU расходуется для вычислений при обработке пакетов документов.
q-btn.flip-btn(
flat
dense
no-caps
size="sm"
label="Подробнее"
@click="showDetails = !showDetails"
)
button.resource-card__toggle(type="button", @click="showDetails = !showDetails")
span {{ showDetails ? 'Скрыть детали' : 'Подробнее' }}
q-icon(:name="showDetails ? 'expand_less' : 'expand_more'", size="18px")
.card-back.back
.card-content
.widget-header
.widget-icon
i.fas.fa-microchip
.widget-title Детали CPU
.resource-details
.detail-row
.detail-label Используется:
.detail-value {{ formatValue(currentUsed) }} μs
.detail-row
.detail-label Доступно:
.detail-value {{ formatValue(available) }} μs ({{ availablePercent.toFixed(1) }}%)
.detail-row
.detail-label Максимум:
.detail-value {{ formatValue(max) }} μs
.detail-row
.detail-label Восстановление:
.detail-value ~{{ formatTime(estimatedRecovery) }}
q-btn.flip-btn(
flat
dense
no-caps
size="sm"
label="Назад"
@click="showDetails = !showDetails"
)
q-slide-transition
.resource-card__details(v-show="showDetails")
.detail-row
span.detail-label Используется
span.detail-value {{ formatValue(currentUsed) }} μs
.detail-row
span.detail-label Доступно
span.detail-value {{ formatValue(available) }} μs ({{ availablePercent.toFixed(1) }}%)
.detail-row
span.detail-label Максимум
span.detail-value {{ formatValue(max) }} μs
.detail-row
span.detail-label Восстановление
span.detail-value ~{{ formatTime(estimatedRecovery) }}
</template>
<style lang="scss" scoped>
.cpu-resource-widget {
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #e0e6ed;
transition: all 0.3s ease;
min-width: 320px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
}
}
.widget-header {
display: flex;
align-items: center;
margin-bottom: 1.5rem;
.widget-icon {
width: 40px;
height: 40px;
border-radius: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
i {
color: white;
font-size: 1.2rem;
}
}
.widget-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
}
.widget-content {
text-align: center;
.q-circular-progress {
margin: 0 auto 1.5rem;
&.high-usage {
::v-deep(.q-circular-progress__track) {
stroke: #f39c12 !important;
}
}
&.critical-usage {
::v-deep(.q-circular-progress__track) {
stroke: #e74c3c !important;
}
}
}
.progress-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
.usage-value {
font-size: 1.1rem;
font-weight: 700;
line-height: 1.2;
}
.usage-label {
font-size: 0.8rem;
margin-top: 2px;
}
}
}
.resource-details {
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #f1f3f4;
&:last-child {
border-bottom: none;
}
.detail-label {
font-size: 0.85rem;
font-weight: 500;
}
.detail-value {
font-size: 0.85rem;
font-weight: 600;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
}
}
}
.card-flipper {
position: relative;
perspective: 1000px;
min-height: 450px;
width: 100%;
display: flex;
flex-direction: column;
.front,
.back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
transition: transform 0.8s ease-in-out;
display: flex;
flex-direction: column;
}
.front {
transform: rotateY(0deg);
}
.back {
transform: rotateY(180deg);
}
&.flipped {
.front {
transform: rotateY(-180deg);
}
.back {
transform: rotateY(0deg);
}
}
}
.card-content {
flex: 1;
}
.flip-btn {
margin-top: auto;
align-self: center;
}
</style>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { useSystemStore } from 'src/entities/System/model'
import { ColorCard } from 'src/shared/ui'
const showDetails = ref(false)
@@ -265,11 +83,13 @@ const availablePercent = computed(() => {
return (available.value / max.value) * 100
})
const progressClass = computed(() => {
// Цвет кольца: канон-статусы. Базовый primary; при перегрузке квоты
// переключаемся на предупреждающий и негативный.
const gaugeColor = computed(() => {
const percent = usagePercent.value
if (percent >= 90) return 'critical-usage'
if (percent >= 70) return 'high-usage'
return ''
if (percent >= 90) return 'negative'
if (percent >= 70) return 'warning'
return 'primary'
})
// Примерная оценка времени восстановления (упрощенная логика)
@@ -305,3 +125,12 @@ const formatTime = (seconds: number): string => {
}
}
</script>
<style lang="scss" scoped>
@use './../../../shared/resource-card.scss';
.resource-card__icon {
background: var(--p-primary-soft);
color: var(--p-primary);
}
</style>
@@ -1,231 +1,49 @@
<template lang="pug">
div.net-resource-widget
.card-flipper(:class="{ flipped: showDetails }")
.card-front.front
.card-content
.widget-header
.widget-icon
i.fas.fa-network-wired
.widget-title Сетевой трафик (NET)
.resource-card
.resource-card__head
.resource-card__icon
i.fas.fa-network-wired
.resource-card__title Сетевой трафик (NET)
.widget-content
q-circular-progress(
:value="usagePercent"
size="180px"
:thickness="0.35"
color="secondary"
track-color="grey-3"
show-value
:class="progressClass"
)
.progress-content
.usage-value {{ usagePercent.toFixed(2) }}%
.usage-label Использовано
.resource-card__gauge
q-circular-progress(
:value="usagePercent"
size="148px"
:thickness="0.14"
:color="gaugeColor"
track-color="grey-3"
rounded
show-value
)
.gauge-inner
.gauge-value {{ usagePercent.toFixed(2) }}%
.gauge-label Использовано
ColorCard(color='green')
| NET используется для передачи данных документов между узлами блокчейна.
.resource-card__note NET используется для передачи данных документов между узлами блокчейна.
q-btn.flip-btn(
flat
dense
no-caps
size="sm"
label="Подробнее"
@click="showDetails = !showDetails"
)
button.resource-card__toggle(type="button", @click="showDetails = !showDetails")
span {{ showDetails ? 'Скрыть детали' : 'Подробнее' }}
q-icon(:name="showDetails ? 'expand_less' : 'expand_more'", size="18px")
.card-back.back
.card-content
.widget-header
.widget-icon
i.fas.fa-network-wired
.widget-title Детали NET
.resource-details
.detail-row
.detail-label Используется:
.detail-value {{ formatBytes(currentUsed) }}
.detail-row
.detail-label Доступно:
.detail-value {{ formatBytes(available) }} ({{ availablePercent.toFixed(1) }}%)
.detail-row
.detail-label Максимум:
.detail-value {{ formatBytes(max) }}
.detail-row
.detail-label Восстановление:
.detail-value ~{{ formatTime(estimatedRecovery) }}
q-btn.flip-btn(
flat
dense
no-caps
size="sm"
label="Назад"
@click="showDetails = !showDetails"
)
q-slide-transition
.resource-card__details(v-show="showDetails")
.detail-row
span.detail-label Используется
span.detail-value {{ formatBytes(currentUsed) }}
.detail-row
span.detail-label Доступно
span.detail-value {{ formatBytes(available) }} ({{ availablePercent.toFixed(1) }}%)
.detail-row
span.detail-label Максимум
span.detail-value {{ formatBytes(max) }}
.detail-row
span.detail-label Восстановление
span.detail-value ~{{ formatTime(estimatedRecovery) }}
</template>
<style lang="scss" scoped>
.net-resource-widget {
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #e0e6ed;
transition: all 0.3s ease;
min-width: 320px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
}
}
.widget-header {
display: flex;
align-items: center;
margin-bottom: 1.5rem;
.widget-icon {
width: 40px;
height: 40px;
border-radius: 12px;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
i {
color: white;
font-size: 1.2rem;
}
}
.widget-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
}
.widget-content {
text-align: center;
.q-circular-progress {
margin: 0 auto 1.5rem;
&.high-usage {
::v-deep(.q-circular-progress__track) {
stroke: #f39c12 !important;
}
}
&.critical-usage {
::v-deep(.q-circular-progress__track) {
stroke: #e74c3c !important;
}
}
}
.progress-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
.usage-value {
font-size: 1.1rem;
font-weight: 700;
line-height: 1.2;
}
.usage-label {
font-size: 0.8rem;
margin-top: 2px;
}
}
}
.resource-details {
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #f1f3f4;
&:last-child {
border-bottom: none;
}
.detail-label {
font-size: 0.85rem;
font-weight: 500;
}
.detail-value {
font-size: 0.85rem;
font-weight: 600;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
}
}
}
.card-flipper {
position: relative;
perspective: 1000px;
min-height: 450px;
width: 100%;
display: flex;
flex-direction: column;
.front,
.back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
transition: transform 0.8s ease-in-out;
display: flex;
flex-direction: column;
}
.front {
transform: rotateY(0deg);
}
.back {
transform: rotateY(180deg);
}
&.flipped {
.front {
transform: rotateY(-180deg);
}
.back {
transform: rotateY(0deg);
}
}
}
.card-content {
flex: 1;
}
.flip-btn {
margin-top: auto;
align-self: center;
}
</style>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { useSystemStore } from 'src/entities/System/model'
import { ColorCard } from 'src/shared/ui'
const showDetails = ref(false)
@@ -265,11 +83,13 @@ const availablePercent = computed(() => {
return (available.value / max.value) * 100
})
const progressClass = computed(() => {
// Цвет кольца: канон-статусы. Базовый info; при перегрузке квоты
// переключаемся на предупреждающий и негативный.
const gaugeColor = computed(() => {
const percent = usagePercent.value
if (percent >= 90) return 'critical-usage'
if (percent >= 70) return 'high-usage'
return ''
if (percent >= 90) return 'negative'
if (percent >= 70) return 'warning'
return 'info'
})
// Примерная оценка времени восстановления (упрощенная логика)
@@ -306,3 +126,12 @@ const formatTime = (seconds: number): string => {
}
}
</script>
<style lang="scss" scoped>
@use './../../../shared/resource-card.scss';
.resource-card__icon {
background: var(--p-info-soft);
color: var(--p-info);
}
</style>
@@ -1,33 +1,30 @@
<template lang="pug">
div.log-details
.log-details
.detail-row
.detail-label Тип операции:
.detail-value
q-chip(
:color="getTypeColor(log.type)"
text-color="white"
size="sm"
dense
) {{ getTypeLabel(log.type) }}
span.detail-label Тип операции
span.detail-value
BaseBadge(:variant="getTypeVariant(log.type)") {{ getTypeLabel(log.type) }}
.detail-row
.detail-label Сумма:
.detail-value {{ log.amount }}
span.detail-label Сумма
span.detail-value {{ log.amount }}
.detail-row
.detail-label RAM:
.detail-value {{ formatBytes(log.resources.ram_usage) }} / {{ formatBytes(log.resources.ram_quota) }} ({{ calculateRamPercent(log.resources.ram_usage, log.resources.ram_quota).toFixed(2) }}% использовано)
span.detail-label RAM
span.detail-value {{ formatBytes(log.resources.ram_usage) }} / {{ formatBytes(log.resources.ram_quota) }} ({{ calculateRamPercent(log.resources.ram_usage, log.resources.ram_quota).toFixed(2) }}% использовано)
.detail-row
.detail-label CPU:
.detail-value {{ formatCpuNet(log.resources.cpu_limit) }} ({{ calculateCpuNetPercent(log.resources.cpu_limit).toFixed(2) }}% использовано)
span.detail-label CPU
span.detail-value {{ formatCpuNet(log.resources.cpu_limit) }} ({{ calculateCpuNetPercent(log.resources.cpu_limit).toFixed(2) }}% использовано)
.detail-row
.detail-label NET:
.detail-value {{ formatCpuNet(log.resources.net_limit) }} ({{ calculateCpuNetPercent(log.resources.net_limit).toFixed(2) }}% использовано)
span.detail-label NET
span.detail-value {{ formatCpuNet(log.resources.net_limit) }} ({{ calculateCpuNetPercent(log.resources.net_limit).toFixed(2) }}% использовано)
</template>
<script lang="ts" setup>
import { BaseBadge } from 'src/shared/ui/base/BaseBadge'
import type { BaseBadgeVariant } from 'src/shared/ui/base/BaseBadge'
interface PowerupLog {
type: 'daily' | 'now'
@@ -52,8 +49,8 @@ const getTypeLabel = (type: string) => {
return type === 'daily' ? 'Ежедневное пополнение' : 'Немедленное пополнение'
}
const getTypeColor = (type: string) => {
return type === 'daily' ? 'green' : 'blue'
const getTypeVariant = (type: string): BaseBadgeVariant => {
return type === 'daily' ? 'pos' : 'info'
}
const formatBytes = (value: any) => {
@@ -102,23 +99,26 @@ const calculateCpuNetPercent = (resource: any) => {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid rgba(241, 243, 244, 0.3);
gap: var(--p-3, 12px);
padding: var(--p-2, 8px) 0;
border-bottom: 1px solid var(--p-line);
&:last-child {
border-bottom: none;
}
}
.detail-label {
font-size: 0.85rem;
font-weight: 500;
}
.detail-label {
font-size: var(--p-fs-body-sm, 13px);
color: var(--p-ink-2);
}
.detail-value {
font-size: 0.85rem;
font-weight: 600;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
}
.detail-value {
font-size: var(--p-fs-mono, 13px);
font-weight: 600;
color: var(--p-ink);
font-family: var(--p-mono);
text-align: right;
}
}
</style>
@@ -1,231 +1,49 @@
<template lang="pug">
div.ram-resource-widget
.card-flipper(:class="{ flipped: showDetails }")
.card-front.front
.card-content
.widget-header
.widget-icon
i.fas.fa-memory
.widget-title Оперативная память (RAM)
.resource-card
.resource-card__head
.resource-card__icon
i.fas.fa-memory
.resource-card__title Оперативная память (RAM)
.widget-content
q-circular-progress(
:value="usagePercent"
size="180px"
:thickness="0.35"
color="positive"
track-color="grey-3"
show-value
:class="progressClass"
)
.progress-content
.usage-value {{ usagePercent.toFixed(2) }}%
.usage-label Использовано
.resource-card__gauge
q-circular-progress(
:value="usagePercent"
size="148px"
:thickness="0.14"
:color="gaugeColor"
track-color="grey-3"
rounded
show-value
)
.gauge-inner
.gauge-value {{ usagePercent.toFixed(2) }}%
.gauge-label Использовано
ColorCard(color='orange')
| RAM используется для временного хранения данных пайщиков при исполнении смарт-контрактов.
.resource-card__note RAM используется для временного хранения данных пайщиков при исполнении смарт-контрактов.
q-btn.flip-btn(
flat
dense
no-caps
size="sm"
label="Подробнее"
@click="showDetails = !showDetails"
)
button.resource-card__toggle(type="button", @click="showDetails = !showDetails")
span {{ showDetails ? 'Скрыть детали' : 'Подробнее' }}
q-icon(:name="showDetails ? 'expand_less' : 'expand_more'", size="18px")
.card-back.back
.card-content
.widget-header
.widget-icon
i.fas.fa-memory
.widget-title Детали RAM
.resource-details
.detail-row
.detail-label Используется:
.detail-value {{ formatBytes(ramUsage) }}
.detail-row
.detail-label Доступно:
.detail-value {{ formatBytes(available) }} ({{ availablePercent.toFixed(1) }}%)
.detail-row
.detail-label Квота:
.detail-value {{ formatBytes(ramQuota) }}
.detail-row
.detail-label Стоимость:
.detail-value {{ formatCost(estimatedCost) }}
q-btn.flip-btn(
flat
dense
no-caps
size="sm"
label="Назад"
@click="showDetails = !showDetails"
)
q-slide-transition
.resource-card__details(v-show="showDetails")
.detail-row
span.detail-label Используется
span.detail-value {{ formatBytes(ramUsage) }}
.detail-row
span.detail-label Доступно
span.detail-value {{ formatBytes(available) }} ({{ availablePercent.toFixed(1) }}%)
.detail-row
span.detail-label Квота
span.detail-value {{ formatBytes(ramQuota) }}
.detail-row
span.detail-label Стоимость
span.detail-value {{ formatCost(estimatedCost) }}
</template>
<style lang="scss" scoped>
.ram-resource-widget {
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #e0e6ed;
transition: all 0.3s ease;
min-width: 320px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
}
}
.widget-header {
display: flex;
align-items: center;
margin-bottom: 1.5rem;
.widget-icon {
width: 40px;
height: 40px;
border-radius: 12px;
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
i {
color: white;
font-size: 1.2rem;
}
}
.widget-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
}
.widget-content {
text-align: center;
.q-circular-progress {
margin: 0 auto 1.5rem;
&.high-usage {
::v-deep(.q-circular-progress__track) {
stroke: #f39c12 !important;
}
}
&.critical-usage {
::v-deep(.q-circular-progress__track) {
stroke: #e74c3c !important;
}
}
}
.progress-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
.usage-value {
font-size: 1.1rem;
font-weight: 700;
line-height: 1.2;
}
.usage-label {
font-size: 0.8rem;
margin-top: 2px;
}
}
}
.resource-details {
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #f1f3f4;
&:last-child {
border-bottom: none;
}
.detail-label {
font-size: 0.85rem;
font-weight: 500;
}
.detail-value {
font-size: 0.85rem;
font-weight: 600;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
}
}
}
.card-flipper {
position: relative;
perspective: 1000px;
min-height: 450px;
width: 100%;
display: flex;
flex-direction: column;
.front,
.back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
transition: transform 0.8s ease-in-out;
display: flex;
flex-direction: column;
}
.front {
transform: rotateY(0deg);
}
.back {
transform: rotateY(180deg);
}
&.flipped {
.front {
transform: rotateY(-180deg);
}
.back {
transform: rotateY(0deg);
}
}
}
.card-content {
flex: 1;
}
.flip-btn {
margin-top: auto;
align-self: center;
}
</style>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { useSystemStore } from 'src/entities/System/model'
import { ColorCard } from 'src/shared/ui'
const showDetails = ref(false)
@@ -253,11 +71,13 @@ const availablePercent = computed(() => {
return (available.value / ramQuota.value) * 100
})
const progressClass = computed(() => {
// Цвет кольца: канон-статусы. Базовый positive; при перегрузке квоты
// переключаемся на предупреждающий и негативный.
const gaugeColor = computed(() => {
const percent = usagePercent.value
if (percent >= 95) return 'critical-usage'
if (percent >= 80) return 'high-usage'
return ''
if (percent >= 95) return 'negative'
if (percent >= 80) return 'warning'
return 'positive'
})
// Примерная оценка стоимости дополнительного RAM (упрощенная логика)
@@ -288,3 +108,12 @@ const formatCost = (cost: number): string => {
}
}
</script>
<style lang="scss" scoped>
@use './../../../shared/resource-card.scss';
.resource-card__icon {
background: var(--p-pos-soft);
color: var(--p-pos);
}
</style>
@@ -1,26 +1,23 @@
<template lang="pug">
.resource-info-widget
.resource-info-card
ColorCard(color='orange')
| Внимание! Не допускайте исчерпание квот ресурсов это заблокирует возможность обработки новых документов в кооперативе.
.resource-info-card
.resource-info-card__head
.resource-info-card__icon
q-icon(name="help_outline", size="20px")
.resource-info-card__title Как это работает
.resource-info-description
.resource-info-card__body
p.resource-info-card__lead
| Вычислительные ресурсы арендуются на 24 часа у делегатов блокчейн-платформы. Минимальная квота 5 AXON в сутки.
p.resource-info-card__text
| Когда использование любого ресурса превышает 70% от доступной квоты, система автоматически арендует
| дополнительную квоту за 5 AXON из вашего баланса.
div.full-width.text-right
q-btn(
dense
flat
color="white"
label="Как это работает"
icon="help_outline"
@click="showHowItWorksDialog = true"
).q-ml-lg
BaseButton.resource-info-card__action(variant="secondary", size="sm", @click="showHowItWorksDialog = true")
template(#icon-left)
q-icon(name="menu_book", size="16px")
| Подробнее о системе ресурсов
// Диалог "Как это работает"
//- Диалог "Как это работает"
q-dialog(v-model="showHowItWorksDialog")
ModalBase(title="Как работает система ресурсов", :style="dialogStyle")
.q-pa-md
@@ -234,18 +231,13 @@
| Планируйте расходы заранее и отслеживайте фактическое потребление, чтобы оптимизировать затраты.
template(#actions)
q-btn(
flat
label="Закрыть"
color="primary"
v-close-popup
)
BaseButton(variant="ghost", v-close-popup) Закрыть
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ModalBase, ColorCard } from 'src/shared/ui'
import { ModalBase } from 'src/shared/ui'
import { BaseButton } from 'src/shared/ui/base/BaseButton'
// Диалог "Как это работает"
const showHowItWorksDialog = ref(false)
@@ -259,80 +251,80 @@ const dialogStyle = ref({
</script>
<style lang="scss" scoped>
.resource-info-widget {
.resource-info-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2rem;
color: white;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
.resource-info-header {
margin-bottom: 1.5rem;
.resource-info-title {
color: white;
margin-bottom: 0;
font-size: 2.5rem;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
}
.resource-info-description {
color: rgba(255, 255, 255, 0.9);
font-size: 1.1rem;
line-height: 1.6;
margin-bottom: 1.5rem;
}
:deep(.color-card) {
font-size: 1.1rem;
line-height: 1.5;
font-weight: 600;
}
.resource-actions {
.how-it-works-btn {
margin-top: 0.5rem;
&:hover {
background: rgba(255, 255, 255, 0.1);
}
}
}
@media (max-width: 768px) {
padding: 1.5rem;
.resource-info-header {
.resource-info-title {
font-size: 2rem;
}
}
.resource-info-description {
font-size: 1rem;
}
}
}
/* Канон-карточка «Как это работает»: плоская surface, без градиента. */
.resource-info-card {
display: flex;
flex-direction: column;
gap: var(--p-4, 16px);
background: var(--p-surface);
border: 1px solid var(--p-line);
border-radius: var(--p-r-lg, 16px);
padding: var(--p-5, 20px);
height: 100%;
}
.how-it-works-content {
.intro-section,
.resources-section,
.automation-section,
.conclusion-section {
.text-h6 {
font-weight: 600;
margin-bottom: 1rem;
}
.resource-info-card__head {
display: flex;
align-items: center;
gap: var(--p-3, 12px);
}
p {
line-height: 1.6;
}
.resource-info-card__icon {
width: 40px;
height: 40px;
flex: 0 0 40px;
border-radius: var(--p-r-md, 12px);
display: flex;
align-items: center;
justify-content: center;
background: var(--p-primary-soft);
color: var(--p-primary);
}
.resource-info-card__title {
font-size: var(--p-fs-h2, 18px);
font-weight: 600;
color: var(--p-ink);
}
.resource-info-card__body {
display: flex;
flex-direction: column;
gap: var(--p-2, 8px);
}
.resource-info-card__lead {
margin: 0;
font-size: var(--p-fs-body, 14px);
font-weight: 500;
color: var(--p-ink);
line-height: 1.55;
}
.resource-info-card__text {
margin: 0;
font-size: var(--p-fs-body-sm, 13px);
color: var(--p-ink-2);
line-height: 1.55;
}
.resource-info-card__action {
align-self: flex-start;
margin-top: auto;
}
/* — Содержимое диалога — канон-токены вместо хардкод-цветов — */
.how-it-works-content {
color: var(--p-ink-1);
.text-h6 {
font-weight: 600;
margin-bottom: var(--p-4, 16px);
color: var(--p-ink);
}
p {
line-height: 1.6;
}
.resource-explanation,
@@ -340,90 +332,64 @@ const dialogStyle = ref({
.explanation-item {
.text-subtitle1 {
font-weight: 600;
margin-bottom: 0.5rem;
margin-bottom: var(--p-2, 8px);
color: var(--p-ink);
}
p {
margin-left: 1.5rem;
padding-left: 0.5rem;
border-left: 3px solid #667eea;
background: rgba(102, 126, 234, 0.05);
padding: 0.5rem 0 0.5rem 1rem;
margin-left: var(--p-6, 24px);
border-left: 3px solid var(--p-primary-line);
background: var(--p-primary-soft);
border-radius: 0 var(--p-r-sm, 8px) var(--p-r-sm, 8px) 0;
padding: var(--p-2, 8px) 0 var(--p-2, 8px) var(--p-4, 16px);
line-height: 1.6;
}
}
}
.important-note {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 8px;
padding: 1rem;
background: var(--p-warn-soft);
border: 1px solid var(--p-warn);
border-radius: var(--p-r-md, 12px);
padding: var(--p-4, 16px);
ul {
margin-top: 0.5rem;
padding-left: 1.5rem;
margin-top: var(--p-2, 8px);
padding-left: var(--p-6, 24px);
li {
margin-bottom: 0.25rem;
}
}
}
.benefits-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
.benefit-item {
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
text-align: center;
.text-subtitle2 {
font-weight: 600;
margin-bottom: 0.5rem;
}
p {
margin: 0;
font-size: 0.9rem;
margin-bottom: var(--p-1, 4px);
}
}
}
.faq-section {
.text-h6 {
font-weight: 600;
margin-bottom: 1rem;
}
:deep(.q-expansion-item) {
margin-bottom: 0.75rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-bottom: var(--p-3, 12px);
border: 1px solid var(--p-line-1);
border-radius: var(--p-r-md, 12px);
overflow: hidden;
.q-expansion-item__container {
.q-item {
background: #f8f9fa;
font-weight: 500;
.q-item {
background: var(--p-surface-2);
font-weight: 500;
color: var(--p-ink);
&:hover {
background: #e9ecef;
}
&:hover {
background: var(--p-surface-3);
}
}
.q-card {
background: white;
background: var(--p-surface);
box-shadow: none;
.q-card__section {
line-height: 1.6;
color: var(--p-ink-1);
p {
margin-bottom: 0.75rem;
margin-bottom: var(--p-3, 12px);
&:last-child {
margin-bottom: 0;
@@ -431,54 +397,28 @@ const dialogStyle = ref({
}
ul, ol {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
padding-left: 1.5rem;
margin-top: var(--p-2, 8px);
margin-bottom: var(--p-2, 8px);
padding-left: var(--p-6, 24px);
li {
margin-bottom: 0.5rem;
margin-bottom: var(--p-2, 8px);
line-height: 1.5;
}
}
b {
color: #667eea;
color: var(--p-primary);
font-weight: 600;
}
.text-negative {
color: #c10015;
color: var(--p-neg);
font-weight: 500;
}
}
}
}
}
.delegates-section,
.accounts-section {
.text-h6 {
font-weight: 600;
margin-bottom: 1rem;
}
.resource-explanation {
.explanation-item {
.text-subtitle1 {
font-weight: 600;
margin-bottom: 0.5rem;
}
p {
margin-left: 1.5rem;
padding-left: 0.5rem;
border-left: 3px solid #667eea;
background: rgba(102, 126, 234, 0.05);
padding: 0.5rem 0 0.5rem 1rem;
line-height: 1.6;
}
}
}
}
}
</style>
@@ -1,6 +1,6 @@
<template lang="pug">
div.page-shell
q-card.q-mt-md(flat)
.accounts-page
q-card(flat)
q-table.full-height(
flat
:grid='isMobile'
@@ -43,7 +43,7 @@ div.page-shell
.q-pa-sm
.row.items-center.q-mb-sm
.col
.text-caption.text-grey-6 История проводок
.text-caption.t-faint История проводок
.col-auto
q-btn(
flat dense size='sm' color='primary'
@@ -81,7 +81,7 @@ div.page-shell
@click='copyFullHash(cp.row.processHash)'
)
q-tooltip Клик копировать полный хэш
span.text-grey-6(v-else)
span.t-faint(v-else)
template(#body-cell-quantity='cp')
q-td.text-right(:props='cp') {{ cp.row.quantity ? formatAsset2Digits(cp.row.quantity) : '—' }}
template(#body-cell-createdAt='cp')
@@ -115,13 +115,13 @@ div.page-shell
q-tooltip К операциям счёта
.row.q-mt-sm
.col-4
.text-caption.text-grey-6 Дебет
.text-caption.t-faint Дебет
.text-body2 {{ formatAsset2Digits(props.row.debitBalance) }}
.col-4
.text-caption.text-grey-6 Кредит
.text-caption.t-faint Кредит
.text-body2 {{ formatAsset2Digits(props.row.creditBalance) }}
.col-4
.text-caption.text-grey-6 Сальдо
.text-caption.t-faint Сальдо
.text-body2.text-weight-bold {{ formatAsset2Digits(props.row.balance) }}
</template>
@@ -234,19 +234,23 @@ onMounted(async () => {
</script>
<style scoped lang="scss">
.accounts-page {
padding: var(--p-6, 24px);
@media (max-width: 768px) {
padding: var(--p-4, 16px);
}
}
.font-monospace {
font-family: 'JetBrains Mono', 'Courier New', monospace;
letter-spacing: 0.03em;
}
// Тип счёта (Активный/Пассивный) и сторона проводки (Дебет/Кредит): subtle
// hue-различие при максимальной читаемости. quasar text-blue-grey-8 /
// text-brown-7 нечитаемы на тёмной теме здесь theme-aware-варианты.
// Тип счёта (Активный/Пассивный) и сторона проводки (Дебет/Кредит):
// различие словом + спокойный канон-контраст. Активный/Дебет основной
// ink, Пассивный/Кредит приглушённый; оба theme-aware через токены.
.account-type-active {
color: #37474F; // blue-grey-9
.body--dark & { color: #B0BEC5; } // blue-grey-3
color: var(--p-ink);
}
.account-type-passive {
color: #5D4037; // brown-8
.body--dark & { color: #BCAAA4; } // brown-3
color: var(--p-ink-2);
}
</style>
@@ -1,5 +1,5 @@
<template lang="pug">
.q-pa-md
.documents-archive
.row.items-center.q-mb-md
.text-h6.col Архив сгенерированных отчётов
@@ -28,36 +28,32 @@
@update:model-value='onFilterChange'
)
q-table(
:rows='reportStore.archive.items'
:columns='archiveColumns'
row-key='id'
flat
:loading='reportStore.archiveLoading'
:pagination='archivePagination'
@request='onArchiveRequest'
)
template(#body-cell-valid='props')
q-td(:props='props')
q-chip(
:color='props.row.isValid ? "positive" : "negative"'
text-color='white'
dense
size='sm'
) {{ props.row.isValid ? 'Валиден' : 'Ошибки' }}
q-card(flat)
q-table(
:rows='reportStore.archive.items'
:columns='archiveColumns'
row-key='id'
flat
:loading='reportStore.archiveLoading'
:pagination='archivePagination'
@request='onArchiveRequest'
)
template(#body-cell-valid='props')
q-td(:props='props')
BaseBadge(:variant='props.row.isValid ? "pos" : "neg"') {{ props.row.isValid ? 'Валиден' : 'Ошибки' }}
template(#body-cell-createdAt='props')
q-td(:props='props') {{ formatDate(props.row.createdAt) }}
template(#body-cell-createdAt='props')
q-td(:props='props') {{ formatDate(props.row.createdAt) }}
template(#body-cell-actions='props')
q-td(:props='props')
q-btn(
flat dense
icon='fa-solid fa-download'
color='primary'
@click='downloadArchive(props.row.id)'
)
q-tooltip Скачать XML
template(#body-cell-actions='props')
q-td(:props='props')
q-btn(
flat dense
icon='fa-solid fa-download'
color='primary'
@click='downloadArchive(props.row.id)'
)
q-tooltip Скачать XML
</template>
<script setup lang="ts">
@@ -68,6 +64,7 @@ import {
useReportStore,
type IReportType,
} from 'src/entities/Report'
import { BaseBadge } from 'src/shared/ui/base/BaseBadge'
const MVP_REPORT_TYPES = ['BUHOTCH', 'NDFL6', 'RSV', 'PSV', 'FSS4'] as IReportType[]
@@ -1,5 +1,5 @@
<template lang="pug">
.q-pa-md
.documents-calendar
ReportsCalendar(ref='calendarRef' @select='onCalendarSelect')
ReportEditorDialog(
@@ -1,58 +1,59 @@
<template lang="pug">
.q-pa-md
.documents-forms
.row.items-center.q-mb-md
.text-h6.col Доступные формы
q-btn(flat dense icon='fa-solid fa-rotate' @click='loadReports' :loading='reportStore.loading')
q-tooltip Обновить
q-table(
:rows='visibleReports'
:columns='columns'
row-key='type'
flat
:loading='reportStore.loading'
hide-pagination
:pagination='{ rowsPerPage: 0 }'
)
template(#body-cell-period='props')
q-td(:props='props')
q-chip(:color='periodColor(props.row.period)' text-color='white' dense) {{ periodLabel(props.row.period) }}
q-card(flat)
q-table(
:rows='visibleReports'
:columns='columns'
row-key='type'
flat
:loading='reportStore.loading'
hide-pagination
:pagination='{ rowsPerPage: 0 }'
)
template(#body-cell-period='props')
q-td(:props='props')
BaseBadge(:variant='periodVariant(props.row.period)') {{ periodLabel(props.row.period) }}
template(#body-cell-ready='props')
q-td(:props='props')
q-icon(
v-if='props.row.readyToGenerate'
name='fa-solid fa-check-circle'
color='positive'
size='20px'
)
q-tooltip Реквизиты заполнены
q-icon(
v-else
name='fa-solid fa-triangle-exclamation'
color='warning'
size='20px'
)
q-tooltip {{ missingTooltip(props.row) }}
template(#body-cell-ready='props')
q-td(:props='props')
q-icon(
v-if='props.row.readyToGenerate'
name='fa-solid fa-check-circle'
color='positive'
size='20px'
)
q-tooltip Реквизиты заполнены
q-icon(
v-else
name='fa-solid fa-triangle-exclamation'
color='warning'
size='20px'
)
q-tooltip {{ missingTooltip(props.row) }}
template(#body-cell-actions='props')
q-td(:props='props')
q-btn(
v-if='props.row.readyToGenerate'
flat dense
icon='fa-solid fa-pen-to-square'
color='primary'
@click='openEditor(props.row)'
)
q-tooltip Открыть редактор формы
q-btn(
v-else
flat dense
icon='fa-solid fa-gear'
color='warning'
:to='{ name: "reports-settings", query: { focus: firstMissing(props.row) } }'
)
q-tooltip Заполнить реквизиты
template(#body-cell-actions='props')
q-td(:props='props')
q-btn(
v-if='props.row.readyToGenerate'
flat dense
icon='fa-solid fa-pen-to-square'
color='primary'
@click='openEditor(props.row)'
)
q-tooltip Открыть редактор формы
q-btn(
v-else
flat dense
icon='fa-solid fa-gear'
color='warning'
:to='{ name: "reports-settings", query: { focus: firstMissing(props.row) } }'
)
q-tooltip Заполнить реквизиты
ReportEditorDialog(
v-if='showEditor'
@@ -74,6 +75,8 @@ import {
type IAvailableReport,
type IReportType,
} from 'src/entities/Report'
import { BaseBadge } from 'src/shared/ui/base/BaseBadge'
import type { BaseBadgeProps } from 'src/shared/ui/base/BaseBadge/BaseBadge.types'
import ReportEditorDialog from './ReportEditorDialog.vue'
const MVP_REPORT_TYPES = ['BUHOTCH', 'NDFL6', 'RSV', 'PSV', 'FSS4'] as IReportType[]
@@ -102,8 +105,8 @@ function periodLabel(p: string) {
return ({ yearly: 'Ежегодно', quarterly: 'Ежеквартально', monthly: 'Ежемесячно' }[p] ?? p)
}
function periodColor(p: string) {
return ({ yearly: 'deep-purple', quarterly: 'blue', monthly: 'teal' }[p] ?? 'grey')
function periodVariant(p: string): BaseBadgeProps['variant'] {
return ({ yearly: 'accent', quarterly: 'info', monthly: 'pos' }[p] ?? 'neutral') as BaseBadgeProps['variant']
}
function missingTooltip(r: IAvailableReport) {
@@ -1,69 +1,37 @@
<template lang="pug">
.column.flex-1.min-h-0.min-w-0.no-wrap
router-view
.reports-shell
SecondLevelTabs(:tabs='tabs')
.reports-shell__content
router-view
</template>
<script setup lang="ts">
import { computed, markRaw, onBeforeUnmount, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useHeaderActions } from 'src/shared/hooks/useHeaderActions'
import RouteMenuButton from 'src/shared/ui/RouteMenuButton/RouteMenuButton.vue'
import { SecondLevelTabs } from 'src/shared/ui/SecondLevelTabs'
// DocumentsPage shell-страница для 3 подвкладок отчётности. Инжектирует
// 3 кнопки в шапку сайта, переключение через именованные маршруты
// reports-documents-calendar / -forms / -archive. Рендер содержимого через
// <router-view>. Паттерн один-в-один как capital/ProjectPage.
const route = useRoute()
const { registerAction, clearActions } = useHeaderActions()
const menuButtons = computed(() => [
{
id: 'reports-documents-calendar-menu',
component: markRaw(RouteMenuButton),
props: {
routeName: 'reports-documents-calendar',
label: 'Календарь',
},
order: 1,
},
{
id: 'reports-documents-forms-menu',
component: markRaw(RouteMenuButton),
props: {
routeName: 'reports-documents-forms',
label: 'Список форм',
},
order: 2,
},
{
id: 'reports-documents-archive-menu',
component: markRaw(RouteMenuButton),
props: {
routeName: 'reports-documents-archive',
label: 'Архив',
},
order: 3,
},
])
function registerAll() {
menuButtons.value.forEach((b) => registerAction(b))
}
onMounted(() => {
registerAll()
})
onBeforeUnmount(() => {
clearActions()
})
// При переходе между своими child-маршрутами подсветка обновляется
// самим RouteMenuButton через route.name; дополнительной регистрации не надо.
// Но если кто-то попадёт на другую страницу (напр. reports-settings),
// clearActions() в onBeforeUnmount shell'а сам всё вычистит.
watch(() => route.name, () => {
// no-op: оставлено как точка расширения, если понадобится условная регистрация.
})
// Shell-страница «Отчётность»: канон-меню второго уровня
// (Календарь / Список форм / Архив). Содержимое через <router-view>.
const tabs = [
{ routeName: 'reports-documents-calendar', label: 'Календарь', icon: 'fa-solid fa-calendar-days' },
{ routeName: 'reports-documents-forms', label: 'Список форм', icon: 'fa-solid fa-list' },
{ routeName: 'reports-documents-archive', label: 'Архив', icon: 'fa-solid fa-box-archive' },
]
</script>
<style scoped lang="scss">
.reports-shell {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
min-width: 0;
}
.reports-shell__content {
flex: 1;
min-height: 0;
overflow: auto;
padding: var(--p-6, 24px);
@media (max-width: 768px) {
padding: var(--p-4, 16px);
}
}
</style>
@@ -47,7 +47,7 @@ q-dialog(
.stub-requisites(v-if='!readinessLoading && notReady')
q-icon(name='fa-solid fa-circle-info' size='56px' color='orange')
.text-h6.q-mt-md.q-mb-sm Сначала заполните реквизиты
.text-body2.text-grey-8.q-mb-md
.text-body2.t-muted.q-mb-md
| Для отчёта «{{ reportTitle }}» не хватает обязательных полей. Заполните их в разделе «Реквизиты», после чего вернитесь сюда.
.missing-chips.q-mb-lg
q-chip(
@@ -735,15 +735,15 @@ function goToRequisites(): void {
<style scoped lang="scss">
.editor-container {
overflow: auto;
background: #f5f6f8;
background: var(--p-canvas);
position: relative;
}
.action-panel {
width: 260px;
flex: 0 0 260px;
border-left: 1px solid rgba(0, 0, 0, 0.08);
background: var(--q-neutral-2, #fafafa);
border-left: 1px solid var(--p-line);
background: var(--p-surface-2);
}
// Mobile: панель становится slide-in overlay справа. Без action-panel--open
@@ -756,7 +756,7 @@ function goToRequisites(): void {
width: min(85vw, 320px);
flex: none;
z-index: 6001; // выше q-dialog (которое ~6000); иначе поверх не ложится
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.18);
box-shadow: var(--p-shadow-modal, -4px 0 16px rgba(0, 0, 0, 0.18));
transform: translateX(100%);
transition: transform 0.22s ease-out;
overflow-y: auto;
@@ -769,7 +769,7 @@ function goToRequisites(): void {
.action-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
background: var(--p-overlay, rgba(0, 0, 0, 0.4));
z-index: 6000;
animation: fade-in 0.2s ease-out;
}
@@ -790,7 +790,7 @@ function goToRequisites(): void {
align-items: center;
justify-content: center;
height: 100%;
color: #666;
color: var(--p-ink-2);
text-align: center;
padding: 40px;
}
@@ -804,7 +804,7 @@ function goToRequisites(): void {
margin: 48px auto;
padding: 32px;
text-align: center;
color: #333;
color: var(--p-ink-1);
.missing-chips {
display: flex;
@@ -826,27 +826,27 @@ function goToRequisites(): void {
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 4px;
font-size: 13px;
border-radius: var(--p-r-xs, 4px);
font-size: var(--p-fs-body-sm, 13px);
font-weight: 500;
&.ok {
background: #e8f5e9;
color: #2e7d32;
background: var(--p-pos-soft);
color: var(--p-pos);
}
&.bad {
background: #ffebee;
color: #c62828;
background: var(--p-neg-soft);
color: var(--p-neg);
}
}
.mark-hint {
font-size: 12px;
color: #666;
font-size: var(--p-fs-meta, 12px);
color: var(--p-ink-2);
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
background: #eceff1;
border-radius: 4px;
background: var(--p-surface-2);
border-radius: var(--p-r-xs, 4px);
}
</style>
@@ -1,7 +1,7 @@
<template lang="pug">
div.page-shell
.operations-page
//- Активные фильтры (чипы) + фильтры по датам
q-card.q-mt-md(flat)
q-card(flat)
q-card-section
.row.q-gutter-sm.items-center.q-mb-sm(
v-if='filters.accountId !== null || filters.walletName || filters.processHash || filters.applyGlobalSequence || filters.username'
@@ -146,7 +146,7 @@ div.page-shell
@click='copyFullHash(props.row.processHash)'
)
q-tooltip Клик копировать полный хэш
span.text-grey-6(v-else)
span.t-faint(v-else)
q-td {{ formatDate(props.row.createdAt) }}
q-td
q-chip(
@@ -172,14 +172,14 @@ div.page-shell
)
.text-h6.text-weight-medium {{ rowLabel(props.row) }}
.row.items-center.q-gutter-sm.q-mb-xs(v-if='props.row.operationCode')
.text-caption.text-grey-7 Тип процесса:
.text-caption.t-muted Тип процесса:
EntityIdBadge(
:rawId='props.row.operationCode'
copy-on-click
)
q-tooltip Клик копировать
.row.items-center.q-gutter-sm.q-mb-xs(v-if='props.row.processHash')
.text-caption.text-grey-7 ID процесса:
.text-caption.t-muted ID процесса:
EntityIdBadge(
:rawId='props.row.processHash'
copy-on-click
@@ -187,7 +187,7 @@ div.page-shell
q-tooltip Клик копировать
.row.items-center.q-gutter-sm(v-if='props.row.memo')
q-icon(name='fa-solid fa-note-sticky' color='grey-6' size='xs')
.text-caption.text-grey-8 {{ props.row.memo }}
.text-caption.t-muted {{ props.row.memo }}
//- Тело: две таблицы рядом на md+, друг под другом на маленьких
template(v-if='childLoading.get(props.row.globalSequence)')
@@ -219,7 +219,7 @@ div.page-shell
WalletIdCell(:wallet-name='cp.row.walletTo')
template(#body-cell-quantity='cp')
q-td.text-right(:props='cp') {{ formatAmount(cp.row.quantity) }}
.text-caption.text-grey-6(v-else) Движений по кошелькам нет
.text-caption.t-faint(v-else) Движений по кошелькам нет
//- Таблица 2: Проводки по счетам (Дт Кт парами)
.col-12.col-md-6
@@ -244,28 +244,28 @@ div.page-shell
AccountIdCell(:account-code='cp.row.creditCode')
template(#body-cell-quantity='cp')
q-td.text-right(:props='cp') {{ formatAmount(cp.row.quantity) }}
.text-caption.text-grey-6(v-else) Проводок нет
.text-caption.t-faint(v-else) Проводок нет
template(#item='props')
.col-12
q-card.q-pa-md.q-mb-sm
.row.items-center.q-gutter-x-md
.col
.text-caption.text-grey-6 {{ formatDate(props.row.createdAt) }}
.text-caption.t-faint {{ formatDate(props.row.createdAt) }}
.text-body2.text-weight-medium {{ actionLabel(props.row.operationCode) }}
.text-caption.text-grey-6.font-monospace {{ props.row.operationCode || '-' }}
.text-caption.t-faint.font-monospace {{ props.row.operationCode || '-' }}
.col-auto.text-right
.text-caption.text-grey-6 Сумма
.text-caption.t-faint Сумма
.text-body1.text-weight-bold.font-monospace {{ formatAmount(props.row.quantity) }}
.col-12.text-caption.text-grey-7
.col-12.text-caption.t-muted
| Пайщик: {{ fioCache.get(props.row.username ?? '') || props.row.username || '-' }}
.col-12.row.q-gutter-xs.q-mt-xs.items-center
.text-caption.text-grey-7 операции
.text-caption.t-muted операции
EntityIdBadge(
:rawId='props.row.globalSequence'
@click='copyText(String(props.row.globalSequence))'
)
.text-caption.text-grey-7 процесса
.text-caption.t-muted процесса
EntityIdBadge(
v-if='props.row.processHash'
:rawId='shortHash(props.row.processHash)'
@@ -831,12 +831,18 @@ onMounted(async () => {
</script>
<style scoped>
.operations-page {
padding: var(--p-6, 24px);
}
@media (max-width: 768px) {
.operations-page { padding: var(--p-4, 16px); }
}
.font-monospace {
font-family: 'JetBrains Mono', 'Courier New', monospace;
letter-spacing: 0.03em;
}
.op-header {
border-left: 4px solid #9e9e9e;
border-left: 4px solid var(--p-line-2);
padding: 4px 0 4px 12px;
}
</style>
@@ -1,12 +1,12 @@
<template lang="pug">
div.page-shell
.postings-page
//- Поиск по «номеру»: умный input определяет тип ID по формату.
//- Числовой bigint (24 цифр) debit.global_sequence (точечно одна проводка).
//- 64-символьная hex-строка process_hash (все проводки одной операции).
//- Любая другая строка с операционными префиксами (o.cap...) игнорируем
//- (для этого есть реестр операций). Фильтры account_id/username прилетают
//- через cross-link.
q-card.q-mt-md(flat)
q-card(flat)
q-card-section
.row.q-gutter-sm.items-center.q-mb-sm(
v-if='filters.accountId !== null || filters.username || filters.debitGlobalSequence || filters.applyGlobalSequence || filters.processHash'
@@ -137,7 +137,7 @@ div.page-shell
@click='copyText(String(props.row.debitGlobalSequence))'
)
q-tooltip Клик копировать
span.text-grey-6(v-else)
span.t-faint(v-else)
q-td
EntityIdBadge(
v-if='props.row.processHash'
@@ -145,7 +145,7 @@ div.page-shell
@click='copyFullHash(props.row.processHash)'
)
q-tooltip Клик копировать полный хэш
span.text-grey-6(v-else)
span.t-faint(v-else)
q-td
q-chip(
v-if='props.row.operationCode'
@@ -154,7 +154,7 @@ div.page-shell
:color='processChipBg(props.row.operationCode)'
:text-color='processChipText(props.row.operationCode)'
) {{ operationLabel(props.row.operationCode) }}
span.text-grey-6(v-else)
span.t-faint(v-else)
q-td.text-center
AccountIdCell(:account-code='debitCode(props.row.debitAccountId)')
q-td.text-center
@@ -176,10 +176,10 @@ div.page-shell
q-card.q-pa-md.q-mb-sm
.row.items-center.q-gutter-x-md
.col
.text-caption.text-grey-6 {{ formatDate(props.row.createdAt) }}
.text-caption.t-faint {{ formatDate(props.row.createdAt) }}
.text-body2.text-weight-medium {{ operationLabel(props.row.operationCode) }}
.col-auto.text-right
.text-caption.text-grey-6 Сумма
.text-caption.t-faint Сумма
.text-body1.text-weight-bold.font-monospace {{ formatAmount(props.row.quantity) }}
.col-auto
q-btn(
@@ -190,22 +190,22 @@ div.page-shell
:to='{ name: "reports-operations", query: { operation_id: props.row.parentApplyGlobalSequence } }'
)
.row.q-mt-sm.items-center.q-gutter-x-md
.col-auto.text-caption.text-grey-7 Дебет
.col-auto.text-caption.t-muted Дебет
.col-auto
AccountIdCell(:account-code='debitCode(props.row.debitAccountId)')
.col-auto.text-caption.text-grey-7 Кредит
.col-auto.text-caption.t-muted Кредит
.col-auto
AccountIdCell(:account-code='creditCode(props.row.creditAccountId)')
.col-12.text-caption.text-grey-7
.col-12.text-caption.t-muted
| Пайщик: {{ fioCache.get(props.row.username ?? '') || props.row.username || '—' }}
.col-12.row.q-gutter-xs.q-mt-xs.items-center
.text-caption.text-grey-7 проводки
.text-caption.t-muted проводки
EntityIdBadge(
v-if='props.row.debitGlobalSequence'
:rawId='props.row.debitGlobalSequence'
@click='copyText(String(props.row.debitGlobalSequence))'
)
.text-caption.text-grey-7 процесса
.text-caption.t-muted процесса
EntityIdBadge(
v-if='props.row.processHash'
:rawId='shortHash(props.row.processHash)'
@@ -599,6 +599,12 @@ onMounted(async () => {
</script>
<style scoped>
.postings-page {
padding: var(--p-6, 24px);
}
@media (max-width: 768px) {
.postings-page { padding: var(--p-4, 16px); }
}
.font-monospace {
font-family: 'JetBrains Mono', 'Courier New', monospace;
letter-spacing: 0.03em;
@@ -1,10 +1,10 @@
<template lang="pug">
q-form.page-shell(@submit.prevent='save' @validation-error='onValidationError' greedy)
q-form.settings-page(@submit.prevent='save' @validation-error='onValidationError' greedy)
//- Реквизиты организации (read-only берутся из профиля кооператива)
q-card.q-mt-md(flat)
q-card-section.q-py-sm
.text-h6 Реквизиты организации
.text-caption.text-grey-7 Справочные данные кооператива не редактируются
.text-caption.t-muted Справочные данные кооператива не редактируются
q-separator
@@ -23,7 +23,7 @@ q-form.page-shell(@submit.prevent='save' @validation-error='onValidationError' g
q-card.q-mt-md(flat)
q-card-section.q-py-sm
.text-h6 Классификаторы
.text-caption.text-grey-7 ОКВЭД, ОКФС, ОКОПФ, ОКТМО, ОКПО обязательны для большинства отчётов ФНС
.text-caption.t-muted ОКВЭД, ОКФС, ОКОПФ, ОКТМО, ОКПО обязательны для большинства отчётов ФНС
q-separator
@@ -50,7 +50,7 @@ q-form.page-shell(@submit.prevent='save' @validation-error='onValidationError' g
q-card.q-mt-md(flat)
q-card-section.q-py-sm
.text-h6 СФР (для ЕФС-1)
.text-caption.text-grey-7 Регистрационный номер СФР и должность председателя
.text-caption.t-muted Регистрационный номер СФР и должность председателя
q-separator
@@ -81,7 +81,7 @@ q-form.page-shell(@submit.prevent='save' @validation-error='onValidationError' g
q-card.q-mt-md(flat)
q-card-section.q-py-sm
.text-h6 Подписант
.text-caption.text-grey-7 Кто подписывает отчёты: председатель или представитель (по доверенности)
.text-caption.t-muted Кто подписывает отчёты: председатель или представитель (по доверенности)
q-separator
@@ -331,6 +331,14 @@ onMounted(async () => {
</script>
<style scoped lang="scss">
// Top-level страница сама задаёт канон-отступы (как WalletPage).
.settings-page {
padding: var(--p-6, 24px);
@media (max-width: 768px) {
padding: var(--p-4, 16px);
}
}
.save-bar {
position: sticky;
bottom: 0;
@@ -339,24 +347,19 @@ onMounted(async () => {
z-index: 10;
display: flex;
align-items: center;
gap: 12px;
margin-top: 16px;
padding: 12px 16px;
background: #fff;
border-top: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
gap: var(--p-3, 12px);
margin-top: var(--p-4, 16px);
padding: var(--p-3, 12px) var(--p-4, 16px);
background: var(--p-surface);
border-top: 1px solid var(--p-line);
border-radius: var(--p-r-md, 12px) var(--p-r-md, 12px) 0 0;
}
.save-bar-status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #2e7d32;
}
.body--dark .save-bar {
background: #1d1d1d;
border-top-color: rgba(255, 255, 255, 0.08);
font-size: var(--p-fs-body-sm, 13px);
color: var(--p-pos);
}
</style>
@@ -1,6 +1,6 @@
<template lang="pug">
div.page-shell
q-card.q-mt-md(flat)
q-card(flat)
q-table.full-height(
flat
:grid='isMobile'
@@ -26,10 +26,9 @@ div.page-shell
WalletIdCell(:wallet-name='props.row.id')
q-td {{ props.row.name }}
q-td.text-right {{ formatAsset2Digits(props.row.available) }}
q-td.text-right {{ formatAsset2Digits(props.row.blocked) }}
q-td.text-right(auto-width)
q-btn(
v-if='isChairman'
v-if='isChairman && transferEnabled'
flat dense round size='sm' color='primary'
icon='fa-solid fa-arrow-right-arrow-left'
@click='openTransferFor(props.row.id)'
@@ -114,9 +113,6 @@ div.page-shell
.col-6
.text-caption.caption-muted Доступно
.text-body2.text-weight-medium {{ formatAsset2Digits(props.row.available) }}
.col-6
.text-caption.caption-muted Заблокировано
.text-body2.text-weight-medium {{ formatAsset2Digits(props.row.blocked) }}
WalletTransferDialog(
v-model='transferDialog.open'
@@ -149,6 +145,13 @@ const ledger2Store = useLedger2Store()
const session = useSessionStore()
const { isChairman } = storeToRefs(session)
// Переводы между кошельками временно скрыты. Текущий UI открывал перевод
// с любого кошелька, включая L3-кошельки пайщиков (w.wal.member) это
// неверно: переводы должны быть доступны только между кооперативными
// кошельками. Вернём, когда определим допустимые пары источникназначение.
// Весь код перевода (диалог, обработчики, колонка) сохранён под этим флагом.
const transferEnabled = false
const { registerAction, unregisterAction } = useHeaderActions()
const HEADER_ACTION_ID = 'reports-wallets-coop-transfer'
@@ -208,9 +211,8 @@ const columns = computed<any[]>(() => {
{ name: 'id', align: 'left', label: 'ID', field: 'id', sortable: true },
{ name: 'name', align: 'left', label: 'Наименование', field: 'name', sortable: true },
{ name: 'available', align: 'right', label: 'Доступно', field: 'available', sortable: true },
{ name: 'blocked', align: 'right', label: 'Заблокировано', field: 'blocked', sortable: true },
]
if (isChairman.value) {
if (isChairman.value && transferEnabled) {
base.push({ name: 'actions', align: 'right', label: '', field: 'actions', sortable: false })
}
return base
@@ -273,7 +275,8 @@ onMounted(async () => {
// Кнопка «Перевести» в шапке сайта только председателю.
// Регистрируем после загрузки, чтобы не мигала на ходу.
if (isChairman.value) {
// transferEnabled=false переводы временно скрыты целиком (см. выше).
if (isChairman.value && transferEnabled) {
registerAction({
id: HEADER_ACTION_ID,
component: markRaw(TransferWalletsButton),
@@ -298,11 +301,8 @@ onBeforeUnmount(() => {
white-space: normal;
word-break: break-word;
}
// Приглушённый caption-цвет, согласованный с темой. Не использовать
// quasar `text-grey-6` он не реагирует на body--dark и плохо читается
// на тёмной теме.
// Канон-токен --p-ink-2 сам адаптируется к тёмной теме.
.caption-muted {
color: rgba(0, 0, 0, 0.6);
.body--dark & { color: rgba(255, 255, 255, 0.6); }
color: var(--p-ink-2);
}
</style>
@@ -1,6 +1,6 @@
<template lang="pug">
div.page-shell
q-card.q-mt-md(flat)
q-card(flat)
q-table.full-height.pw-table(
flat
:grid='isMobile'
@@ -115,27 +115,25 @@ function programTypeFor(colName: string): string {
function totalFor(username: string): IWalletCell {
const row = data.value.matrix[username]
if (!row) return { available: 0, blocked: 0 }
let a = 0, b = 0
if (!row) return { available: 0 }
let a = 0
for (const c of Object.values(row)) {
a += c.available
b += c.blocked
}
return { available: a, blocked: b }
return { available: a }
}
const grandTotal = computed<IWalletCell>(() => {
let a = 0, b = 0
let a = 0
for (const c of Object.values(data.value.totals)) {
a += c.available
b += c.blocked
}
return { available: a, blocked: b }
return { available: a }
})
function cellClass(cell?: IWalletCell): string {
if (!cell) return 'cell-empty'
if (cell.available === 0 && cell.blocked === 0) return 'cell-zero'
if (cell.available === 0) return 'cell-zero'
return 'cell-has-value'
}
@@ -149,7 +147,7 @@ const columns = computed(() => {
label: prog.title,
field: (row: IAccount) => {
const c = data.value.matrix[row.username]?.[prog.id]
return c ? c.available + c.blocked : 0
return c ? c.available : 0
},
sortable: true,
}))
@@ -168,7 +166,7 @@ const columns = computed(() => {
label: 'Итого',
field: (row: IAccount) => {
const t = totalFor(row.username)
return t.available + t.blocked
return t.available
},
sortable: true,
},
@@ -182,9 +180,8 @@ const columns = computed(() => {
]
})
// Inline-рендер ячейки «available / blocked с иконками». Вынесен в h-function,
// чтобы не таскать ещё одну SFC. Иконки: lock-open / lock + q-tooltip чтобы
// без подписей было понятно где «доступно», где «заблокировано».
// Inline-рендер ячейки «доступно». Вынесен в h-function, чтобы не таскать ещё
// одну SFC. Иконка lock-open + q-tooltip «Доступно».
const WalletCell = {
name: 'WalletCell',
props: {
@@ -197,26 +194,17 @@ const WalletCell = {
if (!c) {
return h('div', { class: 'cell-dash' }, '—')
}
const renderLine = (
amount: number,
kind: 'avail' | 'blocked',
) => {
const valueClass = amount > 0 ? `value-${kind}` : 'value-zero'
const tooltip = kind === 'avail' ? 'Доступно' : 'Заблокировано'
const icon = kind === 'avail' ? 'fa-solid fa-lock-open' : 'fa-solid fa-lock'
return h(
const valueClass = c.available > 0 ? 'value-avail' : 'value-zero'
return h('div', { class: 'wallet-cell' }, [
h(
'div',
{ class: ['cell-line', valueClass, props.bold ? 'bold' : ''].join(' ') },
[
h(QIcon, { name: icon, size: '12px', class: 'cell-icon' }),
h('span', { class: 'cell-value' }, formatAsset2Digits(`${amount} RUB`)),
h(QTooltip, { anchor: 'top middle', self: 'bottom middle', delay: 200 }, () => tooltip),
h(QIcon, { name: 'fa-solid fa-lock-open', size: '12px', class: 'cell-icon' }),
h('span', { class: 'cell-value' }, formatAsset2Digits(`${c.available} RUB`)),
h(QTooltip, { anchor: 'top middle', self: 'bottom middle', delay: 200 }, () => 'Доступно'),
],
)
}
return h('div', { class: 'wallet-cell' }, [
renderLine(c.available, 'avail'),
renderLine(c.blocked, 'blocked'),
),
])
}
},
@@ -251,12 +239,9 @@ onMounted(() => void reload())
</script>
<style scoped lang="scss">
// Все цвета через quasar runtime variables (`var(--q-*)`) и body--dark
// overrides без хардкода hex. Иначе светлая/тёмная темы выглядят плохо.
// Не использовать quasar `text-grey-6` он не реагирует на body--dark.
// Канон-токены сами адаптируются к тёмной теме без rgba-хардкода.
.caption-muted {
color: rgba(0, 0, 0, 0.6);
.body--dark & { color: rgba(255, 255, 255, 0.6); }
color: var(--p-ink-2);
}
.pw-table {
@@ -277,19 +262,11 @@ onMounted(() => void reload())
}
:deep(.row-totals) {
background: rgba(0, 0, 0, 0.04);
background: var(--p-surface-2);
font-weight: 600;
td {
border-top: 2px solid rgba(0, 0, 0, 0.12);
}
.body--dark & {
background: rgba(255, 255, 255, 0.06);
td {
border-top-color: rgba(255, 255, 255, 0.12);
}
border-top: 2px solid var(--p-line-2);
}
}
@@ -315,7 +292,7 @@ onMounted(() => void reload())
</style>
<!--
«available + blocked» рендерятся через render-функцию `WalletCell`,
«available» рендерится через render-функцию `WalletCell`,
у которой нет своего scoped-id. scoped + :deep до неё не доходит
стабильно, поэтому стили лежат в global-блоке. Класс `.wallet-cell`
достаточно специфичен конфликтов с другими компонентами нет.
@@ -336,10 +313,10 @@ onMounted(() => void reload())
font-size: 14px;
font-weight: 500;
&.value-avail { color: var(--q-positive); }
&.value-blocked { color: var(--q-warning); }
&.value-avail { color: var(--p-pos); }
&.value-blocked { color: var(--p-warn); }
&.value-zero {
color: rgba(0, 0, 0, 0.55);
color: var(--p-ink-3);
font-weight: 400;
}
&.bold { font-weight: 700; }
@@ -351,12 +328,7 @@ onMounted(() => void reload())
}
.cell-dash {
color: rgba(0, 0, 0, 0.55);
color: var(--p-ink-3);
}
}
.body--dark .wallet-cell {
.cell-line.value-zero { color: rgba(255, 255, 255, 0.7); }
.cell-dash { color: rgba(255, 255, 255, 0.7); }
}
</style>
@@ -1,12 +1,13 @@
<template lang="pug">
q-btn(
size='md'
flat
color='secondary'
icon='fa-solid fa-arrow-right-arrow-left'
label='Перевести'
color='primary'
no-caps
no-wrap
unelevated
@click='onClick'
)
q-icon.q-mr-sm(name='fa-solid fa-arrow-right-arrow-left' size='15px')
span Перевести
q-tooltip Корректировка: перевод между кошельками одного бух.счёта
</template>
@@ -1,95 +1,87 @@
<template lang="pug">
q-dialog(
BaseDialog(
:model-value='modelValue'
@update:model-value='emit("update:modelValue", $event)'
persistent
size='lg'
title='Перевод между кошельками'
:close-on-backdrop='!loading'
:close-on-escape='!loading'
)
q-card(style='min-width: 480px; max-width: 540px;')
q-bar.bg-primary.text-white
.text-subtitle1 Перевод между кошельками
q-space
q-btn(flat dense icon='fa-solid fa-xmark' @click='close')
q-tooltip Закрыть
p.transfer-dialog__lead
| Перевод между кошельками одного бухгалтерского счёта без изменения сальдо счёта.
| Доступен только председателю.
q-card-section.q-pa-md
.text-caption.caption-muted.q-mb-md
| Перевод между кошельками одного бухгалтерского счёта без изменения сальдо счёта.
| Доступен только председателю.
q-form(@submit.prevent='submit' ref='formRef' greedy)
q-select.q-mb-sm(
v-model='form.fromWallet'
:options='fromOptions'
option-label='label'
option-value='id'
emit-value map-options
label='Из кошелька'
outlined dense
:disable='!!props.fixedFromWallet'
:rules='[(v) => !!v || "Выберите кошелёк-источник"]'
@update:model-value='onFromChange'
)
template(#option='scope')
q-item(v-bind='scope.itemProps')
q-item-section
q-item-label {{ scope.opt.label }}
q-item-label(caption) Доступно: {{ scope.opt.available }}
q-form(@submit.prevent='submit' ref='formRef' greedy)
q-select.q-mb-sm(
v-model='form.fromWallet'
:options='fromOptions'
option-label='label'
option-value='id'
emit-value map-options
label='Из кошелька'
outlined dense
:disable='!!props.fixedFromWallet'
:rules='[(v) => !!v || "Выберите кошелёк-источник"]'
@update:model-value='onFromChange'
)
template(#option='scope')
q-item(v-bind='scope.itemProps')
q-item-section
q-item-label {{ scope.opt.label }}
q-item-label(caption) Доступно: {{ scope.opt.available }}
q-select.q-mb-sm(
v-model='form.toWallet'
:options='toOptions'
option-label='label'
option-value='id'
emit-value map-options
label='В кошелёк'
outlined dense
:disable='!form.fromWallet'
:hint='toHint'
:rules='[(v) => !!v || "Выберите кошелёк-получатель", (v) => v !== form.fromWallet || "Источник и получатель не должны совпадать"]'
)
template(#option='scope')
q-item(v-bind='scope.itemProps')
q-item-section
q-item-label {{ scope.opt.label }}
q-item-label(caption) Доступно: {{ scope.opt.available }}
//- Причина пустого списка важнее, чем «No results» по дефолту:
//- walmove ходит ТОЛЬКО внутри одного бух.счёта, поэтому если у
//- кооператива нет других кошельков на том же счёте выбора нет.
template(#no-option)
q-item
q-item-section.text-italic.caption-muted
| Нет других кошельков на бух.счёте {{ accountIdLabel }}.
| Перевод между разными счетами требует решения совета.
q-select.q-mb-sm(
v-model='form.toWallet'
:options='toOptions'
option-label='label'
option-value='id'
emit-value map-options
label='В кошелёк'
outlined dense
:disable='!form.fromWallet'
:hint='toHint'
:rules='[(v) => !!v || "Выберите кошелёк-получатель", (v) => v !== form.fromWallet || "Источник и получатель не должны совпадать"]'
)
template(#option='scope')
q-item(v-bind='scope.itemProps')
q-item-section
q-item-label {{ scope.opt.label }}
q-item-label(caption) Доступно: {{ scope.opt.available }}
//- Причина пустого списка важнее, чем «No results» по дефолту:
//- walmove ходит ТОЛЬКО внутри одного бух.счёта, поэтому если у
//- кооператива нет других кошельков на том же счёте выбора нет.
template(#no-option)
q-item
q-item-section.text-italic.caption-muted
| Нет других кошельков на бух.счёте {{ accountIdLabel }}.
| Перевод между разными счетами требует решения совета.
.row.items-center.q-gutter-sm.q-mb-sm(v-if='form.fromWallet')
.col
.text-caption.caption-muted Бух.счёт
.text-body2.text-weight-medium {{ accountIdLabel }}
.row.items-center.q-gutter-sm.q-mb-sm(v-if='form.fromWallet')
.col
.text-caption.caption-muted Бух.счёт
.text-body2.text-weight-medium {{ accountIdLabel }}
q-input.q-mb-sm(
v-model='form.amountStr'
label='Сумма (RUB)'
outlined dense type='number' step='0.0001' min='0.0001'
:rules='[validateAmount]'
hint='До 4 знаков после запятой'
)
q-input.q-mb-sm(
v-model='form.amountStr'
label='Сумма (RUB)'
outlined dense type='number' step='0.0001' min='0.0001'
:rules='[validateAmount]'
hint='До 4 знаков после запятой'
)
q-input.q-mb-sm(
v-model='form.memo'
label='Обоснование'
outlined dense type='textarea' rows='3' counter maxlength='255'
:rules='[(v) => (v && v.trim().length > 0) || "Обязательно — укажите причину перевода"]'
)
q-input.q-mb-sm(
v-model='form.memo'
label='Обоснование'
outlined dense type='textarea' rows='3' counter maxlength='255'
:rules='[(v) => (v && v.trim().length > 0) || "Обязательно — укажите причину перевода"]'
)
.row.justify-end.q-gutter-sm.q-mt-md
q-btn(flat label='Отмена' color='grey-7' :disable='loading' @click='close')
q-btn(
type='submit'
color='primary'
label='Перевести'
:loading='loading'
icon='fa-solid fa-arrow-right-arrow-left'
)
template(#footer)
BaseButton(variant='ghost' :disabled='loading' @click='close') Отмена
BaseButton(variant='primary' :loading='loading' @click='submit')
template(#icon-left)
q-icon.q-mr-xs(name='fa-solid fa-arrow-right-arrow-left' size='15px')
| Перевести
</template>
<script setup lang="ts">
@@ -98,6 +90,8 @@ import { Ledger2 } from 'cooptypes'
import { FailAlert, SuccessAlert } from 'src/shared/api'
import { useSystemStore } from 'src/entities/System/model'
import { useLedger2Store, type ILedger2Wallet } from 'src/entities/Ledger2'
import { BaseDialog } from 'src/shared/ui/base/BaseDialog'
import { BaseButton } from 'src/shared/ui/base/BaseButton'
interface Props {
modelValue: boolean
@@ -218,6 +212,11 @@ watch(
)
async function submit() {
if (loading.value) return
// Кнопка «Перевести» теперь в footer диалога (вне q-form), поэтому
// валидацию правил полей запускаем явно перед отправкой.
const valid = await formRef.value?.validate()
if (!valid) return
if (!form.fromWallet || !form.toWallet) return
if (!sourceAccountId.value) {
FailAlert(new Error('Не удалось определить бух.счёт для выбранного кошелька'))
@@ -254,9 +253,14 @@ function close() {
</script>
<style scoped lang="scss">
// Не использовать quasar `text-grey-X` фиксированный hex без body--dark.
.transfer-dialog__lead {
margin: 0;
color: var(--p-ink-2);
font-size: var(--p-fs-body-sm, 13px);
line-height: 1.5;
}
// Канон-токен --p-ink-2 сам адаптируется к тёмной теме без rgba-хардкода.
.caption-muted {
color: rgba(0, 0, 0, 0.6);
.body--dark & { color: rgba(255, 255, 255, 0.6); }
color: var(--p-ink-2);
}
</style>
@@ -1,44 +1,37 @@
<template lang="pug">
.column.flex-1.min-h-0.min-w-0.no-wrap
router-view
.reports-shell
SecondLevelTabs(:tabs='tabs')
.reports-shell__content
router-view
</template>
<script setup lang="ts">
import { computed, markRaw, onBeforeUnmount, onMounted } from 'vue'
import { useHeaderActions } from 'src/shared/hooks/useHeaderActions'
import RouteMenuButton from 'src/shared/ui/RouteMenuButton/RouteMenuButton.vue'
import { SecondLevelTabs } from 'src/shared/ui/SecondLevelTabs'
// Shell-страница «Кошельки»: инжектит две кнопки в шапку сайта
// (Кооператив / Пайщики), переключает через именованные дочерние маршруты.
// Паттерн один-в-один как DocumentsPage.
const { registerAction, clearActions } = useHeaderActions()
const menuButtons = computed(() => [
{
id: 'reports-wallets-coop-menu',
component: markRaw(RouteMenuButton),
props: {
routeName: 'reports-wallets-coop',
label: 'Кооператив',
},
order: 1,
},
{
id: 'reports-wallets-participants-menu',
component: markRaw(RouteMenuButton),
props: {
routeName: 'reports-wallets-participants',
label: 'Пайщики',
},
order: 2,
},
])
onMounted(() => {
menuButtons.value.forEach((b) => registerAction(b))
})
onBeforeUnmount(() => {
clearActions()
})
// Shell-страница «Реестр кошельков»: канон-меню второго уровня
// (Кооператив / Пайщики). Кнопка «Перевести» живёт в топбаре
// её регистрирует дочерняя CoopWalletsPage через useHeaderActions.
const tabs = [
{ routeName: 'reports-wallets-coop', label: 'Кооператив', icon: 'fa-solid fa-building' },
{ routeName: 'reports-wallets-participants', label: 'Пайщики', icon: 'fa-solid fa-users' },
]
</script>
<style scoped lang="scss">
.reports-shell {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
min-width: 0;
}
.reports-shell__content {
flex: 1;
min-height: 0;
overflow: auto;
padding: var(--p-6, 24px);
@media (max-width: 768px) {
padding: var(--p-4, 16px);
}
}
</style>
@@ -23,12 +23,11 @@ export interface IProgramColumn {
export interface IWalletCell {
available: number
blocked: number
}
export interface IProgramsAndWallets {
programs: IProgramColumn[]
/** matrix[username][program_id] → {available, blocked}. Отсутствие ключа = кошелька нет. */
/** matrix[username][program_id] → {available}. Отсутствие ключа = кошелька нет. */
matrix: Record<string, Record<number, IWalletCell>>
/** Сумма по каждой программе (по всем кошелькам). */
totals: Record<number, IWalletCell>
@@ -77,7 +76,7 @@ export async function loadProgramsAndWallets(
const matrix: Record<string, Record<number, IWalletCell>> = {}
const totals: Record<number, IWalletCell> = {}
for (const p of programs) totals[p.id] = { available: 0, blocked: 0 }
for (const p of programs) totals[p.id] = { available: 0 }
const wallets = walletsResponse[Queries.Wallet.GetProgramWallets.name]?.items ?? []
@@ -85,13 +84,11 @@ export async function loadProgramsAndWallets(
const username = String(w.username)
const program_id = Number(w.program_id)
const avail = parseAsset(w.available)
const blocked = parseAsset(w.blocked)
if (!matrix[username]) matrix[username] = {}
matrix[username][program_id] = { available: avail, blocked }
matrix[username][program_id] = { available: avail }
const t = totals[program_id]
if (t) {
t.available += avail
t.blocked += blocked
}
}
@@ -1,5 +1,5 @@
<template lang="pug">
span(v-if='accountCode === null || accountCode === undefined').text-grey-6
span(v-if='accountCode === null || accountCode === undefined').t-faint
span.account-id-cell(v-else)
EntityIdBadge(
:rawId='accountCode'
@@ -12,10 +12,9 @@
color='negative'
size='xs'
)
q-icon(
q-icon.direction-cell__move(
v-else
name='fa-solid fa-right-left'
color='grey-6'
size='xs'
)
span.text-caption {{ label }}
@@ -34,3 +33,9 @@ const label = computed(() => {
return 'Перевод'
})
</script>
<style scoped lang="scss">
.direction-cell__move {
color: var(--p-ink-3);
}
</style>

Some files were not shown because too many files have changed in this diff Show More