single-producer — текущее состояние сети, не архитектура. Этап 2 —
выборная машинерия финализаторов под кооперативную модель (1 пайщик =
1 голос); upstream regfinkey/switchtosvnn (top-21 по stake) не подходит.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Порт self-contained action из upstream eosio.bios (reference-contracts):
проверка BLS-ключей и proof of possession, вызов host-функции
set_finalizers. Машинерия regfinkey/switchtosvnn голосующего DPoS
не переносится — цепь single-producer, политику задаёт оператор.
Требует CDT >= 4.1 (crypto_bls_ext); собрано CDT savanna 4.3.0 из
образа dicoop/blockchain:latest, setfinalizer присутствует в ABI.
До активации протокол-фичи SAVANNA поведение цепи не меняется.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Найдена реальная причина «белого экрана» на первом SSR-заходе (доказано
DOM-диагностикой + __INITIAL_STATE__ прода: 87 сериализованных "component"
в desktops.workspaces[].routes):
SSR-сервер кладёт RouteRecordRaw вместе с component в Pinia-стор; Quasar
сериализует стейт в __INITIAL_STATE__; Vue-компонент не переживает JSON
(render-функция выпадает). Клиент гидратируется мёртвыми маршрутами,
registerWorkspaceMenus регистрирует их в router (routes=108 сразу),
initExtensions живые не добавляет («маршрут уже есть») → каждая страница
рендерится пустой при работающем layout/меню. F5 «лечит», потому что SW
отдаёт SPA-shell без гидратации (Config.js-путь, routes=13 → живые 108).
Локально не воспроизводилось: dev-режим SPA, без SSR.
Фикс: на клиенте перед loadDesktop зачищаем гидратированные routes из
workspaces — живые добавит useInitExtensionsProcess, ровно как при
SPA-заходе. Диагностика [BOOTRACE] остаётся до подтверждения на проде.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Переходы из установленного PWA на сайт кооператива (vars.website из
публичного getSystemInfo, кэш 5 мин) остаются в окне приложения вместо
выброса в браузер. Заодно явные id и scope для стабильной identity.
Работает в Chrome/Edge 138+ на десктопе; сайт должен отдать
/.well-known/web-app-origin-association с {"https://<домен-лк>/": {"scope": "/"}}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Глобальный канон .table{min-width:0!important} снимал локальный min-width
таблицы — колонки сжимались уже контента и кнопки «Подтвердить/Отклонить»
ложились поверх бейджа статуса. Возвращён min-width (860/980px) с
!important: при нехватке ширины таблица скроллится в .table-scroll, как
журнал уведомлений, а не схлопывает колонки.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
padding/margin на .q-table__grid-item virtual-scroll игнорирует, поэтому
зазор не появлялся. Отступ повешен на саму .participant-card (margin-bottom
внутри grid-item) — его virtual-scroll учитывает в измеряемой высоте.
Grid-item padding возвращён в 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Отступ между карточками: margin-bottom игнорился virtual-scroll, а
padding:0 убрал дефолтный зазор. Зазор задан через padding grid-item
(входит в измеряемую высоту элемента virtual-scroll).
- Наезд бейджа: длинный бейдж («Ожидает решения совета») переполнял свой
контейнер и налезал на дату/удаление. Мета-строка переведена на flex-wrap
— дата+удаление переносятся на след. строку, когда бейдж не помещается.
- Аккаунт: иконка badge с подсказкой «Имя аккаунта» в начале + кнопка
копирования username. Email: иконка mail в начале. Текст с ellipsis.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ListOfParticipantsPage: на <768px (grid-режим карточек) убрана внешняя
обрамлённая поверхность .participants-page__card — карточки уже сами в
рамках, обёртка давала «подложку»/двойное обрамление. На десктопе рамка
остаётся (там таблица).
- ParticipantCard: широкий бейдж статуса («Ожидает решения совета») сжимал
имя/аккаунт/email до пары букв. Перестроено: идентификация на всю ширину
в верхней строке, бейдж + дата + удаление — отдельной строкой снизу.
- ParticipantsTable: на мобиле карточки во всю ширину с понятным
вертикальным отступом (убран дефолтный 4px-padding grid-item, задан gap).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Релизный флоу переведён на линейную fast-forward модель — устраняет
регулярные конфликты на 20 package.json при релизе.
Корень проблемы: publish-alpha.sh/publish-prod.sh бампали версию НА КАЖДОЙ
ветке (alpha-N на testnet, чистую на main) через `git merge -X theirs` +
back-merge. Два независимых bump-коммита за цикл + merge'и плодили
расхождение веток → конфликты (особенно при гонке push/pull).
Новая модель:
- Версию бампает lerna ОДИН раз на dev (scripts/cut-release.sh).
- Тот же коммит едет вверх по FF: scripts/promote.sh testnet|main
(server-side fast-forward push, рабочее дерево не трогается).
- testnet/main не несут своих коммитов → ветки не диверджатся → конфликты
структурно невозможны.
release.yaml:
- Триггер: push в testnet/main с изменением lerna.json (вместо тега v*).
Окружение определяет ВЕТКА (main→prod, testnet→staging), не суффикс -alpha.
- Версия читается из закоммиченного lerna.json (едет с коммитом по FF).
- Гейты npm-publish/доки: branch == main (вместо !contains '-alpha').
- Образы/webhook по-прежнему версия-тегированы → playbooks/приёмник деплоя
не затрагиваются.
Удалены publish-alpha.sh/publish-prod.sh и мёртвые дубли тех же merge-X-theirs
скриптов (root sync:main, components/contracts production/testnet/docs-publish).
Документация — scripts/RELEASE.md + CLAUDE.md PR-flow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Eyebrow + название кооператива висели отдельным блоком над карточкой и
выглядели оторванно. Перенесены в head карточки (отделены линией от сетки
реквизитов); размер h1→h2 под карточный контекст; field-значения
overflow-wrap anywhere→break-word (телефон не рвётся посреди цифр).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Карточка «Минимальный неснижаемый остаток» рисовалась вручную сырыми
canon-классами .wallet в обход WalletCard, поэтому marquee на неё не
распространялся (бежали только программные кошельки). Переведена на
<WalletCard neutral> — DRY + бегущая строка заголовка/подписи бесплатно.
WalletCard: program стал опциональным, добавлен neutral-вариант подсветки
иконки (--p-canvas-2/--p-ink-2) вместо акцента программы.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Журнал уведомлений: на телефоне вместо горизонтального скролла/посимвольного
переноса — компактные карточки, поля по два в ряд (col-xs-6); таблица скрыта
на ≤599px (скрытие через двойной селектор .nj-cards.nj-mobile).
- WalletCard: заголовок и подпись при переполнении становятся «бегущей строкой»
(marquee) вместо обрезки «…» — JS-детект overflow + CSS-анимация, обе строки
бегут одинаково (одна длительность, синхронные паузы). Уважает
prefers-reduced-motion.
- Реквизиты (PaymentMethods): кнопка удаления переведена на канон — icon-only
BaseButton (delete_outline, danger), без текста «удалить»; убраны лишние
fallthrough-атрибуты flat/color.
- BaseCard head: align-items center → flex-start, чтобы угловое действие при
2-строчном заголовке не проваливалось к центру.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Реальная причина белого экрана (предыдущий коммит чинил не то — гонка
чанков по логам не подтвердилась, router.onError не срабатывал):
Вход в той же вкладке (incognito-логин или выход→вход без F5) НЕ
переинициализирует приложение. LoginForm после login() звал только
selectDefaultWorkspace(true) + goToDefaultPage(), но НЕ loadDesktop().
Поэтому currentDesktop оставался АНОНИМНЫМ (загруженным до входа), целевой
стол (chairman/connect) резолвился, но рендерился пустым — данные/гранты
DesktopWorkspace были анонимные. Ручной reload делал init начисто как
авторизованный → стол подгружался → всё рисовалось.
Фикс: добавить `await desktops.loadDesktop()` в LoginForm после ожидания
loadComplete, перед навигацией — зеркало проверенного паттерна в
SignUp.vue / init-app / EnableButton (там стол перезагружают, вход забыли).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Гонка на первом заходе в проде: ленивый чанк маршрута падал на import()
из-за конкуренции за сеть с фоновым precache только что установленного SW,
а перехватить ошибку было некому → пустой router-view до ручной перезагрузки.
- FIX-1: router.onError ловит провал загрузки чанка и делает авто-reload на
целевой путь (с защитой от reload-цикла через sessionStorage).
- FIX-2: регистрацию Service Worker откладываем до window 'load', чтобы
precache-шторм не конкурировал с первой отрисовкой и догрузкой чанков.
- Диагностика: таймстемп-логи [BOOTRACE] по всей boot-цепочке (initApp,
router beforeEach/afterEach/onError, App.onMounted/isLoaded, SW register,
safety-timeout) для грепа порядка инициализации на проде.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Старые клиенты селектят kpp на BankAccountDetails → GraphQL-валидация
падала (Cannot query field "kpp"). Добавлено nullable-поле в DTO + во все
места Zeus-селектора SDK (иначе сборка десктопа падает на MakeAllFieldsRequired).
Помечено TODO «удалить после 1 августа».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Реестр платежей: мобильные карточки (.payments-cards.pmt-mobile) показывались
и на десктопе поверх таблицы. Причина — .payments-cards{display:flex} имел ту
же специфичность, что одиночный .pmt-mobile{display:none}, и перебивал его по
порядку источника. Скрытие/показ переведены на двойной селектор
.payments-cards.pmt-mobile (специфичнее) — на десктопе только таблица.
- Удостоверение/DataRow: горизонтальная пара label|value на телефоне (≤599px)
стекается в одну колонку — длинный публичный ключ занимает всю ширину
карточки вместо узких ~150px и больше не рвётся в столбик по буквам.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Корень «push отправлен, но не виден»: workboxMode GenerateSW игнорирует
sourceFiles.serviceWorker, поэтому push/notificationclick из
custom-service-worker.ts в прод не попадали — браузер push получал, но SW его
не показывал (бэкенд честно «sent to N subscription(s)»). Доказано: прод
/service-worker.js содержит 0 push-обработчиков.
- desktop: public/push-sw.js (push → showNotification, notificationclick →
focus/openWindow) + importScripts('push-sw.js') в extendGenerateSWOptions
- controller: «Доставлено»/«Канал пропущен» debug→info + сводка
«web-push → user: N подписок» — чтобы доставка была видна в логах без БД
Корень: overflow-wrap:anywhere рвал имена/email по буквам в столбик, а
бейджи статуса воровали ширину у имени рядом.
- BaseBadge: white-space:nowrap + flex-shrink:0 — бейдж не сжимается/не
переносится (фикс глобально для всех экранов).
- components.css мобильный .table: anywhere → break-word (перенос по словам).
- Реестр платежей (pay-card): имя обрезается «…», сумма flex-shrink:0,
тип переносится по словам.
- Реестр пайщиков (ParticipantCard): имя/аккаунт/email обрезаются «…»;
попутно FontAwesome-иконка аватара → Material person.
- Журнал уведомлений: восстановлен задуманный горизонтальный скролл
таблицы на мобиле (перебит глобальный min-width:0!important), 7 колонок
с кнопкой действия больше не схлопываются в буквы-в-столбик.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Нижний padding rail__signout 12→4px, version padding-top 4→0px,
шрифт версии 11→10px — версия идёт сразу под кнопкой, не висит крупной.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Было: q-menu прижат справа фиксированной шириной 360px → правый край резался
(«Прочитать в…», тексты заявок), снизу горизонтальный скролл. На мобиле (≤600px)
раскрываем меню во всю ширину вьюпорта (left/right 8px), панель width:100%.
- тело уведомления приходит с <br>/HTML (шарится с email/push-шаблонами), а
in-app панель рендерит как текст с переносом \n — нормализую HTML→plain в toItem
- push-strip и кнопку «Включить на устройстве» показываем только когда SW реально
активен (hasActiveServiceWorker), иначе клик умирал на «Service Worker не активен»;
в dev/без-PWA strip не рисуем (не путать с «браузер не поддерживает»)
- strip не переносит текст/кнопку на узких экранах (nowrap + flex 0 0 auto)
Корень бага «push приходит только на одно устройство»: autoSubscribe
гейтился store.isSubscribed (= у аккаунта есть подписка на ЛЮБОМ устройстве),
поэтому второе устройство никогда не подписывалось. Введён endpoint текущего
браузера + computed isThisDeviceSubscribed; autoSubscribe теперь идемпотентно
регистрирует каждое устройство. Добавлены resubscribe()/refreshDeviceState()
и строка статуса push с кнопкой «Включить/Переподписать» в виджете колокольчика.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Релей переехал в отдельный репозиторий C9S/email-relay, собирается локально
плейбуком (playbooks/email-relay) — релиз mono его больше не пересобирает.
Прод-образ mono-base (prune --prod) не подходил для standalone-сервиса.
Удалено: components/email-relay, build_service в release.yaml, dev-сервис в
docker-compose. Контроллерный relay-режим (EMAIL_RELAY_URL/TOKEN в
email-channel.adapter + config) ОСТАЁТСЯ — он и активирует отправку через релей.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Прод-образ mono-base делает `pnpm prune --prod` (Dockerfile:74) — devDeps
вырезаются. ts-node был в devDependencies → в проде `sh: ts-node: not found`,
контейнер в restart-loop, /health не отвечал. controller держит ts-node в
dependencies — повторяем паттерн. (Коммит из закрытого PR #115 не попал в dev,
вношу прямо.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mailpit был только локальным SMTP-перехватчиком для теста — в поставке не нужен.
email-relay сам ходит в реальный SMTP через nodemailer; конфиг (RELAY_TOKEN,
SMTP_*) выносим в components/email-relay/.env по образцу coopback, без отдельного
контейнера и inline-параметров.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Хостинги коопов режут исходящий SMTP → письма controller'а уходят в timeout
(раньше хабом был Novu, его убрали). Новый компонент @coopenomics/email-relay
принимает письма по HTTPS с Bearer-токеном и форвардит по SMTP. Ставится на
сервер с открытыми портами (плейбук в playbooks/email-relay).
- components/email-relay: express + nodemailer, POST /send + GET /health,
Bearer-токен (constant-time), опц. IP-allowlist, конфиг из ENV
- controller: email-channel.adapter получил relay-режим — если задан
EMAIL_RELAY_URL, письмо уходит POST'ом на релей; иначе прямой SMTP как
раньше (opt-in, без регрессии)
- docker-compose: дев-сервисы email-relay + mailpit (SMTP-перехватчик для E2E)
- release.yaml: сборка образа dicoop/email-relay
E2E проверен локально: controller-формат письма → релей → SMTP → mailpit,
тело с <br> и заголовки целы; 401 на отсутствие/неверный токен, 400 на пустое тело.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
В колоколе уведомлений на тёмной теме элементы сливались с фоном панели:
разделители на --p-line (0.06 white) почти невидимы, а непрочитанные
отличались лишь цветом текста + точкой — всё тонуло в --p-surface. На светлой
контраст тёмного текста на белом спасал.
Канон-токенами (обе темы, светлую не ухудшает): панели — рамка --p-line-1
для контура; разделители между уведомлениями — --p-line-1 (заметнее); у
непрочитанных подложка --p-primary-soft + левый акцент-бар --p-primary,
hover/focus — --p-primary-line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Заменяет ненадёжный триггер от lifecycle service worker'а (не срабатывал
на iOS standalone PWA, при первой установке и когда бинарь SW не менялся)
на сравнение версий: запечённая в бандл package-версия (process.env.APP_VERSION)
сверяется с self-report SSR-ноды (/version). При расхождении — существующий тост.
- quasar.config: APP_VERSION из package.json в build.env; middleware 'version'
- src-ssr/middlewares/version.ts: self-report /version (JSON, no-store)
- entities/AppVersion: стор useUpdateWatch (реестр компонентов, сейчас shell),
опрос /version раз в 5 мин + при возврате на вкладку; SPA-safe (нет ноды -> no-op)
- init-app: старт version-watch рядом с systemMonitoring
- register-service-worker: убран SW-триггер; applyUpdate устойчив (waiting -> SKIP_WAITING, иначе reload)
- LeftDrawerMenu: версия приложения в футере рейла
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
«исчезнет» → «исчезает»; убрано предложение про отзыв заявки на повестке —
кнопка доступна только когда аккаунт уже в цепи (after оплаты), решение совета
тут неприменимо.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Прежний allow-list статусов исключал Registered — но именно туда попадают
отклонённые советом и оплаченные брошенные регистрации (отказ/возврат
оставляют статус registered; failed/refunded users.status никем не
выставляются). То есть чистка не работала для своей же цели «убрать
незавершённые/отклонённые регистрации (тестеры)».
- backend deleteAccount: убран allow-list по статусу, остаётся единственный
гард — НЕ принят в кооператив (нет participant_account).
- frontend isDeletable: !participant_account. Кнопка удаления теперь и у
Registered (оплачен/на рассмотрении совета/отклонён/возврат).
- confirm-диалог: для blockchain-registered (Registered) предупреждение, что
on-chain аккаунт неудаляем (имя в цепи занято навсегда) + про отзыв заявки
на повестке, если ещё на рассмотрении совета.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
На мобиле (и в узком окне десктопа) canon-таблицы выходили за ширину: левая
колонка обрезалась, появлялся внутренний горизонтальный скролл — из-за
фиксированной min-width (780–1000px), которую виджеты ставят локально, а
скелетон inline-стилем.
Централизовал в каноне components.css: `.table { min-width: 0 !important }`
снимает форс-ширину на всех экранах (!important — иначе scoped/inline
перебивают канон), таблица всегда влезает (width:100%) и переносит содержимое
на следующую строку вместо скролла. На узких экранах (≤599px) дополнительно
table-layout:auto + перенос в ячейках + плотнее паддинги, чтобы многоколоночная
таблица читалась переносом, а не клиппингом.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Пользователю в тост летел сырой ассерт контракта, напр.
`walletop TRANSFER: недостаточно средств на кошельке w.wal.share`.
Ему не нужны ни scope-префикс операции, ни служебное имя кошелька — он
нажал конкретную кнопку и так понимает, о каком кошельке речь.
Добавлен универсальный санитайзер sanitizeBlockchainError, встроенный в
единую точку показа ошибок FailAlert:
- снимает ведущий технический scope-префикс (`walletop TRANSFER:`,
`ledger2::apply:`, `o.mkt.supply:`) — распознаёт идентификатор с
`.`/`::` либо слово+ALLCAPS-опкод, кириллицу не трогает;
- убирает служебные имена ledger2-кошельков (`w.wal.share` и т.п.);
- нормализует пробелы и ставит заглавную первую букву.
Результат для примера выше — «Недостаточно средств на кошельке».
Обычные (нетехнические) сообщения проходят без изменений.
Полный технический текст остаётся разработчикам: FailAlert дублирует
сырой message в console.error, бэкенд пишет его в логи контроллера.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Баг: при «Получить возврат» с суммой больше L3-баланса платёж писался в
gateway-БД (AWAITING_AUTHORIZATION) ДО on-chain транзакции. Контракт wallet
отклонял заявку (недостаточно L3-средств), а в разделе «Платежи» оставался
висеть фантомный исходящий платёж со статусом FAILED.
Фикс — порядок операций в WalletInteractor.createWithdraw:
1. prepareWithdraw — все валидации (символ, дубликат hash, платёжный метод)
и сборка записи, БЕЗ записи в БД;
2. on-chain wallet::createwthd — при недостатке средств падает здесь,
ни одной записи о платеже не создаётся;
3. persistWithdraw — фиксация платежа в БД только после успешной заявки.
GatewayInteractor.createWithdraw разбит на prepareWithdraw/persistWithdraw
(старый combined-метод сохранён как обёртка). Порт и оба адаптера обновлены.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Программные оферты (program_id > 0) с Эпика 2 уходят на цепь через
wallet::signagree (commit 21e94ce0ed), а не soviet::sndagreement —
soviet::newagreement по ним не создаётся. Агрегатор пакета документов
(document-package-v1) искал приложения к заявлению только по
soviet::newagreement, поэтому подписанная оферта молча выпадала из links
и не отображалась как приложение к заявлению на повестке совета (при этом
сам документ виден в реестре).
Резолв каждого linkHash вынесен в общий хелпер buildLinkedAggregate;
носитель подписи ищется сначала в soviet::newagreement, затем фолбэком в
wallet::signagree. Унифицированы три дублировавшихся цикла (statement /
decision / acts). Добавлен регрессионный unit-тест.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In-app тело инбокса рендерится как текст ({{ n.description }}, без v-html —
payload несёт ФИО/заголовки, v-html = XSS-вектор), поэтому HTML-тег <br> из
шаблона показывался строкой «№C53E<br>От: …». Перевёл 2 in-app шаблона
(new-agenda-item, approval-request) с <br> на \n и добавил white-space:
pre-line на .notification-center__item-desc (line-clamp:2 сохранён).
Push не трогаю — его тела уже plain-text без тегов, отображается верно.
Email оставлен на HTML (<br>/<strong>) — рендерится почтовым клиентом.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Временный shim очистки persisted-localStorage от устаревшего «КПП банка».
К 01.09.2026 старые незавершённые черновики регистрации истекут — утилиту
и оба вызова (CreateUser/AddUser model) можно удалить.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Пайщики, начавшие регистрацию ИП/юрлица ДО удаления поля «КПП банка»
(commit 93b94326), имеют это значение в persisted-localStorage стора
регистратора (useRegistratorStore, persist:true). При построении мутации
RegisterAccount/AddParticipant org/entrepreneur объект уходил целиком,
включая bank_account.details.kpp → BankAccountDetailsInput его больше не
принимает → GRAPHQL_VALIDATION_FAILED, форму не отправить.
Защитный strip stripLegacyBankKpp() срезает kpp из bank_account.details
на входе в мутацию (createUser/signup и addUser/председатель). КПП самой
организации (details.kpp) не трогаем — легитимный реквизит юрлица.
Read-путь (старый фронт запрашивает details{kpp}) лечится перезагрузкой
страницы — селекторы SDK уже без kpp.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI vue-tsc (PR #60) после фикса lockfile дошёл до тайпчека и поймал хвосты:
- src-ssr/middlewares/{injectEnv,generateConfig}.ts: NOVU_APP_ID/BACKEND_URL/
SOCKET_URL слались в объект EnvVars, откуда поля удалены де-Novu → TS2353.
Убрал мёртвые строки.
- NotificationJournalWidget.vue: getName() возвращает string|undefined
(organization_data?.short_name) → .replace на possibly-undefined (TS2532).
Обернул в (getName(account) ?? '').
- NotificationCenter/model/index.ts: n.createdAt — DateTime-скаляр SDK типа
unknown, NotificationItem.date ждёт string|Date (TS2322). Каст as string.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI vue-tsc (PR #60) после фикса lockfile дошёл до тайпчека и поймал хвосты:
- src-ssr/middlewares/{injectEnv,generateConfig}.ts: NOVU_APP_ID/BACKEND_URL/
SOCKET_URL слались в объект EnvVars, откуда поля удалены де-Novu → TS2353.
Убрал мёртвые строки.
- NotificationJournalWidget.vue: getName() возвращает string|undefined
(organization_data?.short_name) → .replace на possibly-undefined (TS2532).
Обернул в (getName(account) ?? '').
- NotificationCenter/model/index.ts: n.createdAt — DateTime-скаляр SDK типа
unknown, NotificationItem.date ждёт string|Date (TS2322). Каст as string.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Удаление @novu/js из desktop/package.json (b500789dd6) не сопровождалось
пересборкой pnpm-lock.yaml → CI install --frozen-lockfile падал
ERR_PNPM_OUTDATED_LOCKFILE до шага typecheck. Пересобрал lockfile-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Получатель без активной push-подписки (не дал разрешения / нет PWA / 410)
давал {delivered:false,error} → worker ретраил 5× → терминальный FAILED:
ложные «Ошибка» в журнале и warn в логах для всех, у кого push не настроен
(в dev SW не регистрируется без ENABLE_PWA_DEV → у всех).
Ввёл исход skipped в ChannelDeliveryResult: канал неприменим к получателю.
Адаптер web-push возвращает skipped при 0 подписок / отсутствии username.
Worker гасит такую строку в canceled — без ретраев, без записи попытки в
журнал, без error-лога (debug); причину кладёт в lastError → тултип
«Отменено» на столе председателя. Гейт на слое отправки (состояние подписки
авторитетно в момент доставки), роутер outbox остаётся чистым.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Получатель в журнале:
- две строки — ФИО (резолв username→account через getName на фронте, кэш
на компонент) + AccountBadge с username и копированием. DTO журнала несёт
только username/subscriber_id, поэтому ФИО тянем getAccount'ом. Нет ФИО
(приватные данные недоступны) → одна строка с username.
Видимость ошибки доставки:
- worker раньше логировал только исключение тика; провалы отправки шли лишь
в БД → при падающем канале в логах пусто и не видно, что worker жив.
Добавлено: лог старта (onModuleInit), debug-heartbeat тика с числом строк,
warn на ретрай и error на терминальный провал — с контекстом
(workflow/канал/получатель/попытка) и текстом ошибки.
- в журнале на ячейке статуса «Ошибка» — q-tooltip с lastError (поле уже
отдаётся DTO/селектором), курсор help.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Футер «Показать все» удалён из личного колокола: журнал — админский
(стол председателя), пайщику туда не нужно; председатель открывает
журнал через пункт меню. Колокол = свежие из инбокса + «Прочитать все».
- Бейдж счётчика вытолкнут за угол кнопки через transform translate(45%,-45%)
— больше не «наезжает» на глиф колокола.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Журнал уведомлений зарегистрирован в реальном меню стола председателя
(extensions/chairman/install routes[0].children) — был воткнут в мёртвый
src/desktops/Chairman/model (не в реестре расширений), потому страница
была недостижима. Дубль из мёртвого манифеста убран.
- Колокол: спиннер заменён канон-скелетоном (.skel), тело фиксированной
высоты (~4 уведомления, max 60vh) — панель больше не «прыгает» при
скелетон → меньше элементов → пусто.
- Футер: вместо неясной пагинации «Все загружены/Показать ещё» —
навигация «Показать все →» в журнал (только для председателя; q-menu
закрывается при переходе).
- Бейдж счётчика смещён в угол колокола (-4/-4) — убран «наезд» на иконку.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Публичный call.element.io перешёл на слот-протокол MatrixRTC: имя LiveKit-комнаты
теперь считает lk-jwt-service как unpaddedBase64(sha256(JSON.stringify([roomId,
"m.call#ROOM"]))), а не старое sha256(roomId+"|m.call#ROOM"). Старая формула
перестала матчить webhook'и → секретарь молча игнорировал все звонки
(«Комната ... не в реестре транскрипции»).
Матчинг теперь сверяет оба формата (текущий + legacy для старых клиентов Element).
Тест на живой паре с прода (комната «Ферма на Паях», созвон 07.06).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Кнопка удаления не показывалась НИ у кого, и сервер отверг бы все удаления.
Причина: гейт опирался на blockchain_account, но on-chain аккаунт заводится рано
и присутствует уже у статуса Created — то есть у всех. Плюс на фронте сравнение
шло со строками в нижнем регистре, а GraphQL отдаёт enum UserStatus с большой
буквы (Created/Joined/...).
- interactor.deleteAccount: убран blockchain_account из guard; различитель —
participant_account (принят) + статус-allow-list (домен lowercase).
- ParticipantsTable.isDeletable: убран blockchain_account; allow-list через
Zeus.UserStatus (Created/Joined/Payed/Failed/Refunded). Registered (после приёма
взноса), Active, Blocked — не удаляются.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Idempotency-guard listener'а пропускал создание возврата при наличии ЛЮБОГО
REGISTRATION_REFUND. После первого цикла отказа возврат cycle1 оставался в БД
(его hash = registration_hash = sha256(username) детерминирован и совпадает с
новым), поэтому на втором цикле возврат не создавался, latest refund оставался
старше нового платежа, и фронт зависал на «платёж принят».
Guard теперь пропускает только возврат ТЕКУЩЕГО цикла — свежее текущего
вступительного платежа. Детекция цикла по дате — как в account.interactor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Повторная подача после отказа совета:
- createRegistrationPayment больше не переиспользует старый COMPLETED рег-платёж,
если возврат свежее его (цикл закрыт) — заводит новый ордер со свежим QR,
иначе пайщик видел исполненный QR, а в реестре совета платёж не появлялся.
- ReadStatement сбрасывает согласие с Уставом при входе в раздел (как doc-галочки).
- GenerateAccount сбрасывает «Я сохранил ключ» при входе в шаг (q-step не размонтируется).
Назначение платежа (memo) — единый полный источник:
- оговорка «НДС не облагается.» вынесена в константу VAT_EXEMPT_NOTE и добавлена во
все 4 назначения (вступительный, паевой, возврат паевого, возврат рег-взноса);
- qrpay/sberpoll больше НЕ дописывают «. Без НДС.» к Purpose — назначение целиком
в memo, без задвоения.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Два явных шага возврата при отказе совета (по просьбе): пока касса не подтвердила
исходящий платёж — показываем «возврат выполняется» без кнопки; после подтверждения —
«взнос возвращён» + кнопка. Иначе ранний клик упирался в reguser (карточка ещё не
снята) и падал.
- getAccount: registration_payment.status = PROCESSING пока возврат идёт,
REFUNDED по завершении (refundpay снял карточку — повторная подача возможна).
- WaitingRegistration: ветка isCouncilRefundPending (PROCESSING) — баннер + песочные
часы, без кнопки; isCouncilDeclined (REFUNDED) — баннер + «Подать заявку заново».
- fixData: ошибка через extractGraphQLErrorMessages вместо e.message
(раньше показывало «undefined» — у GraphQL-ошибки SDK message лежит глубже).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Вариант 1: после отказа совета и возврата взноса несостоявшийся пайщик может
подать заявку заново на том же аккаунте (а не упереться в тупик).
Контракт:
- reset_account_card: refundpay (и миграц.ветка declinereg) при закрытии кандидата
снимают карточку участника (type="", storages) — иначе повторный reguser падает
«повторное получение невозможно». Аккаунт и ключи сохраняются.
Бэкенд:
- registerBlockchainAccount: CreateAccount шлётся только если eosio-аккаунта ещё нет
(при повторной подаче он уже существует — иначе двойное создание падало бы).
- resetRegistration: ветка «отказ совета» — пропускает pre-payment гарды (аккаунт в
цепи и взнос принят здесь нормальны), требует возврат COMPLETED (карточку снял
refundpay), сбрасывает кандидата + user.status=created. Старые платежи не удаляет.
- getAccount: сигнал REFUNDED только если возврат свежее последнего рег-платежа —
после повторной подачи новый REGISTRATION перекрывает старый отказ.
Повторная подпись соглашений идемпотентна (signagree перезаписывает программу,
sndagreement падает лишь на confirmed — при отказе совета соглашение не confirmed).
Стандарт p.reg.refund: отмечено освобождение аккаунта для повторной подачи.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
На момент релиза двухфазного учёта будут кандидаты в статусе payed, чей платёж
принят по СТАРОМУ пути — без o.reg.inpay, то есть без баланса на w.reg.pend (76).
Новые confirmreg/declinereg делали бы TRANSFER/BURN с пустого суспенса → падение
«недостаточно средств».
Fallback по наличию баланса кандидата на w.reg.pend (helper
get_registration_pending_balance читает ledger2::userwallets cross-contract):
- есть баланс (новый путь): confirmreg → o.reg.setmin/setent (76→80/86),
declinereg → o.reg.refund (BURN 76, Dr 76/Cr 51);
- нет баланса (принят до релиза): confirmreg → o.reg.putmin/payent (прямой
ISSUE Dr 51/Cr 80|86, как раньше), declinereg → без on-chain проводки
(возврат банковским переводом из бэкенда, как было).
Весь блок помечен «снять условие после 30.07.2026»: к этому сроку все зависшие
на момент релиза кандидаты гарантированно получат решение совета.
registrator.wasm пересобран.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Раньше регистрационный взнос вставал на учёт одношагово только на одобрении
совета (confirmreg: Dr 51/Cr 80 + Dr 51/Cr 86), а приём денег кассой
(confirmpay) и отказ совета (declinereg) не делали проводок вообще.
Вводит суспенс-фазу через новый счёт 76 «Расчёты с пайщиками»:
- confirmpay (касса приняла деньги) → o.reg.inpay: Dr 51 / Cr 76, ISSUE w.reg.pend
- confirmreg (совет одобрил) → o.reg.setmin (Dr 76/Cr 80) + o.reg.setent (Dr 76/Cr 86),
TRANSFER w.reg.pend → w.reg.minshr / w.reg.entry
- declinereg (совет отказал) → o.reg.refund: Dr 76 / Cr 51, BURN w.reg.pend
(отдельный процесс p.reg.refund) + удаление кандидата
Новые сущности ledger2:
- счёт 76 PARTICIPANT_SETTLEMENTS (ACTIVE_PASSIVE)
- кошелёк w.reg.pend REGISTRATION_PENDING (USER_SHARED, program_id=0)
- операции o.reg.inpay/setmin/setent (p.reg.accept) + o.reg.refund (p.reg.refund)
confirmreg: проводки под гардами amount>0 (чинит латентный креш при initial==0).
declinereg: добавлен require_auth(_soviet) + возврат + чистка кандидата
(устраняет залипание записи кандидата, блокировавшее повторную подачу).
Синхронизированы оба зеркала (cpp registries + cooptypes TS), обновлён
snapshot реестра кошельков, добавлен p.reg.refund в process-hash-locator
(иначе integrity-check валит controller на старте). Стандарт p.reg.accept
обновлён под двухфазную схему, заведён p.reg.refund.standard.yaml.
Контракты registrator.wasm и ledger2.wasm собраны (static_assert'ы реестров прошли).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
notifyPaymentStatus выбирал workflow только по статусу (PAID → PaymentPaid
«Платёж принят»), игнорируя direction. Исходящий PAID — это возврат взноса
пайщику, и он получал письмо как на приём. Баг общий для всех возвратов
(регистрационный + паевой — оба идут через gateway.setPaymentStatus).
- Новый workflow notifications/payment-refunded «Возврат взноса выполнен»
(зеркало payment-paid, тот же payload).
- payment-notification.service: OUTGOING+PAID → PaymentRefunded, INCOMING+PAID →
PaymentPaid, остальное → PaymentCancelled.
Workflow засинкан в Novu (pnpm sync, 24 workflow, 0 ошибок).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backend: memo возвратного платежа теперь зеркалит назначение входящего взноса —
«Возврат вступительного и минимального паевого взносов №XXXX. Без НДС.» с тем же
номером (original.hash.slice(0,8), как в QR при приёме) и НДС-оговоркой, что
qrpay/sberpoll добавляют к Purpose. Кассир копирует as-is в платёжку.
Desktop: PaymentDetails показывал «Назначение платежа» с кнопкой копирования
только в ветке OUTGOING+payment_details. Возврат идёт без payment_details и падал
в «нет дополнительной информации». Добавлена ветка: если есть payment.memo —
показываем его через CopyableInput (с копированием).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
В БД registration_hash хранится lowercase (sha256().digest('hex')), а из
цепи checksum256 в declinereg.registration_hash приходит UPPERCASE — exact-match
findOneBy не находил кандидата, возврат взноса пропускался с warn.
Нормализуем входящий хэш .toLowerCase() в findByRegistrationHash.
Проверено на voskhod: candidate czeudubzzuql имеет registration_hash
f25ca396... (lowercase), warn пришёл с F25CA396... (uppercase) — тот же хэш.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. WaitingRegistration: крутящийся спиннер заменён статичной иконкой
hourglass_top с подписью «Ожидаем решение совета». Анимация загрузки
на процессе до 30 дней сбивала с толку — казалось, страница не дозагрузилась.
2. fixData (возврат к редактированию после отклонённого платежа) теперь зовёт
новый store.resetConsents(): сбрасывает agreements (устав/ПД/…) + подписи +
документы. Раньше галочка «прочитал устав» оставалась проставленной на
повторном проходе ReadStatement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
hover ставил background: var(--p-surface) (#fff на light) — карта на
--p-canvas (#f4f4f5) осветлялась до белого, выглядело как засвет выбора.
На dark было ок (canvas #0a0a0a → surface #141416 — чуть светлее).
Замена на --p-canvas-2: на light темнее канвы, на dark светлее — hover
идёт в одну сторону с темой в обеих.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Конфликт только в сгенерированном Zeus-клиенте (controller/zeus + sdk/zeus) — разрешён
перегенерацией codegen, не ручной разводкой:
- пересобран cooptypes dist (протух: DeclineRegistration был в src, не в dist → TS2339)
- generate-schema (56 резолверов) + generate-client
- zeus теперь надмножество: dev + fsm-recovery (resetRegistration) + refund (REGISTRATION_REFUND)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Конвенция для Actions/*: независимые от data чтения — в resolveParallel,
зависимые (getMeta/getDecision/getMeetQuestions) — после батча; ловушка TDZ;
системный шрифт вместо base64.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
В предыдущем коммите arial.ttf закоммитился битым: components/controller/.gitattributes
содержит `* text eol=lf`, что применилось и к бинарному шрифту → срезало 105 байт
\r (275572 → 275467), шрифт невалиден. Добавил `*.ttf/otf/woff binary` в
controller/.gitattributes (перекрывает правило выше) и перезаписал blob корректно.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Главное горло генерации PDF: в каждый html инжектился @font-face с Arial через
data:base64 (~270 КБ TTF). weasyprint декодировал и парсил весь шрифт на КАЖДЫЙ
рендер — ~1.3 сек на документ в 754 байта (замер: тот же текст со встроенным
шрифтом 1282–1715 мс, без него 120–289 мс).
Ставим Arial СИСТЕМНО в образ controller (fontconfig) — парсится один раз и
кешируется. Шрифт тот же файл (декод прежнего ArialBase64 → arial.ttf), имя
семейства "Arial", глифы/вёрстка идентичны.
- controller/assets/fonts/arial.ttf — наш Arial (275 КБ)
- controller/Dockerfile — fontconfig + COPY шрифта + fc-cache
- factory Generator — убран @font-face data:base64, оставлен font-family: Arial
- удалён src/Fonts/arial.ts (367 КБ base64, больше не нужен; bundle 1.48 → 0.75 МБ)
Итог по всей серии (warm-pool + parallel-fetch + системный шрифт):
generateProjectOfFreeDecision ~5 сек → ~400 мс (×10).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Сборка данных документа в каждом Action делала серию строго последовательных
await: getTemplate (2 сетевых запроса в explorer) + getMeta(getCurrentBlock) +
mongo-чтения (getUser/getCooperative/getProject/getVars) + udata/request/meet.
Сетевые запросы складывались в задержку ~1.5с поверх рендера PDF.
Добавлен хелпер DocFactory.resolveParallel({...}) — резолвит ВЗАИМНО
НЕЗАВИСИМЫЕ источники через Promise.all. В каждом из 46 Action-факторов
независимый префикс (template + mongo-чтения + независимые meet/request/udata)
сведён в один resolveParallel; зависимые вызовы оставлены последовательными
после батча, с сохранением порядка и аргументов:
- getMeta({title: template.title}) — зависит от template (TDZ в Promise.all);
- getFullName/getCommonUser — от user;
- getDecision/getApprovedDecision — от coop (+meta.created_at);
- getMeetQuestions/getGeneralMeetingDecision — от meet;
- getProgram — от request.
Поведение не меняется — только параллелизуются независимые чтения. Условные
ветки (bank_account по типу юзера, branch по is_branched, signature) и гварды
сохранены на местах. Файлы, где параллелить нечего (только template+meta,
meta зависит от template) — не тронуты.
tsc + eslint + unbuild — зелёные.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vue-tsc TS2719 в Decision/CreateProject/model: publishProjectOfFreeDecision
объявлен Promise<IAgenda | null>, но поле мутации nullable в схеме —
result включает undefined и не присваивается IAgenda | null.
Fix: return result ?? null.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Генерация PDF спавнила новый процесс `weasyprint` на каждый документ через
`exec`. Медленные 3-5 сек — это холодный старт Python: import всего стека
(cffi/pydyf/tinycss2/Pango/cairo/HarfBuzz) + скан fontconfig. Сам рендер
быстрый (<0.5 сек), но старт платился каждый раз, т.к. процесс умирал сразу.
Держим пул из N долгоживущих Python-процессов горячими (WeasyPool/WeasyWorker):
import платится один раз за жизнь бэкенда, далее каждый документ = чистый
рендер. Размер пула = env WEASY_POOL_SIZE (дефолт min(4, CPU)) — даёт реальный
параллелизм, одновременные генерации не сериализуются к одному процессу.
Воркер живёт внутри процесса controller'а: без докера, миграций и отдельного
сервиса. Питон берётся из /venv (controller/Dockerfile), фоллбэк python3.
Детерминизм сохранён: SOURCE_DATE_EPOCH=0 в env воркера. Заодно выровнял
weasyPrintVersion 62.3 → 67 (синхрон с pip install в controller/Dockerfile —
метадата producer'а документов больше не врёт).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getAgendaItemByHash сравнивал decision.hash с data.document.doc_hash (хэш только
содержимого), тогда как decision.hash в блокчейне == data.document.hash («общий
хэш» = doc_hash + meta_hash). Из-за этого матч НИКОГДА не срабатывал: publish
впустую опрашивал повестку весь таймаут (~4.5 c) и возвращал null — мгновенный
показ не работал ни разу, вопрос всегда дожидался поллинга страницы.
- Матч по data.document.hash (= decision.hash on-chain).
- Поллинг: проверка сразу (без слепой стартовой паузы), тик 400 мс — ловим
индексацию парсером action'а (~2 c) минимальной задержкой; publish ~2.2 c.
По таймингам: decision на чейне мгновенно, aggregate ~0.4 c; доминирует лаг
индексации парсером (~2 c) — единственный способ убрать его — синтез action,
отвергнут (требует подделки ~12 explorer/SHiP-полей BlockchainActionDTO).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Бэкенд возвращает пункт повестки только когда он полностью собран (decision из
блокчейна + проиндексированные парсером действие и документ-заявление) — то есть
он уже идентичен тому, что отдаёт getAgenda. Значит блокировать голос/утверждение
до следующего поллинга незачем: пользователь может действовать немедленно.
Убрана блокировка: pendingConfirmationIds (store/process-decisions) и
disabledDecisions (страница). Буфер-мердж в agendaStore оставлен как страховка от
«мигания» (вставленный вопрос держится, пока поллинг не вернёт его authoritative).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
После публикации свободного решения повестка показывала «Нет вопросов на
повестке» до следующего поллинга (10+ сек). getAgenda собирается join'ом таблицы
decisions из блокчейна с проиндексированными парсером действием newsubmitted и
документом-заявлением, поэтому только что созданный вопрос появляется не сразу.
Решение: мутация publishProjectOfFreeDecision теперь возвращает созданный пункт
повестки. Бэкенд после публикации делает settle-паузу (parser-индексация) и
извлекает готовый вопрос с повестки по хэшу документа; фронт вставляет его в
таблицу немедленно. Кнопки голосования/утверждения/правки на нём заблокированы,
пока ближайший getAgenda-поллинг не подтвердит вопрос authoritative-данными.
Backend:
- AgendaInteractor.getAgendaItemByHash / AgendaService.getAgendaItemByHash —
сборка одного пункта повестки по хэшу заявления (null, пока не проиндексирован).
- FreeDecisionService.publishProjectOfFreeDecision — settle-пауза + страховочная
петля, возвращает AgendaWithDocuments | null. Резолвер: тип возврата сменён с
Boolean на AgendaWithDocuments (nullable).
- AgendaModule экспортирует AgendaService; DecisionModule импортирует AgendaModule.
SDK: publish-селектор → форма agenda-item (rawAgendaSelector); regen zeus-клиента.
Frontend:
- agendaStore: буфер оптимистично вставленных вопросов + computed-мердж с
authoritative-списком (без «мигания»); pendingConfirmationIds для блокировки.
- CreateProjectButton: вставляет вернувшийся вопрос через insertCreated.
- process-decisions/страница: блокируют кнопки неподтверждённых вопросов.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Дополнена страница «Повестка совета» бизнес-языком: если решение не принято
или истёк его срок, вопрос снимается с повестки — председателем вручную
(отрицательный консенсус) или автоматически по истечении срока. Обновлён
mermaid-граф (ветки «большинство против» и «срок истёк»), добавлен раздел
«Снятие вопроса с повестки», в блок голосования добавлено описание индикатора
отклонения. Без скриншотов.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Голос «против» больше не затирает решение автоматически. Развязаны два пути
отказа:
- по истечению срока — авто (cancelexprd кроном chairman, ключом кооператива);
- по отрицательному консенсусу (большинство «против») — явным действием
председателя declinedec, до истечения срока.
Контракт (soviet):
- voteagainst: только фиксирует голос, без авто decline_and_erase_decision.
- Новый action declinedec (в cancelexprd.cpp): require_auth(coopname), проверяет
votes_against*100 > members*50 (зеркало votefor), переиспользует общую ветку
decline_and_erase_decision. Собран в CDT, declinedec в ABI.
- Стандарты sov.authpkg / sov.decision приведены к развязке.
cooptypes: action Declinedec + интерфейс IDeclinedec.
Контроллер:
- SovietBlockchainPort/adapter: declineDecision + getBoards.
- Мутация declineDecision (resolver/service/interactor + DeclineDecisionInput),
роль chairman.
- Повестка обогащается council_members_count (состав совета `soviet`, дёргается
один раз) — фронту для порога принятия/отклонения.
- chairman: убран флаг cancelApprovedDecisions и кривая подпись; крон по сроку
гасит ВСЕ истёкшие решения.
SDK: селектор council_members_count, мутация DeclineDecision; zeus regen (обе копии).
Desktop:
- BaseButton: сплошной вариант negative.
- VotingButtons: красный крестик при отрицательном консенсусе.
- QuestionCard: у председателя красная «Отклонить» (declinedec) вместо «Утвердить»
когда решение отклонено советом; фича Decision/DeclineDecision.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Баг: при перезаходе/перезагрузке страницы оплаты в момент приёма взноса
заводился второй регистрационный платёж. Дедуп findActivePendingPayment ловит
только status=PENDING — а когда первый платёж уже PAID/COMPLETED (принят,
аккаунт регистрируется), дедуп промахивался и createInitialPayment создавал
платёж-дубль. Его нельзя ни подтвердить (processIncomingPayment →
registerBlockchainAccount → «аккаунт уже зарегистрирован»), ни осмысленно
отклонить — мусорный сирота в реестре.
Фикс: регистрационный платёж одноразовый. Берём последний рег-платёж пайщика
и, если он в живом статусе (PENDING/PROCESSING/PAID/COMPLETED), возвращаем его
вместо создания нового. Новый ордер заводим только если прошлого нет либо он
FAILED/EXPIRED/CANCELLED (легитимная повторная попытка, в т.ч. после
resetRegistration). Авторитетная защита на бэке, не зависит от гонок на фронте.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
По ревью @ant: не хардкодить 'action::registrator::declinereg' строкой.
Добавил action-константу RegistratorContract.Actions.DeclineRegistration
(actionName='declinereg', тип IDeclinereg) в cooptypes — её не было.
Листенер подписывается через
action::${RegistratorContract.contractName.production}::${...DeclineRegistration.actionName}
по образцу signed-document-ingestion (SovietContract).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
При отказе совета в приёме пайщика (отрицательный консенсус >50% — PR #94 —
или истечение срока повестки) on-chain срабатывает registrator::declinereg.
Взнос по фондам на этот момент НЕ разносился (confirmreg не было), но реальные
деньги пайщика уже у кооператива на расчётном счёте — их нужно вернуть.
- Новый PaymentTypeEnum.REGISTRATION_REFUND (исходящий), отдельный от WITHDRAWAL
(возврат паевого), чтобы не путать термины. Лейблы + OUTGOING_PAYMENT_TYPES.
- RegistrationDeclineListener на action::registrator::declinereg: резолвит
кандидата по registration_hash, по исходному REGISTRATION-платежу берёт сумму
и номер (hash8 = тот же № из QR), заводит исходящий платёж PENDING с
назначением «Возврат вступительного и мин.паевого взноса №<hash8>».
Идемпотентно (повторный приход события не плодит возвраты).
- processOutgoingPayment: для REGISTRATION_REFUND подтверждение кассиром —
off-chain no-op (статус → COMPLETED, без completeOutcome). Взнос on-chain не
двигался, откатывать нечего; обратная проводка по расчётному счёту появится
здесь же, когда введём проводки на приёме платежа.
- Реестр платежей: для возврата спрятана кнопка «Отклонить» (отказать в
возврате нельзя — совет уже отказал, деньги обязаны вернуть).
- Codegen: PaymentType.REGISTRATION_REFUND проброшен в Zeus (sdk + controller).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Провайдер QR (Bank) не эмитит success-колбэк, деньги подтверждаются вебхуком
на бэке асинхронно — без поллинга экран висел на форме оплаты до перезагрузки.
Каждые 10с подтягиваем аккаунт; как только вступительный платёж покинул PENDING
(принят/отклонён) — уходим на шаг ожидания решения совета, который сам
показывает «платёж принят» либо причину отказа.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PNG-иконки manifest'а физически отсутствовали (на проде /icons/*.png отдавали
HTML-фоллбэк) → на домашнем экране дефолтная иконка. Генерим полный набор из
logo.svg (квадрат, белый фон, лого по центру, maskable-safe) + в динамическом
manifest основная иконка — масштабируемый /logo.svg (purpose any maskable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Образ desktop generic, кооп задаётся env контейнера (COOP_SHORT_NAME), а
manifest пёкся на сборке и коопа не знал → на домашнем экране всегда дефолтное
'Цифровой Кооператив'. Отдаём manifest динамически SSR-middleware из env по
пути /manifest.webmanifest (статический /manifest.json перехватывается
serveStatic ДО user-middleware, поэтому новый путь + переписываем <link
rel=manifest> в HTML). Плюс рантайм apple-mobile-web-app-title для iOS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Новый SW больше не активируется в фоне сам (skipWaiting/clientsClaim=false) —
ждёт в waiting, убирая гонку «подвисание между релизами» (старая страница +
новый SW). updated() шлёт window-событие 'sw:update-available'; boot 'pwa-update'
ловит его и показывает канон-тост UpdateAlert в левом нижнем углу (timeout 0,
кнопки «Обновить»/«Позже»). По «Обновить» → window.applyUpdate() → SKIP_WAITING
→ controllerchange → reload. Детерминированно, без фонового свопа версий.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan B (recovery + источник истины о шаге на сервере), поверх гарда A.
Backend:
- resetRegistration (self-scoped по токену): откат незавершённой регистрации
к редактированию до отправки в блокчейн. Снимает заморозку профиля/e-mail
(users.status → created), чистит документы кандидата и отпечаток заявления,
удаляет непринятую попытку вступительного платежа. Принятые средства
(PAID/COMPLETED) или уже созданный on-chain аккаунт → отказ (нужна
перерегистрация/возврат — отдельная ветка).
- PaymentRepository.delete.
Frontend:
- SignUp реконструирует шаг с сервера: Joined → шаг оплаты (переживает
перезагрузку и работу в другой вкладке), приоритет у registration_payment.
- WaitingRegistration «Исправить данные» дёргает resetRegistration перед
возвратом к данным (после подписи профиль заморожен, локального отката мало).
- Фича Account/ResetRegistration + Zeus-клиент (codegen).
Существующие статусы (joined/registered/active) не меняем, новых не вводим:
фронтовые active-гейты висят на on-chain user_account.status — другая ось,
не пересекается с mono users.status, который трогает recovery.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
На контракте совета теперь возможно не только заблокировать решение
голосованием, но и принять отрицательное решение: как только против
проголосовало больше половины состава совета, решение отклоняется
немедленно (не дожидаясь истечения срока) — той же веткой отказа, что и
просрочка: decline-callback в исходный процесс + удаление решения.
- Общая ветка отказа вынесена в decline_and_erase_decision (cancelexprd.cpp),
переиспользована в cancelexprd (просрочка) и voteagainst (негативный консенсус).
- Порог зеркален votefor: votes_against*100 > total_members*50, строгое «больше».
- Стандарты sov.authpkg / sov.decision приведены к новому поведению; заодно
убран cancelvote (в коде запрещён: eosio::check(false)).
Контракт soviet собран в CDT — зелёный.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Гард A: на цепь не уходят данные, отличные от подписанных в заявлении.
Каноничный отпечаток удостоверяющих личность полей фиксируется при
подписании заявления (registerParticipant → candidate.meta), сверяется
перед registerBlockchainAccount. Расхождение → BAD_REQUEST, платёж уходит
в FAILED с причиной (видна пайщику через registration_payment из #91).
Пропуск уже идущих регистраций: кандидаты без отпечатка в meta (заявление
подписано до внедрения гарда) не проверяются — grandfather.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Обработка отклонения платежа на этапе регистрации в SignUp:
- Session store: геттер registrationPayment из аккаунта.
- SignUp.vue onMounted: если есть незавершённый вступительный платёж —
восстанавливаем шаг WaitingRegistration после перезагрузки/в любой вкладке
(процесс может тянуться днями).
- WaitingRegistration.vue: ветка «Платёж не принят» с причиной из
registration_payment.message и кнопками «Повторить оплату» / «Исправить
данные» (откат назад по процессу — только по нажатию пайщика). Live-опрос
аккаунта раз в 10с, чтобы экран сам переключился на отклонение/приём.
- Payment/SetStatus: setCancelledStatus(id, message); кнопка «Отклонить»
теперь ставит CANCELLED (триггерит уведомление) с полем причины.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend под обработку отклонения регистрационного платежа (этап до создания
аккаунта в блокчейне):
- SetPaymentStatusInput: опциональное поле message — причина смены статуса;
сохраняется в payment.message и показывается пайщику.
- gateway.interactor.setPaymentStatus: персистит message при смене статуса
(уведомление о CANCELLED уже шлётся через PaymentNotificationService).
- PaymentRepository.findLatestByUsernameAndType — последний платёж типа.
- AccountDTO.registration_payment {status,message,hash,quantity,symbol}:
сводка вступительного платежа в getAccount. Источник истины для фронта,
чтобы восстановить шаг регистрации (ожидание/отклонение) после
перезагрузки и в любой вкладке. Заполняется пока нет participant_account.
- Регенерация Zeus-клиента (schema.gql gitignored).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'maintenance' на фронте включает полноэкранную заглушку «Техническое
обслуживание» (watch-desktop-health), которая накрывала бы саму страницу
установки. Поэтому статус остаётся 'initialized' на всё время install
(фронт показывает страницу установки, как и прежде), а в 'maintenance'
переводим ТОЛЬКО в catch при падении ПОСЛЕ записи в цепь — это блокирует
повторный запуск (guard требует 'initialized') и корректно сигналит, что
коопа в полу-установленном состоянии и требует ручного разбора.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Счётчик сходится к верному значению (5) за один деплой независимо от
порядка выполнения migrate registrator vs soviet. Сверено с цепью:
11 participants = 6 фантомов + 5 реальных (3 founders + 2 новых пайщика
Белоус Нина/Сергей, вступивших после инцидента).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Причина (инцидент 2026-06-04, gorozhane): InstallInteractor.install прогнан 3×
(2 провала + успех). Каждый прогон генерил случайный username и необратимо
регистрировал совет на цепи (adduser), а откат в catch чистил только off-chain.
На цепи осели 6 дублей-аккаунтов 3 учредителей + 1800 RUB фантома в паевом фонде.
Багфикс (install.interactor.ts):
- лок: статус → 'maintenance' ДО любых on-chain операций; повторный install
блокируется guard'ом (требует 'initialized'), что и плодило дубли;
- при ошибке ПОСЛЕ первого adduser off-chain rollback не делаем (сохраняем
привязку личность↔username для разбора), статус остаётся 'maintenance';
- при ошибке ДО on-chain — чистый откат + возврат 'initialized'.
On-chain очистка (вызовется автоматически на деплое, идемпотентна, scope=coopname):
- registrator::migrate — erase 6 фантом-accounts + пересчёт active_participants_count
из soviet::participants;
- soviet::migrate — erase 6 фантом-participants (гард: остаётся ≥1 реальный);
- ledger2::migrate — снять L3-доли фантомов на w.reg.minshr, на снятую сумму
уменьшить L2-кошелёк и бухсчета 80 (Cr, паевой фонд) и 51 (Dr, расчётный).
Off-chain коррекции (mongo individuals и пр.) — вручную, см. ~/fixes.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
КПП — реквизит юрлица, у ИП и физлиц его нет. Общий тип реквизитов
банковского счёта (IBankAccount.details.kpp) требовал КПП у всех держателей
счёта, из-за чего индивидуальный предприниматель не мог пройти регистрацию
(и фронт, и бэкенд заворачивали пустой КПП).
Поле снято во всех слоях:
- cooptypes: тип IBankAccount.details + шаблон ParticipantApplication
(render-аргументы и плейсхолдеры) + mock-данные registry
- controller: BankAccountDetails(Input) DTO, domain-интерфейсы, init-config
- factory: ajv-схема BankAccountSchema (properties + required)
- sdk: zeus-селектор bankAccountSelector + перегенерённый zeus-клиент
- desktop: формы ИП/юрлица/физлица, реквизиты, добавление способа оплаты,
CSV-импорт участников
КПП организации/кооператива (details.kpp, отчётность, qrpay/sberpoll)
НЕ затронут — это легитимный реквизит юрлица.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Story 9.9 «Стол восхода: approve pending packages» — backend-сторона.
GraphQL contract:
- Query.appsCatalogPendingModerations(status?: ModerationStatusEnum, limit?: Int)
→ [ModerationRequestDTO]. По умолчанию SUBMITTED. Chairman-only.
- Mutation.approveModeration(data: ApproveModerationInputDTO!)
→ ApproveModerationResultDTO. Discriminated outcome:
applied / pendingChain (423) / conflict (409) / requiresOverride (403) / failed.
- Mutation.rejectModeration(data: RejectModerationInputDTO!)
→ RejectModerationResultDTO. Discriminated outcome: applied / conflict / failed.
HTTP-service: 3 новых метода (listSubmittedModerations / approveModeration /
rejectModeration) на ca-admin endpoints, добавленных параллельным
apps-catalog PR #7. Mapping snake_case → camelCase; degraded mode на
list — пустой массив, на mutations — failed с явной причиной (чтобы UI
не вводился в заблуждение «approved»).
ReleaseScope передаётся как InputType с union-style полями
(type + subnets?/coopnames?); резолвер валидирует соответствие type →
непустой массив перед отправкой на ca-admin.
Зависимость: apps-catalog PR #7 (GET /v1/admin/moderation) должен быть
смёрджен в feat/E9-demo-app-pilot до того, как этот endpoint можно
использовать в e2e.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Story 10.4 — pipeline установки subgraph-расширения:
1. (опц.) OCI token у CA-auth → docker pull образа;
2. (опц.) docker compose up сервиса;
3. healthcheck poll по url subgraph'а до 200 OK;
4. upsert в subgraph_registry (Apollo Gateway за ≤10с подхватит);
5. на failure — composeDown rollback + discriminated outcome {applied|failed}.
InstallController стал тонкой обёрткой над InstallOrchestratorService.
Внешние зависимости (docker shell / fetch / CA-auth) изолированы за
портами; тесты (7/7 green) гоняют сценарий через ин-мемори моки без
реального docker daemon'а и сети.
Что ОСТАЁТСЯ для отдельной ветки под этим зонтиком:
- wiring approve→install REST из mono controller (mutation approvePackage);
- desktop notification после успешного install'а;
- E2E на реальном compose + chatcoop subgraph (Story 10.8).
Story 9.3.b-rel — мутация выкладки нового релиза ранее зарегистрированного
пакета. Прокидывает manifest + версию + tarball_sha256 на ca-admin
POST /v1/admin/releases, который сам валидирует manifest Zod-схемой
(PackageManifestSchema) и подписывает on-chain apps::setrelease от
имени chairman'а кооператива-оператора каталога.
Backend (apps-catalog-proxy):
- PublishReleaseInputDTO / PublishReleaseResultDTO — GraphQL Input/Object;
manifest как GraphQLJSON (произвольная структура валидируется на
ca-admin'е).
- AppsCatalogHttpService.createRelease(): POST /v1/admin/releases;
discriminated outcome — applied (с опциональным transaction_id) |
invalidManifest (HTTP 422 INVALID_MANIFEST мапится явно, чтобы UI
показывал валидационную ошибку отдельно от network/auth) | failed
для прочих.
- AppsCatalogProxyResolver.publishRelease(): @AuthRoles(['chairman']).
Под архитектуру E10 manifest содержит ссылки на артефакты в Nexus:
- coopenomics.backend.image — docker-image (subgraph расширения);
- coopenomics.frontend.tarball — npm tarball (desktop-bundle).
Сам manifest НЕ валидируется на стороне controller'а — это делает
ca-admin (там Zod-схема и она же будет расширена для package.json
полей в story 10.7).
Frontend (форма Опубликовать релиз) — отдельной story 9.3.b-ui после
codegen'а Zeus.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Story 9.3.b-pub стола разработчика — мутация для воскходовского
chairman'а: координаты пакета (@scope/name + owner + compatible_subnets)
прокидываются на ca-admin POST /v1/admin/package, который сам
подписывает on-chain action apps::regpkg от имени chairman'а
кооператива-оператора каталога.
Backend (apps-catalog-proxy):
- PublishPackageInputDTO / PublishPackageResultDTO — GraphQL InputType +
ObjectType (camelCase для Zeus, без snake-case утечек).
- AppsCatalogHttpService.registerPackage(): POST на /v1/admin/package;
discriminated outcome (applied | conflict | failed) — мапит 409 в
conflict, прочие axios-ошибки в failed; degraded-mode (без
APPS_CATALOG_API_KEY) возвращает failed с явным сообщением, чтобы UI
не вводился в заблуждение «опубликовано».
- AppsCatalogProxyResolver.publishPackage(): @UseGuards(GqlJwtAuthGuard,
RolesGuard) + @AuthRoles(['chairman']) — мутация доступна только
председателю кооператива.
Multipart upload install.js под архитектуру E10 НЕ делаем: расширения
теперь публикуются как npm-package + docker-image в Nexus отдельной
процедурой (`npm publish` / `docker push`) через scoped JWT от
CA-auth. Эта мутация — только on-chain маркер «такой пакет
существует»; релизы с manifest'ом — story 9.3.b-rel.
schema.gql / Zeus client — codegen за пределами этого PR (mono-каноном
pnpm generate-schema + generate-client + sdk build делается отдельным
коммитом, чтобы PR с backend changes не зависел от пересборки SDK
артефактов).
Frontend — отдельной story 9.3.b-ui: DeveloperPublishReleasePage.vue
сейчас заглушка, форму подключим после codegen'а Zeus.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Epic 10 Story 10.2b — минимальный subgraph на extension-sdk доказывает что
Federation v2 driver + JWT module + healthcheck реально работают вместе.
Smoke результат (SUBGRAPH_PORT=33001 JWT_SECRET=test-secret COOPNAME=voskhod):
- GET /_health → {"status":"ok","version":"unknown","uptimeSeconds":N}
- POST /v1/graphql { helloFromExtension(name:"мир") { greeting fromExtension } }
→ {"greeting":"Здравствуй, мир!","fromExtension":"hello-subgraph@0.1.0"}
- POST /v1/graphql { _service { sdl } } → returns federation v2 SDL с
@key/@external/@requires/@shareable директивами
Также в этом коммите:
- extension-sdk переключён с unbuild на обычный tsc (unbuild esbuild не
поддерживает декораторы для target ES2020). main: dist/index.js, CommonJS.
- @types/passport-jwt в devDeps.
Cross-extension entity references (SharedCooperator @key + ResolveField)
живут в chatcoop subgraph'е (story 10.8) — там есть локальная сущность
ChatThread которая ссылается на пайщика. Этот PoC только доказывает
базовую механику.
Epic 10 Story 10.1 — coopback становится первым federation v2 subgraph'ом.
Локальная core-схема компилируется с federation директивами (`autoSchemaFile.federation: 2`).
Endpoint остаётся на /v1/graphql; non-federated клиенты (desktop) не должны
заметить переключения — Apollo Federation добавляет лишь служебные
`_service { sdl }` и `_entities` query.
Gateway-режим (Apollo Gateway + IntrospectAndCompose) зайдёт отдельной
story 10.1b — поверх этого subgraph'а + registry в Postgres. На MVP пока
один subgraph, gateway не нужен.
Зависимости:
- + @apollo/subgraph ^2 (peer для federation 2 в @nestjs/apollo)
@key директивы на core entity (User/Cooperative/Account/Wallet) добавятся
по мере вытаскивания первых extension'ов — отдельными точечными PR.
Новый push в PR отменяет ещё бегущий typecheck той же ветки —
прежде 30-минутные прогоны копились очередью.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mono controller:
- AppsCatalogHttpService.fetchInstallScript(scope, name) — fetch text
install.js из ca-admin /v1/public/packages/:scope/:name/install.js;
degraded mode (null) при отсутствии env-config'а apps-catalog'а.
- Новый REST controller AppsCatalogInstallScriptController
`GET /v1/apps-catalog/install/:scope/:name` под HttpJwtAuthGuard:
валидирует scope/name regex'ом, проксирует на ca-admin, отдаёт
text/javascript + Cache-Control: no-store. Admin-API key остаётся
на backend'е.
- AppsCatalogProxyModule: добавлен controller в module.controllers[].
mono desktop:
- remote-loader.ts: реальный fetch + CJS-eval. fetchInstalledRemotePackages
читает Queries.Extensions.AppsCatalogRemotePackages, парсит packageId
на (scope, name). loadRemoteExtension качает install.js по REST через
axios responseType:'text' с JWT desktop'а, оборачивает в
`new Function('module','exports','require', code)`, вызывает
`module.exports.install()` → IWorkspaceConfig[].
- Try/catch per item (NFR15-стиль): падение одного пакета не валит
остальные. require() сразу throw — пакеты должны быть CJS без
внешних зависимостей (sandbox — Story 11.x).
- sha256 пока не проверяем (release-таблица будет в 9.4.c).
Demo path: voskhod publishes @voskhod/demo-app → ca-admin отдаёт
examples/demo-app/install.js → controller-proxy → desktop boot тянет +
eval'ит + получает workspace «Demo» с роутом demo-hello.
При nullable=true декоратор @Field NestJS GraphQL не умеет вывести тип из
TS-reflection-метаданных (TS пишет в metadata-ключ Object). Без явного
type fn (() => String) — UndefinedTypeError на старте.
Фикс:
- @Field({ nullable: true }) → @Field(() => String, { nullable: true })
для lastActiveVersion: string | null.
- .env.example добавлены APPS_CATALOG_URL и APPS_CATALOG_API_KEY с
готовыми значениями для dev (URL = DNS-имя ca-admin в mono-shared
сети).
Verify: coopback стартует чисто (Nest application successfully started),
DTO виден через introspection (__type AppsCatalogRemotePackageDTO →
поля packageId/publisher/title/description/rubPerMonth/lastActiveVersion/
compatibleSubnets), guard срабатывает (без JWT → 401 Unauthorized).
Story 9.5.b mono часть: добавляет модуль расширения
`apps-catalog-proxy` с GraphQL Query.appsCatalogRemotePackages, который
проксирует запрос на apps-catalog/ca-admin /v1/public/packages.
Архитектурно — выбран альтернатива C (см. apps-catalog
docs/epics/E9-demo-app-pilot.md, V2 follow-ups): браузер ходит в каталог
через mono controller, а не напрямую в ca-admin. Поэтому:
- admin-secret apps-catalog'а живёт в env controller'а, не в SPA;
- нет CORS — desktop запрос на свой же origin;
- единый канон mono GraphQL (никаких raw HTTP в desktop).
Компоненты:
- AppsCatalogRemotePackageDTO (@ObjectType): packageId/publisher/
compatibleSubnets/lastActiveVersion + UI-поля title/description/
rubPerMonth (в V1 description и rubPerMonth — заглушки; в V2 будут из
package manifest и apps-catalog pricing).
- AppsCatalogHttpService: axios-клиент к ca-admin с Authorization: Bearer
admin-key из APPS_CATALOG_API_KEY. Degraded mode (пустой список без
ошибки), если env пустой — чтобы dev/CI без apps-catalog не валили
desktop boot.
- AppsCatalogProxyResolver: @Query, @UseGuards(GqlJwtAuthGuard) — каталог
видят только авторизованные пайщики.
- Регистрация в ExtensionsModule.
Config:
- Новые env APPS_CATALOG_URL и APPS_CATALOG_API_KEY (оба optional).
- Зеркальные поля в config.apps_catalog.{url, api_key}.
TODO (требует ручного запуска на остановленном контейнере, см. CLAUDE.md:
'pnpm generate-schema / generate-client — не запускать локально'):
- pnpm -F controller generate-schema → пересоздать schema.gql.
- pnpm -F controller generate-client → graphql-zeus в SDK.
- pnpm -F sdk build → unbuild dist.
Без codegen фронт ещё не сможет дёрнуть резолвер через Zeus.
Секция «Удалённые пакеты» с карточкой @voskhod/demoapp (publisher,
price 1000 ₽/мес) + dialog «Подписаться» (V1 — заглушка).
Why: Анна видит реальный второй раздел, отделённый от bundle-расширений,
и проходит UX-сценарий клика «Подписаться» — без backend-резолверов.
Реальный список remoteExtensions через Query.appsCatalogRemotePackages
+ Mutation.appsCatalogSubscribe — отложено в 9.5.b (см. mono#64
follow-ups).
Стили — токены MONO Design System (--p-*), никаких magic hex.
V1 минимум для UI ExtensionStore:
- Информационный banner сверху grid'а карточек, объясняющий что bundle-
extensions ниже бесплатные, а remote-пакеты с подпиской появятся
на следующем шаге пилота.
- Реальная карточка remote-пакета с кнопкой «Подписаться» / «Уже
подписан до DD.MM» — story 9.5.b (требует расширения extStore
на kind/subscribed/pricingRubPerMonth + backend-резолверы coopback'а).
Refs: docs/integration/native-to-catalog-research.md §9 story 9.5.
9.3 — components/desktop/extensions/developer/:
- install.ts: workspace `developer`, defaultRoute=developer-my-packages,
4 страницы (packages, publish, subscribers, pricing), все under
meta.roles=['chairman']. Регистрация в extensions-registry.ts.
- pages/: V1 Vue-заглушки с описанием TODO (без реальных мутаций).
Mutation flow (POST /v1/admin/package, /releases, /pricing с подписью
chairman'а) — story 9.3.b.
9.4 — processes/init-installed-extensions/remote-loader.ts:
- Контракт fetch/load для remote-пакетов с активной подпиской: тип
RemoteExtensionDescriptor, функции fetchInstalledRemotePackages,
loadRemoteExtension, installRemoteExtensions.
- V1 — заглушка возвращающая []. Не падает, логирует пропуск.
- index.ts: после bundle-pass — remote pass через installRemoteExtensions;
try/catch чтобы падение remote-flow не валило bundle extensions.
- TODO 9.4.b: реальный fetch к coopback + tar-x + dynamic require
+ sha256-check + (V2 sandbox).
Видимость стола: пока через meta.roles в install.ts. Полноценная
авторизация через DesktopWorkspace.grants с флагом is_operator_coop —
9.3.b/9.5.
Refs: /home/admin/mono-ai-5/docs/integration/native-to-catalog-research.md §9.
Финальные решения по V1 пилоту (без миграции native):
- demo-app = пустой Hello-стол, @voskhod/demo-app
- Pricing 1000 RUB/мес (60 сек на dev), trial 15 мин
- partner1 как покупатель через pnpm boot:extra (на той же цепи)
- Self-subscription bypass: воскод как оператор подписан бесплатно
- Без кэша tarball'ов — каждый desktop boot качает заново
- Стол Разработчика = новый bundle-extension developer, виден chairman оператора
- partner1 desktop+coopback в отдельном docker overlay из apps-catalog
Epic 9 раскладка на 8 stories с зависимостями. Реализация — следующими
коммитами в зонтичной ветке apps-catalog mono + feat/E9-demo-app-pilot
в apps-catalog репо.
html2pdf.js на верхнем уровне модуля трогает `self` → при SSR-бандле Quasar
падает [Quasar] boot error: ReferenceError: self is not defined. Экспорт PDF
нужен только на клиенте по действию пользователя — переносим импорт в
dynamic import() внутри exportFormToPdf, сервер модуль не вычисляет.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Были перекрещены относительно реестра документов: draft 3=PrivacyPolicy,
4=UserAgreement (и тексты on-chain drafts, и cooptypes Registry, и protocol-vars
совета это подтверждают). Только make_base_coagreements крестил user↔privacy.
Затрагивает вновь создаваемые коопы; существующим нужна отдельная миграция.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getNotifications/getNotification/resendNotification сверяют coopname
(фильтра и найденной строки) с config.coopname → ForbiddenException на чужой.
Канон-паттерн контроллера (free-decision/branch interactor). Federation-hardening.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Документ остаётся в скроллящемся body BaseDialog, кнопка вынесена в слот
footer (.base-dialog__foot, вне overflow) — устраняет залипание, когда до
кнопки нельзя домотать (стек maximized-диалогов RequireAgreements).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code-review нашёл: --p-elev-2 нет в tokens.css → box-shadow не отрисовывался.
Канон-тень overlay (поповер) — --p-shadow-pop.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Эпик 6.5: страница «Журнал уведомлений» (Chairman desktop, /notifications-journal).
Канон-таблица .table-wrap (дата/получатель/тип/канал/статус/попытки), FilterBar по
каналу и статусу, BaseBadge-статусы, кнопка «Переотправить» на строке (resend →
новая строка в очереди, перечитываем). TableSkeleton/EmptyState/«Загрузить ещё».
Имя типа уведомления — из каталога Workflows. Только канон-компоненты + токены.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Эпик 6.4: виджет NotificationCenter переписан с Novu UI на доменный канон-компонент
shared/ui/domain/NotificationCenter (колокол .icon-btn + BaseBadge-счётчик + q-menu).
Pinia-стор: лента инбокса/unreadCount/markRead/markAllRead через SDK, оптимистичные
отметки, поллинг непрочитанных 30с (live-подписки нет — опрос), тост на новое.
Категория группировки выводится из workflowId. Удалён @novu/js и NOVU_* из env.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Эпик 6.6 (backend-часть): личный инбокс канала In-app поверх notification_inbox.
getInboxNotifications (лента по сессии, пагинация канон), getUnreadNotificationsCount
(бейдж колокола), markNotificationRead/markAllNotificationsRead. Скоуп строго по
subscriber_id из JWT — пайщик видит только свой инбокс (ownership на markRead).
Live-подписка onNotification отложена: graphql-ws/PubSub в репо нет — фронт poll'ит.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop-in переключение ~28 call-sites в 15 сервисах с
NOVU_WORKFLOW_PORT.triggerWorkflow на NOTIFICATION_PORT.notify (Центр
уведомлений DC v3). Listener'ы/события не тронуты — меняется только тело
вызова: name→workflowId, coopname обязателен (action.coopname/config.coopname),
recipient дополнен username (адресация in-app/web-push).
NotificationCenterModule помечен @Global — порт cross-cutting, консьюмеры
инжектят без импорта модуля (как NOTIFICATION_SUBSCRIPTION_PORT).
Cross-plugin InterCoopCalendarEventNotificationPort (chatcoop) сохранён,
мигрировано лишь тело. Из application NotificationSenderService убраны мёртвые
Novu-измы (broadcast/cancel/getStatus) — не использовались.
tsc: 0 новых ошибок (17 = baseline). NOVU_WORKFLOW_PORT-биндинг остаётся
мёртвым до удаления в эпике 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OutboxWorkerService @Interval(3000): поллит notification_outbox по индексу
(status, scheduledAt), шлёт через порт канала, исход пишет в журнал
notification_deliveries. at-least-once: backoff-ретрай (5s→30s→2m→10m→1h,
maxAttempts), после исчерпания — терминальный failed (переотправка на столе
председателя). Реклейм зависших SENDING после рестарта. Строка = один канал →
независимые попытки по каналам. Статусы — enum. ScheduleModule.forRoot()
подключён в app.module. Параметры worker'а из env с дефолтами.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
InAppChannelAdapter за InAppChannelPort: доставка in-app = запись
отрендеренного из каталога уведомления в notification_inbox. Новая
сущность notification_inbox (isRead/readAt, индексы под ленту + unreadCount).
ChannelMessage расширен outboxId + actorSubscriberId (worker даёт из outbox).
Live-подписка/unreadCount/mark-read — read-side инбокса, в эпик 6.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EmailChannelAdapter (nodemailer, SMTP из config.email; disabled при пустом
SMTP_HOST, наружу не стучится — NFR11). WebPushChannelAdapter: контроллер
доставляет сам своим подписчикам напрямую через WebPushService на endpoint'ы
из web_push_subscriptions по username (фикс пушей — точки доставки, не ключи);
410 → деактивация подписки. Шаблоны рендерятся из каталога @coopenomics/
notifications (controlValues.subject/body), mustache-интерполяция {{payload.x}}
без liquidjs (лишняя зависимость для MVP). Порты EMAIL_CHANNEL_PORT/
WEB_PUSH_CHANNEL_PORT в NotificationCenterModule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Контракты EmailChannelPort/InAppChannelPort/WebPushChannelPort + ChannelMessage/
ChannelDeliveryResult + DI-токены. Только интерфейсы — реализации в эпике 2;
worker эпика 3 зависит от абстракций, не от провайдеров. Имена без I-префикса
(канон портов репо).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NotificationService implements NotificationPort: резолвит каналы по типу из
каталога @coopenomics/notifications (steps[].type, только email/in_app/push),
для каждой пары получатель×канал пишет строку в notification_outbox идемпотентно
(ON CONFLICT DO NOTHING по unique idempotencyKey). Опциональный EntityManager —
запись в транзакции вызывающего (story 1.3). idempotencyKey = sha256(coopname|
workflowId|subscriberId|channel|stable(payload)) с детерминированной сериализацией
payload (story 1.5). email-канал — только при подтверждённом email (gate).
Модуль зарегистрирован в app.module, экспортирует NOTIFICATION_PORT.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Сущности notification_outbox (очередь recipient×channel, hot-path индекс
(status, scheduledAt), unique idempotency_key) и notification_deliveries
(append-журнал попыток, индексы по coopname/recipient для стола председателя
и личного инбокса). Статусы — enum, не строки. Фундамент для роутера (1.3),
worker'а (эпик 3) и стола председателя (эпик 6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Порт notify(NotifyInput) + типы входа (coopname обязателен как federation-инвариант,
workflowId ссылается на каталог @coopenomics/notifications, NotificationChannel enum
email/in_app/push). Старый NovuWorkflowPort сохраняется до миграции call-sites (эпик 4),
удаляется в эпике 5. Часть истории 1.1 эпика 1 (C28-22).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Дефолт реестра совета был newresolved → status=Resolved → виден только
утверждённый документооборот, submitted-документы (в т.ч. свежеподанные
заявления) пропадали. Переключатель вкладок на странице скрыт
(showFilter=false), сменить вид было нельзя. Выравниваем с реестром пайщика:
newsubmitted → status не ограничивается → совет видит ВСЕ документы кооператива.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
freedecision эмитит newresolved и newdecision по одному package в одной
транзакции — оба @OnEvent-хендлера срабатывают одновременно. safeReassemble
(newdecision/newact/newlink) пере-upsert'ил все строки пакета ПОЛНЫМ набором
колонок со сталым row.status, прочитанным до коммита newresolved → затирал
resolved обратно в submitted. Решение совета застревало в submitted и пропадало
из реестра совета (дефолт «Только утверждённые» = status=Resolved).
Пересборка теперь обновляет ТОЛЬКО агрегат-производные колонки
(document_aggregate/full_title/content_text/signers_text) точечным UPDATE.
status/block_num/source_action_data принадлежат статус-событиям и пересборкой
не трогаются → конкурентный newresolved не откатывается. Новый метод
SignedDocumentRepository.updateAggregate + SignedDocumentAggregateUpdate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Предыдущий фикс снимал лоадер, но onSelectWorkspace пушил на mainRoute.name —
родительский layout-роут без компонента → серый экран. WorkspaceSwitcher
работает потому что зовёт goToDefaultPage(), который через getDefaultPageRoute()
резолвит реальную лист-страницу (authorized_default_route / defaultRoute /
первый child) И сбрасывает isWorkspaceChanging. Используем его же.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Кнопка «Утвердить» совета слала authorize+exec напрямую из браузера ключом
председателя — при HTTP 504 от ноды ошибка терялась в браузере, бэкенд её не
видел и не логировал. Переводим действие на проведение через контроллер ключом
кооператива, как остальные soviet-мутации (sendAgreement) — теперь ошибки связи
с блокчейном попадают в логи бэкенда.
Контракт soviet: authorize/exec require_auth(chairman/executer) → require_auth(coopname).
Согласие председателя не теряется — закреплено его личной подписью на document2
(verify_document_or_fail) + проверкой is_valid_chairman.
- contracts: authorize.cpp/exec.cpp — require_auth(coopname)
- controller: новая мутация authorizeDecision (DecisionAuthorizeModule) + порт/адаптер,
пушит authorize+exec одной транзакцией ключом кооператива из vault
- cooptypes: authorizations actor chairman/username → coopname
- sdk: Mutations.Decisions.AuthorizeDecision + регенерация schema.gql/zeus
- desktop: фича AuthorizeAndExecDecision и process-decisions зовут бэкенд-мутацию
вместо useGlobalStore().transact; подпись документа остаётся клиентской
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
selectWorkspace() ставит isWorkspaceChanging=true (full-page WindowLoader
поверх router-view), а сбрасывал флаг только goToDefaultPage(). Обычное меню
(WorkspaceSwitcher) зовёт goToDefaultPage и лоадер снимается; command palette
(onSelectWorkspace/onSelectPage) делал прямой router.push без сброса флага —
лоадер висел вечно. Тот же leak в ConnectionAgreementStepper.goToQuickClient.
Снимаем флаг через setWorkspaceChanging(false) в .finally() после навигации.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Глобальный ::selection был --p-primary-soft (teal 10% alpha) — болотный,
почти невидим. Меняем на solid --p-accent + белый текст: видно на любом фоне.
Тост-специфичные ::selection-override (.q-notification/.toast) убраны —
accent читаем и на тёмной поверхности тоста, выделение единое везде.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Страницы deep-link (карточка документа, name=document-details/
user-document-details) помечены meta.hidden=true и в навигацию не
попадают — иконка им не нужна. Drawer рендерит q-icon под v-if='item.icon',
undefined безопасен. Убирает TS2741 'Property icon is missing'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Карточка реестра вела на страницу документа, передавая rawDocument.hash — а это
doc_hash (хеш самого документа, без подписи). Бэкенд же ищет документ по колонке
hash (подписанный документ = содержимое + подпись): UPPER(d.hash)=UPPER(:hash).
Значения разные → getDocuments возвращал 0 → «Документ не найден».
Из поиска работало, потому что searchDocuments отдаёт именно hash (колонку), а не
doc_hash. Карточка теперь берёт тот же document.hash (= колонка hash в реестре).
Проверено по БД: фильтр по doc_hash → 0 строк, по document.hash → 1; у всех
записей document.hash == колонка hash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Реестр документов переведён с таблицы на канон плоские строки (DocumentRow):
строка кликабельна и ведёт на отдельную страницу документа. Таблица с
инлайн-раскрытием оставлена только для Union-страницы (expand=true).
- DocumentCardsList: список DocumentRow (заголовок, дата, ФИО подписантов,
скачивание в actions), скелетоны при загрузке, «Загрузить ещё». Статус-чип
не показываем — в реестре всё подписано (канон stop-signal).
- ListOfDocumentsWidget: проп expand — карточки по умолчанию (совет/пайщик),
таблица для Union (передаёт expand=true) — поведение Union не меняется.
- useDocumentNavigation перенесён из фичи поиска в entity Document
(переиспользуют и поиск, и карточный список — чистые слои).
- DocumentDetailsPage: кнопка «назад» теперь в липком баре (sticky,
top=--p-topbar-h), видна всегда при прокрутке документа.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
activeWorkspaceName инициализируется асинхронно — при холодном переходе по
прямой ссылке на документ совета скоуп мог упасть в скоуп пайщика и документ
«не находился». Имя роута (document-details / user-document-details) доступно
сразу и детерминированно.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Прошлый подход (фильтр реестра по ?document=<hash> + баннер) отклонён —
нужна полноценная отдельная страница документа с переходом назад и deep-link.
- DocumentDetailsPage: canon-страница по эталону карточки собрания (back-link
под шапкой, имя документа в заголовок через setPageTitleOverride, скелетон,
EmptyState; контент — ComplexDocument). Грузит документ сама по hash из роута,
поэтому открывается и прямым переходом по ссылке.
- Роуты documents/:hash в обоих расширениях: document-details (совет) и
user-document-details (пайщик), hidden — по аналогии с meet-details.
- DocumentModel.loadDocument(username, hash): точечная загрузка одного документа
(статус-агностичный фильтр newsubmitted), не трогает состояние списка.
- Клик в поиске ведёт на страницу документа (useDocumentNavigation выбирает роут
по рабочему столу), а не фильтрует реестр.
- Откат фильтр-подхода: удалены SearchedDocumentBanner и useDocumentRouteFilter,
страницы реестра/виджет/таблица возвращены к baseline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Клик по результату поиска вёл в никуда (openDocument был пуст) — найденный
документ нельзя было посмотреть. Теперь клик кладёт hash в ?document=<hash>
текущего роута; страница реестра читает его и сводит список к одному документу,
авто-раскрывая его. Переиспользуем готовый backend-фильтр filter.document.hash
(тот же путь, что у Union-страницы) — без изменений API и codegen.
- useDocumentRouteFilter/useDocumentNavigation: чтение и установка ?document.
- SearchedDocumentBanner: баннер «показан один документ» с возвратом к списку.
- Обе страницы реестра (совет + пайщик) фильтруют виджет по выбранному документу.
- Виджет реагирует на смену filter и форсит статус-агностичный newsubmitted при
hash-фильтре (иначе на вкладке «только утверждённые» неутверждённый документ
из поиска не нашёлся бы).
- DocumentsTable авто-раскрывает строку по expandHash (регистронезависимо).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
В экране ожидания регистрации (WaitingRegistration) «до 24 часов» — неактуально:
реальный регламентный срок рассмотрения советом — до 30 дней (обычно решение
принимается за день-два). Поправили текст под фактический срок.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Большая пустая полоса между строкой поиска и первым результатом мешала —
снят верхний padding секции результатов (q-pt-none), список начинается ближе.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Поиск показывал username («ant») и сырой full_title с хвостом ` - - дата.pdf`.
Приводим к виду реестра на главной:
- SearchResult.signer — ФИО подписанта-субъекта («Иванов Иван Иванович»), берётся
из signer_certificate готового агрегата (подпись с signer===username, иначе первая;
организация → short_name). Тот же источник, что у чипов подписей в реестре.
- full_title в выдаче поиска теперь = meta.title (чистое наименование), как в реестре;
fallback — прежний full_title.
- Фронт DocumentSearchDialog: заголовок + чип ФИО (BaseBadge) + дата вместо строки
«username · дата»; убран пустой highlights-блок.
Схема + zeus-клиент перегенерированы (generate-schema/generate-client + sdk build).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Документы хранят дату в локализованном формате `DD.MM.YYYY HH:mm` + отдельное
meta.timezone (так пишет фабрика @coopenomics/factory через moment.tz). Прежний
`new Date(meta.created_at)` этот формат не парсил → Invalid Date → колонка
timestamptz падала с "invalid input syntax ... 0NaN-NaN-NaN", и throw в одной
записи ронял весь прогон backfill. Парсим тем же moment.tz, что и фабрика
(timezone из меты, иначе config.timezone), невалидное → null. Плюс per-record
try/catch в backfill: одна битая запись логируется и пропускается, а не валит прогон.
Проверено вживую на voskhod: backfill завершён scanned=8 created=4 updated=4
skipped=0, document_created_at = 2026-05-28 07:53:00+00 (валидные даты, не null).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TypeORM @Index(['...']) принимает имена СВОЙСТВ сущности, а не db-колонок.
Колонка объявлена @Column({ name: 'package' }) packageHash, поэтому индекс
['coopname','package'] падал на старте: "Index contains column that is missing
in the entity (SignedDocumentEntity): package". Индекс по db-колонке `package`
строится корректно и через имя свойства packageHash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ошибки цепи возвращают суммы с сырой precision=4 ("100.0000 RUB"). Добавлен formatAssetsInText() рядом с formatAsset2Digits и применён в FailAlert — единая точка для всех тостов ошибок. Суммы вида <целое>.<>=3 знаков> <ТИКЕР> приводятся к 2 знакам с группировкой; уже отформатированные не трогаются (идемпотентно).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
theme boot подписывал watch на глобальный Quasar-реактив Dark. В SSR boot-файлы
выполняются на каждый рендер-запрос, а watch без owning-scope не утилизируется —
подписчики копились на синглтоне Dark безгранично (та же природа, что у sentry.ts,
только дешевле за итерацию). Тема (data-theme на documentElement) нужна только на
клиенте; гейтим boot через process.env.SERVER.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Boot-файл sentry.ts использует браузерный @sentry/vue с browserTracingIntegration.
В SSR boot-файлы выполняются на каждый рендер-запрос, поэтому Sentry.init плодил
новый клиент/интеграции на каждом запросе (утечка памяти → «пила» RAM, заметная
на нодах с малым объёмом RAM) и спамил серверный лог строкой про инициализацию
(~80/мин). Гейтим boot через process.env.SERVER (как в chatwoot.ts) — Sentry
остаётся только на клиенте, где ему и место.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Глобальный ::selection красит текст в var(--p-ink) (тёмный на свет-теме)
поверх полупрозрачного фона — на тёмной поверхности тоста выделенный текст
пропадал. Scoped-override для .q-notification/.toast: полупрозрачная светлая
подложка + светлый текст, читаемо на всех типах тостов.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Баг A: предупреждение "Failed to locate Teleport target '#header-actions-host'"
возникало, потому что host был под v-if='loggedIn'. Сразу после регистрации
навигация на стол происходит раньше, чем loggedIn (зависит от async
session.isAuth / isRegistrationComplete) станет true → цель Teleport
отсутствует в DOM → даже defer-Teleport источников страниц её не находит.
Рендерим #header-actions-host безусловно — видимость блока действий уже
регулируется CSS (.topbar__actions:has(> .header-actions-host:only-child:empty)).
Баг B: после регистрации grant-gated кнопки («Совершить взнос», «Получить
возврат») не появлялись до F5. В потоке завершения регистрации (watch на
participant_account) не вызывался desktops.loadDesktop() (свежие столы/гранты),
а кошелёк/agreements не грузились: на момент первого run() статус ещё не
'active' → isFullyActive=false. После того как статус обновился, до-вызываем
run(true) и loadDesktop() по образцу init-app / EnableButton. Условие
isFullyActive в init-wallet не ослаблялось.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Только неочевидное поле — наименование банка получило подсказку ПАО "Сбербанк".
Остальные поля ИП самоописательны (формат даты, кол-во цифр, «как в паспорте»).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Поля краткого/полного наименования, должности представителя и наименования
банка получили placeholder-подсказки (ПК "Ромашка", Председатель совета,
ПАО "Сбербанк") — показываются при фокусе пустого поля.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hover-правило .base-radio-card:hover (специфичность 0,2,0) перекрывало
.base-radio-card--selected (0,1,0), поэтому при наведении на выбранную
карточку программы primary-фон/граница подменялись на surface/soft и
выбор визуально пропадал. Ограничил hover невыбранными карточками через
:not(.base-radio-card--selected).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Заголовок workspace-row получал outline только когда стол активный
(.is-active .row.is-selected). На неактивных столах при навигации курсор
«терялся»: только лёгкая смена фона на surface-2, без рамки — в отличие
от page'ей, которые подсвечиваются outline'ом всегда. Выровнял правило:
outline на любом .workspace-row.is-selected.
На верхней/нижней позиции стрелка дальше прыгала в противоположный
конец списка из-за (idx+delta+len)%len. Заменено на clamp в [0..len-1]:
курсор остаётся на месте, как в нативном listbox/меню. Пустое выделение
+ ↓ ставит на первый, ↑ — на последний.
Активный workspace был position:sticky top:0 — при навигации стрелкой
вверх курсор-выделение «исчезал» за sticky-overlay'ем, потому что
scrollIntoView({block:'nearest'}) считал элемент видимым в normal flow,
не учитывая sticky-перекрытие. Активный стол и так идёт первым по
сортировке + есть badge «Активный», sticky не нужен.
- CommandPalette.vue: все --p-accent / --p-accent-soft / --p-ink-on-accent
заменены на --p-primary (бирюзовая палитра канона). Откатывается
регрессия после fix-коммитов design-wave1 (sticky-banner / clamp /
рамка курсора), которые случайно вернули accent поверх primary
(см. b82b778b81 — изначально palette был в primary).
- default.vue::paletteWorkspaces: возвращена фильтрация meta.conditions
(participant/chairman/soviet install.ts) + meta.hidden (capital
install.ts) — та же логика что в LeftDrawerMenu.filteredRoutes,
чтобы CommandPalette не показывал страницы, которые не отображаются
в rail-меню. onSelectWorkspace/onSelectPage закрывают левый дровер
на мобильниках через desktop.closeLeftDrawerOnMobile().
- layouts/default.vue: вместо widgets/Desktop/CmdkMenu mount'им
shared/ui/domain/CommandPalette (canon из design-wave1) с адаптером
workspaceMenus → CommandPaletteWorkspace[] и глобальным ⌘K / Ctrl+K.
- entities/CommandPalette: минимальный store (isOpen / open / close /
toggle) — заменяет тяжёлый useCmdkMenuStore.
- LeftDrawerMenu.onCmdk → palette.open() (триггер «Найти» в AppDrawer).
- Удалены целиком осиротевшие после design-wave1 директории:
entities/CmdkMenu, widgets/Desktop/CmdkMenu, widgets/Desktop/CmdkTrigger,
widgets/Desktop/SecondLevelMenuList (никем не использовался),
widgets/Desktop/WorkspaceMenu (заменён на WorkspaceSwitcher),
widgets/Wallet/MicroWallet (заменён на RailUserCard).
- Поправлены barrels widgets/Desktop и widgets/Wallet.
Фон и текст тоста (.q-notification + .toast) теперь фиксированные тёмная
поверхность + светлый текст в обеих темах, а не инвертируемые
var(--p-ink)/var(--p-canvas). На dark-теме текст уходил в тёмный на тёмном
deep-tint фоне (#052e16 и т.п.) — нечитаемо. Цветные иконки по типу
остаются на токенах.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
q-pa-lg на flat-карточке SignAgreementDialog — документ больше не прижат
к границе на странице кошелька пайщика.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Глобальный Omit<IPayment,'id'>&{id:string} ломал store.ts/mergePayments
и features/Payment/SetStatus/api (туда приходит сырой Zeus-вывод с
id:unknown). Откатываю override в entities/Payment/model/types.ts,
сужаю id до string локально в ListOfPaymentsWidget через IPaymentRow —
точка сужения только в UI, контракт стора/API не трогается.
vue-tsc desktop локально: PASS.
QuestionsTable принимал Cooperative.Document.IComplexAgenda[] (cooptypes,
table:IDecision без certificates, id:IUint64=number|string), а сверху
ListOfAgendaQuestions передаёт IAgenda[] из desktop/entities/Agenda
(GraphQL-вывод getAgenda, table:BlockchainDecision с certificates,
id:number). Перевожу QuestionsTable на IAgenda — это реальная форма
данных из стора; уходит расхождение IComplexAgenda↔IAgenda для
:agenda='row' в QuestionCard (он тоже IAgenda).
IPayment.id был ModelTypes['ID']=unknown (Zeus не имеет scalar-resolver
для ID), из-за чего шаблон ListOfPaymentsWidget падал на expanded.get/.set
и :id-биндингах. Переопределяю id как string через Omit+&.
Ошибки цепи возвращают суммы с сырой precision=4 ("100.0000 RUB"). Добавлен formatAssetsInText() рядом с formatAsset2Digits и применён в FailAlert — единая точка для всех тостов ошибок. Суммы вида <целое>.<>=3 знаков> <ТИКЕР> приводятся к 2 знакам с группировкой; уже отформатированные не трогаются (идемпотентно).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
theme boot подписывал watch на глобальный Quasar-реактив Dark. В SSR boot-файлы
выполняются на каждый рендер-запрос, а watch без owning-scope не утилизируется —
подписчики копились на синглтоне Dark безгранично (та же природа, что у sentry.ts,
только дешевле за итерацию). Тема (data-theme на documentElement) нужна только на
клиенте; гейтим boot через process.env.SERVER.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Boot-файл sentry.ts использует браузерный @sentry/vue с browserTracingIntegration.
В SSR boot-файлы выполняются на каждый рендер-запрос, поэтому Sentry.init плодил
новый клиент/интеграции на каждом запросе (утечка памяти → «пила» RAM, заметная
на нодах с малым объёмом RAM) и спамил серверный лог строкой про инициализацию
(~80/мин). Гейтим boot через process.env.SERVER (как в chatwoot.ts) — Sentry
остаётся только на клиенте, где ему и место.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Глобальный ::selection красит текст в var(--p-ink) (тёмный на свет-теме)
поверх полупрозрачного фона — на тёмной поверхности тоста выделенный текст
пропадал. Scoped-override для .q-notification/.toast: полупрозрачная светлая
подложка + светлый текст, читаемо на всех типах тостов.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Устаревшая формулировка в docstring backfill-сервиса: после перехода на ключ
doc_hash идемпотентность/уникальность реестра — по (coopname, doc_hash), а НЕ по
(coopname, package). package сознательно не уникален (один процесс = несколько
разных документов с разными подписантами), поэтому ключом записи он быть не может.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ось упорядочивания версий подписи одного doc_hash должна мерить одну величину
на обеих сторонах guard'ов ingestAction. Раньше block_num хранился через
resolveBlockNum(meta.block_num, action.block_num) с приоритетом meta.block_num
(блок ГЕНЕРАЦИИ документа фабрикой), а incomingBlock в guard'ах — action.block_num
(блок ДЕЙСТВИЯ в цепи). Так как документ всегда генерируется до подачи в цепь
(meta.block_num <= action.block_num), защита от отката на старую версию подписи
(incomingBlock < existing.blockNum) и идемпотентность по блоку фактически не
срабатывали. Теперь приоритет — action.block_num, meta.block_num лишь fallback.
Заодно выправлены устаревшие комментарии интерфейса репозитория (ключ — doc_hash,
а не hash).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Два уточнения по замечаниям:
1. Ключ реестра переведён с hash на doc_hash. doc_hash = идентичность документа; hash включает
наложенные подписи, поэтому у ОДНОГО документа бывает несколько hash (версии по мере накопления
подписей — напр. акт приёма-передачи: 1-я подпись → один hash, 2-я → другой, doc_hash тот же).
При ключе по hash документ задваивался в списке (одна подпись / две подписи). Теперь:
- уникальный индекс (coopname, doc_hash); hash и package — неуникальные индексы;
- ingestAction ключуется по data.document.doc_hash и держит КРАЙНЮЮ версию: guard'ы «не понижать
статус» и «не откатывать на меньший block_num» (getState отдаёт status+blockNum); крайняя версия
перетирает предыдущую (тот же документ, больше подписей);
- hash хранится как версия последней подписи (для фильтра Union по document.hash и отображения).
package по-прежнему группирует разные документы процесса (обмен = разные doc_hash = разные строки).
2. searchDocuments вернул доступ пайщику, но со скоупом: член совета (chairman/member) ищет по всему
кооперативу, обычный пайщик — ТОЛЬКО по своим документам (search scope username=user.username).
Снял role-гард, добавил @CurrentUser; SearchResolver решает скоуп по роли. Заодно закрывает прежнюю
утечку (старый поиск был coop-wide для любого авторизованного).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Самоаудит по замечанию: package — это идентификатор ПРОЦЕССА, в который может входить несколько
заявлений с разными подписантами. Доказано в контракте: marketplace/change.cpp шлёт ДВА newsubmitted
с одним package, но разными document и username (обмен: contribute+return). Прежний уникальный индекс
(coopname, package) + upsert-по-package затирал второе заявление первым → ПОТЕРЯ ДОКУМЕНТА и склейка
username. Старый explorer-путь (список по newsubmitted) показывал оба.
- Сущность: уникальный индекс теперь (coopname, hash) — одна строка = один документ-заявление;
(coopname, package) стал НЕуникальным группирующим индексом.
- ingestAction ключуется по data.document.hash; смена статуса (newsubmitted/newresolved/newdeclined,
все несут то же заявление — подтверждено createagenda/declinedoc/make_complete_document) матчит
строку по hash.
- newdecision/newact/newlink несут package (не заявление) → пересобирают агрегаты ВСЕХ заявлений
пакета (repository.findByPackage), сохраняя статус каждого.
- Репозиторий: upsert/getStatus по hash; findByPackage вместо getSourceActionData; убраны
неиспользуемые setStatus/exists.
Также: searchDocuments закрыт гардом RolesGuard + @AuthRoles(['chairman','member']) — поиск по
документообороту кооператива только для совета (SearchDocumentsInput не содержит username, поэтому
правило «свои ресурсы» не открывает доступ обычному пайщику). Поиск пайщиком по своим документам
пока не предусмотрен.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Самоаудит видимости документов выявил две регрессии относительно прежнего explorer-пути:
1. Вкладка «Все входящие» (type=newsubmitted) теряла решённые документы. Прежний фильтр был по
ИМЕНИ действия: у каждого пакета есть newsubmitted-действие → «Все входящие» = все документы
(решённые показывались и тут, и во «Только утверждённые»). Партиция по статусу (newsubmitted→
status=Submitted) скрывала решённые из дефолтной вкладки User/DocumentsPage. Фикс: newsubmitted →
без фильтра статуса (все), newresolved → status=Resolved.
2. Backfill сканировал explorer с query={} — без скоупа. Explorer (cooparser) хранит трейсы по
получателям; без receiver вернулись бы все трейсы каждого действия (soviet/пайщик) и, возможно,
чужие кооперативы. Фикс: query={receiver: coopname} — ровно как прежний chairman-путь getDocuments
(один трейс на документ, только наш кооператив).
Утечки не было и нет: RolesGuard разрешает обычному пайщику только data.username==его_имя, а фильтр
findAggregates ограничивает username; coopname-скоуп (весь кооператив) доступен лишь совету/председателю.
findAggregates всегда фильтрует по config.coopname.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Завершение перехода одним PR: searchDocuments и getDocuments уже читают PG-реестр подписанных
документов, OpenSearch стал мёртвым кодом — убираем его полностью.
- Удалены infrastructure/search/* (opensearch.service, search-registry.service,
search-infrastructure.module) и application/search/services/search-event.service
(старый индексатор по newsubmitted — заменён SignedDocumentIngestionService + backfill).
- app.module: снят SearchInfrastructureModule. search.module: только SearchResolver (читает
SIGNED_DOCUMENT_REPOSITORY из глобального TypeOrmModule).
- package.json: убрана зависимость @opensearch-project/opensearch; pnpm-lock.yaml пересчитан.
- docker-compose: удалён сервис opensearch + volume opensearch_data.
- .env-example: убраны OPENSEARCH_* (поиск больше не конфигурируется через env).
- system.interactor: features.search = true (поиск на PG доступен всегда, не зависит от OPENSEARCH_ENABLED).
Файлы удалены только из текущего состояния (git rm) — в истории остаются.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Старый getDocuments через DocumentService подмешивал в explorer-фильтр `receiver: username`
(on-chain require_recipient): receiver=coopname отдавал все документы кооператива, receiver=<пайщик>
— его документы; Union-страница добавляла filter.document.hash. PG read-path эти ключи игнорировал
(искал несуществующий data.username) — личная страница пайщика показала бы все документы кооператива.
- interactor: extractReceiverScope (receiver===coopname → весь кооператив; иначе фильтр username=receiver,
т.к. колонка username = пайщик-субъект заявления) + extractHashFilter (filter.document.hash).
- repository: новый параметр hash в SignedDocumentListParams; фильтр UPPER(d.hash)=UPPER(:hash)
(фронт шлёт хэш в upper-case).
Набор документов getDocuments теперь совпадает со старым explorer-путём для всех трёх потребителей
(Cooperative/ListOfDocuments, User/DocumentsPage, Union/ListOfCooperatives).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Поиск по документам — в основном по фамилии подписанта, а не username. content_text (html заявления)
этого не покрывает: соподписанты (председатель/совет) подписывают решение/акты, а не тело заявления,
плюс зависимость от текста шаблона. ФИО уже присутствуют в агрегате — в signer_certificate каждой подписи
(DocumentAggregator резолвит их из учётных данных подписанта).
- Новая колонка signed_documents.signers_text — ФИО/наименования всех подписантов пакета
(last/first/middle_name физлиц-ИП и short_name организаций из signer_certificate всех частей
агрегата: заявление/решение/акты/связанные) + их username.
- Заполняется на ingestion и backfill из готового агрегата (collectSignersText).
- searchDocuments ILIKE теперь покрывает signers_text (приоритетно), full_title, content_text, username.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Фундамент C28-21: PG-проекция подписанных документов (таблица signed_documents),
наполняемая ловлей blockchain-событий soviet с внутренней шины и разовым backfill'ом.
Аддитивно — OpenSearch и резолверы (searchDocuments/getDocuments) пока не трогаются.
- SignedDocumentEntity (signed_documents): package/hash/coopname/username/status,
full_title/content_text/html/pdf(bytea), document_aggregate(jsonb), meta, block_num.
- SignedDocumentRepository (+SIGNED_DOCUMENT_REPOSITORY) + TypeORM-реализация (upsert/setStatus/search ILIKE).
- SignedDocumentIngestionService: @OnEvent(action::soviet::{newsubmitted,newresolved,newdeclined})
-> идемпотентный upsert с защитой от понижения статуса; контент из factory-Mongo.
- SignedDocumentBackfillService: разовый идемпотентный скан действий (env-gate SIGNED_DOCS_BACKFILL_ON_BOOT).
- Регистрация в TypeOrmModule + app.module.
newdeclined пока не экспортирован как registry-action в cooptypes — подписка по литералу на будущее.
Следующая итерация: перевод searchDocuments/getDocuments на PG + снос OpenSearch + тесты.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Баг A: предупреждение "Failed to locate Teleport target '#header-actions-host'"
возникало, потому что host был под v-if='loggedIn'. Сразу после регистрации
навигация на стол происходит раньше, чем loggedIn (зависит от async
session.isAuth / isRegistrationComplete) станет true → цель Teleport
отсутствует в DOM → даже defer-Teleport источников страниц её не находит.
Рендерим #header-actions-host безусловно — видимость блока действий уже
регулируется CSS (.topbar__actions:has(> .header-actions-host:only-child:empty)).
Баг B: после регистрации grant-gated кнопки («Совершить взнос», «Получить
возврат») не появлялись до F5. В потоке завершения регистрации (watch на
participant_account) не вызывался desktops.loadDesktop() (свежие столы/гранты),
а кошелёк/agreements не грузились: на момент первого run() статус ещё не
'active' → isFullyActive=false. После того как статус обновился, до-вызываем
run(true) и loadDesktop() по образцу init-app / EnableButton. Условие
isFullyActive в init-wallet не ослаблялось.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Только неочевидное поле — наименование банка получило подсказку ПАО "Сбербанк".
Остальные поля ИП самоописательны (формат даты, кол-во цифр, «как в паспорте»).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Поля краткого/полного наименования, должности представителя и наименования
банка получили placeholder-подсказки (ПК "Ромашка", Председатель совета,
ПАО "Сбербанк") — показываются при фокусе пустого поля.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hover-правило .base-radio-card:hover (специфичность 0,2,0) перекрывало
.base-radio-card--selected (0,1,0), поэтому при наведении на выбранную
карточку программы primary-фон/граница подменялись на surface/soft и
выбор визуально пропадал. Ограничил hover невыбранными карточками через
:not(.base-radio-card--selected).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Заголовок workspace-row получал outline только когда стол активный
(.is-active .row.is-selected). На неактивных столах при навигации курсор
«терялся»: только лёгкая смена фона на surface-2, без рамки — в отличие
от page'ей, которые подсвечиваются outline'ом всегда. Выровнял правило:
outline на любом .workspace-row.is-selected.
На верхней/нижней позиции стрелка дальше прыгала в противоположный
конец списка из-за (idx+delta+len)%len. Заменено на clamp в [0..len-1]:
курсор остаётся на месте, как в нативном listbox/меню. Пустое выделение
+ ↓ ставит на первый, ↑ — на последний.
Активный workspace был position:sticky top:0 — при навигации стрелкой
вверх курсор-выделение «исчезал» за sticky-overlay'ем, потому что
scrollIntoView({block:'nearest'}) считал элемент видимым в normal flow,
не учитывая sticky-перекрытие. Активный стол и так идёт первым по
сортировке + есть badge «Активный», sticky не нужен.
- CommandPalette.vue: все --p-accent / --p-accent-soft / --p-ink-on-accent
заменены на --p-primary (бирюзовая палитра канона). Откатывается
регрессия после fix-коммитов design-wave1 (sticky-banner / clamp /
рамка курсора), которые случайно вернули accent поверх primary
(см. b82b778b81 — изначально palette был в primary).
- default.vue::paletteWorkspaces: возвращена фильтрация meta.conditions
(participant/chairman/soviet install.ts) + meta.hidden (capital
install.ts) — та же логика что в LeftDrawerMenu.filteredRoutes,
чтобы CommandPalette не показывал страницы, которые не отображаются
в rail-меню. onSelectWorkspace/onSelectPage закрывают левый дровер
на мобильниках через desktop.closeLeftDrawerOnMobile().
- layouts/default.vue: вместо widgets/Desktop/CmdkMenu mount'им
shared/ui/domain/CommandPalette (canon из design-wave1) с адаптером
workspaceMenus → CommandPaletteWorkspace[] и глобальным ⌘K / Ctrl+K.
- entities/CommandPalette: минимальный store (isOpen / open / close /
toggle) — заменяет тяжёлый useCmdkMenuStore.
- LeftDrawerMenu.onCmdk → palette.open() (триггер «Найти» в AppDrawer).
- Удалены целиком осиротевшие после design-wave1 директории:
entities/CmdkMenu, widgets/Desktop/CmdkMenu, widgets/Desktop/CmdkTrigger,
widgets/Desktop/SecondLevelMenuList (никем не использовался),
widgets/Desktop/WorkspaceMenu (заменён на WorkspaceSwitcher),
widgets/Wallet/MicroWallet (заменён на RailUserCard).
- Поправлены barrels widgets/Desktop и widgets/Wallet.
Фон и текст тоста (.q-notification + .toast) теперь фиксированные тёмная
поверхность + светлый текст в обеих темах, а не инвертируемые
var(--p-ink)/var(--p-canvas). На dark-теме текст уходил в тёмный на тёмном
deep-tint фоне (#052e16 и т.п.) — нечитаемо. Цветные иконки по типу
остаются на токенах.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
q-pa-lg на flat-карточке SignAgreementDialog — документ больше не прижат
к границе на странице кошелька пайщика.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Глобальный Omit<IPayment,'id'>&{id:string} ломал store.ts/mergePayments
и features/Payment/SetStatus/api (туда приходит сырой Zeus-вывод с
id:unknown). Откатываю override в entities/Payment/model/types.ts,
сужаю id до string локально в ListOfPaymentsWidget через IPaymentRow —
точка сужения только в UI, контракт стора/API не трогается.
vue-tsc desktop локально: PASS.
QuestionsTable принимал Cooperative.Document.IComplexAgenda[] (cooptypes,
table:IDecision без certificates, id:IUint64=number|string), а сверху
ListOfAgendaQuestions передаёт IAgenda[] из desktop/entities/Agenda
(GraphQL-вывод getAgenda, table:BlockchainDecision с certificates,
id:number). Перевожу QuestionsTable на IAgenda — это реальная форма
данных из стора; уходит расхождение IComplexAgenda↔IAgenda для
:agenda='row' в QuestionCard (он тоже IAgenda).
IPayment.id был ModelTypes['ID']=unknown (Zeus не имеет scalar-resolver
для ID), из-за чего шаблон ListOfPaymentsWidget падал на expanded.get/.set
и :id-биндингах. Переопределяю id как string через Omit+&.
Снижает минимальный порог состава совета кооператива на этапе
Install Coop с 5 до 3 человек. Единственный ограничитель —
контракт soviet::createboard (см. consts.hpp + createboard.cpp:58).
Backend (install.interactor) и desktop (SetSovietForm) собственных
числовых проверок не имеют — фронт требует только «хотя бы одного»,
бэкэнд передаёт массив как есть в blockchainPort.createBoard.
Что сделано:
- Перенёс canon-стили card-label/card-value/section-title локально в scoped
блоки каждого реального потребителя (26 файлов).
- 7 файлов которые казались USE-ONLY оказались false-positive substring
совпадений (.resource-info-card / .meet-info-card / .membership-info-card /
.tariff-card-container / .cmdk-empty-state / .import-wizard__section-title /
.result-section-title) — глобальные классы info-card/card-container/
empty-state/section-title для них уже не нужны.
- Все 8 живых классов из legacy-stylers.scss (.card-label/.card-value/
.section-title/.card-container/.info-card/.empty-state/.section-header/
.title-container) теперь имеют ноль use-only потребителей: каждый файл
имеет либо local scoped def, либо использует другой класс с похожим именем.
- Удалил `src/css/legacy-stylers.scss` целиком.
- Убрал `'legacy-stylers.scss'` из css array в `quasar.config.cjs`.
Метрики:
- 26 файлов получили scoped-блоки (~6-10 строк canon-токенов в каждом).
- 1 файл удалён (legacy-stylers.scss, 134 строки).
- 1 импорт убран (quasar.config.cjs).
Sanity:
- Все 26 scoped scss-блоков прокомпилированы через sass.compileString — OK.
- false-positives откатил через git checkout HEAD.
Файл legacy-stylers.scss больше не существует.
Долг wave1/2/3 закрыт.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Что сделано:
- Снёс 8 классов с 0 потребителей: info-card-hover, page-main-card,
card-title, card-action-btn, balance-card{,-primary,-warning},
info-warning-card, section-card-warning (со всеми вложенными
.balance-label/.balance-value/.warning-info/.section-content/...).
- Снёс глобальный .profile-section — единственный потребитель
(MicroWallet) определяет его локально в <style scoped>.
- Перевёл живые классы на canon-токены var(--p-*):
• title-container — layout-only, токенов не требует.
• card-container — radius/border/bg через --p-r-lg/--p-line-1/--p-surface.
• info-card — surface-2/line-1/r-md/p-4/p-3.
• section-header + section-title — h2 (--p-fs-h2), color --p-ink,
section-icon margin --p-3, header margin-bottom --p-6.
• card-label/card-value — --p-fs-body/--p-ink-2 vs 16/500/--p-ink.
• empty-state — p-8/p-5/p-4/p-2/h2/body/ink-1/ink-2.
- Убрал ручные dark-overrides (.body--dark &, .q-dark &) —
canon-токены сами переключаются через [data-theme="dark"] в tokens.css.
- Media @max-width:768px ужал до одного правила (мобильный h3 для секций).
Метрика: 433 → 56 строк (-377/+56), CSS-выхлоп 2.1 КБ.
Не тронуто:
- Все 22-23 потребителя card-value/card-label — большинство определяет
стили локально (LOCAL), 5 USE-ONLY компонентов наследуют глобальный.
- 13 потребителей section-title — 6 LOCAL, 7 USE-ONLY.
Долг: переписать USE-ONLY компоненты на canon-виджеты (ColorCard / BaseCard),
после чего глобальные card-* и section-* можно полностью снести
вместе с этим файлом.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ResourceInfoWidget в extensions/powerup импортирует ModalBase через
barrel: import { ModalBase } from 'src/shared/ui'. После сноса
re-export'а vite валился SyntaxError: does not provide an export.
Возвращаю строку обратно — папка ModalBase уже восстановлена
предыдущим коммитом.
В прошлой волне снёс shared/ui/{ModalBase,TitleStyles,CardStyles,AutoAvatar},
но смотрел только src/. В extensions/{capital,chairman}/ осталось:
* ModalBase — 17 файлов
* TitleStyles (side-effect SCSS) — 6 страниц capital
* CardStyles (side-effect SCSS) — 1 виджет capital
* AutoAvatar — 1 страница capital
Из-за этого vite валился на 404 TitleStyles → "Failed to fetch dynamically
imported module src/boot/init.ts" и фронт не грузился.
Восстановлены оригинальные файлы. AutoAvatar в shared/ui/AutoAvatar
оставлен как re-export нового канон-расположения shared/ui/domain/AutoAvatar.
Полная миграция extensions на BaseDialog и канон-токены — отдельная волна.
При bootstrap'е фронта RequireAgreements/SignUp успевают смонтироваться
ДО завершения system.loadSystemInfo(), и info.coopname=undefined.
Zeus сериализует variables в {} → сервер кидает
"Variable $coopname of required type String! was not provided".
Решение по слою:
- api/index.ts: гард — если coopname пустой, не отправляем запрос.
- RequireAgreements/SignUp: watch(() => info.coopname) — load
отрабатывает, как только coopname прорастёт, без блокировки UI.
В BaseDialog добавлен prop :maximized (boolean) — q-dialog получает maximized, q-card растягивается на весь экран, body становится flex-scrollable. При :maximized размер size игнорируется.
Это покрывает 5 last maximized call-sites:
* Agreementer.SignAgreementDialog — fullscreen для подписи договора (hideCloseButton)
* Agreementer.ReadAgreementDialog — fullscreen для просмотра договора
* Agreementer.StaticPrivacyDialog — fullscreen для статичной privacy policy
* Branch.SelectBranch.SelectBranchOverlay — fullscreen onboarding flow с шагами 1/2
* Decision.CreateProject.CreateProjectButton — fullscreen для предложения повестки
Все 5 переведены на BaseDialog :maximized='true' :close-on-backdrop='false' :close-on-escape='false'.
После миграции ModalBase больше не используется — удалена папка shared/ui/ModalBase и реэкспорт из shared/ui/index.ts. Design-wave1 закрыт полностью.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Простые модальные диалоги переведены с q-dialog+ModalBase на canon BaseDialog (внутри уже q-dialog), API v-model:modelValue:
* features: DeleteBranch / CreateBranch / DepositToWallet (двух-стейтовый dialog) / WithdrawFromWallet / AddPaymentMethod / DeletePaymentMethod / Decision.CreateProjectFreeDecision (lg) / FreeDecision.CreateProject (md) / Payment.SetStatus.SetOrderPaid/Completed/Refunded
* widgets: ConnectionDashboard.AxonWallet / Desktop.WorkspaceMenu (size=lg вместо 700px-фикса)
* shared: CreateDialog (общий wrapper) / CouncilOnboarding.CouncilOnboardingCard (кнопки в slot footer)
Persistent → :close-on-backdrop='false' + :close-on-escape='false'. @hide='clear' → @update:model-value с проверкой v===false. style='width: NNNpx' переведён на canon size sm/md/lg.
Что НЕ мигрировалось (5 файлов остались с ModalBase):
SelectBranchOverlay, SignAgreementDialog, ReadAgreementDialog, StaticPrivacyDialog, Decision.CreateProject.CreateProjectButton — все используют :maximized='true'. BaseDialog в canon не имеет fullscreen-режима, расширять canon без согласования — нарушение правила "Канон-нейминг согласовывать ДО реализации". Эти 5 кейсов оставлены legacy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Form.vue переписан как тонкий wrapper над canon BaseForm + BaseButton с сохранением старого API (handlerSubmit/isSubmitting/showCancel/showSubmit/buttonSubmitTxt/buttonCancelTxt/disabled/size).
Что изменилось визуально:
* Кнопки cancel/submit вынесены в slot #footer (canon-layout с gap=14px между body и footer).
* Cancel — variant='ghost' (плоская), Submit — variant='primary' (canon-primary), оба BaseButton с no-caps/без ripple.
* Кнопки выровнены justify-content:flex-end (canon-pattern для форм), вместо старого .flex без выравнивания.
API call-sites не трогаем — 20 form-call-sites (Branch/Wallet/PaymentMethod/Agreementer/Decision/Union/FreeDecision/Payment/Request/AxonWallet) получают canon-визуал сразу.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Глобальный CardStyles из 430 строк CSS реально использовался только через .info-card в RestartMeetForm.vue.
* Все остальные классы (card-container, section-header, card-label, card-value) определены локально в scoped CSS соответствующих компонентов — глобальный импорт лишний.
* Стили .info-card (+ dark-override) заинлайнены в scoped стиль RestartMeetForm под нативным именем .meet-form-agenda-item.
* Удалены: глобальный импорт в App.vue, @import в RestartMeetForm, папка shared/ui/CardStyles.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* InputStyles — весь файл состоял из закомментированного кода.
* TitleStyles — .title-container не использовался ни в одном .vue.
* Удалены 3 импорта InputStyles в Editable*Card и реэкспорт TitleStyles из shared/ui/index.ts.
CardStyles оставлен: используется в 4 call-sites (RestartMeetForm, UnionMembershipStep, TariffCard, MeetInfoCard) — миграция отдельным шагом.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ThemeToggle получает asButton/showText/isMobile, поглощая роль ToogleDarkLight (исправлена опечатка в имени).
* AutoAvatar перенесён в shared/ui/domain/AutoAvatar — он DiceBear-процедурный, а не base-примитив (canon Avatar делает инициалы/image и не заменяет процедурную генерацию).
* Снесены legacy папки shared/ui/ToogleDarkLight и shared/ui/AutoAvatar.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
В CLAUDE.md новый раздел «ДИЗАЙН-КАНОН desktop — ОБЯЗАТЕЛЕН»:
1) HTML-канон ~/blago/production/shared/MONO Design System.html;
2) Живая реализация components/desktop/src/pages/_dev/ui/index.vue.
После компакта забыл, что shared/MONO Design System.html — основной SoT,
и нашёл «канон» в auth-prototype, который НЕ канон — пришлось переверстать
тосты. Фиксирую правило, чтобы не повторять.
Завершаю аудит — добавил capital по запросу:
- RouteMenuButton получил опциональный icon-проп; на мобильном
превращается в round-иконку + tooltip, на десктопе — flat + label.
- В ProjectPage и ComponentPage прописал material-иконки для каждого
раздела (Описание/Артефакты/Компоненты/План/Участники/История/
Задачи/Голосование/Результаты).
- ImportContributorButton, ImportContributorsButton, FilterDialogWithButton
переведены в canon-micro (isMobile → flat+dense+sm+accent + tooltip).
Заодно вынес q-dialog из q-btn-вложенности.
RouteMenuButton по сути — раздельные таб-кнопки навигации; полноценное
UX-решение для capital (вынести в SecondLevelTabs под шапкой) — отдельная
задача, тут только мобильная читаемость шапки.
CSS-хак `.topbar__actions .base-btn__label { display: none }` не покрывал
эти две кнопки: они рисуют q-btn напрямую через registerAction (не через
BaseButton под .topbar__actions). В итоге на мобильном «Добавить члена»
и «добавить участок» оставались с лейблами и распирали шапку.
Перевёл обе в canon-паттерн как CreateMeetButton/DepositButton/WithdrawButton:
isMobile → flat+dense+sm+accent, иконка + q-tooltip; без обёртки
.header-action, чтобы кнопка не растягивалась на всю высоту шапки.
- quasar-canon.css: переписан override .q-notification под реальный
канон (shared/MONO Design System.html → .toast): тёмный фон
(var(--p-ink) для нейтрального; #052e16/#4c0a0a/#3d2400/#0c1e3f
для positive/negative/warning/info), светлый текст, ЦВЕТНАЯ иконка
по типу (--p-pos/--p-neg/--p-warn/--p-info). Раньше ошибочно был
сделан нейтральный фон + left-accent-полоска — «откуда попало»,
не из канона.
- alerts.ts: убран color: 'grey-7'/'positive' у actions — они
наследуют светлый тон от тёмного фона через override.
- _dev/ui секция 39 «Тосты»: пять кнопок для триггера всех типов
тостов (успех, успех с CTA, ошибка, in-app push, push с avatar).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- SuccessAlert/FailAlert/NotifyAlert (shared/api/alerts.ts): единый
визуал через override .q-notification в quasar-canon.css. Нейтральная
поверхность + 3px accent-полоска по типу (positive/negative/warning
/info), без «цветной заливки карточки» (stop-signal §19.1).
- Позиция тостов bottom-right — было top-right, что противоречило
канону mono-design-system v2 (toast-host: fixed; bottom: 24px;
right: 24px). На мобильнике — full-width минус 16px полей.
- SuccessAlert: type:'positive' вместо color:'primary' — раньше успех
красился брендовым teal, а не позитивным зелёным.
- NotifyAlert: дефолтная иконка notifications, если нет avatar — чтобы
in-app push от Novu визуально совпадал с Success/Fail.
- Topbar @media (max-width:600px): .topbar__actions .base-btn__label
скрыт, остаются только иконки CTA-кнопок страницы. Бренд-название
с ellipsis (max-width: 50vw). Раньше кнопки переносились на вторую
строку на узких экранах.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI vue-tsc --noEmit падал на четырёх файлах:
- QuestionsTable.vue: props.decisions типизирован через
PropType<Cooperative.Document.IComplexAgenda[]>. Раньше тип был
плоский Array → row в v-for становился unknown, и template
ругался на row.table.id, isProcessing(row.table.id) и т.п.
- ListOfPaymentsWidget.vue: явный computed items: IPayment[] через
cast (as unknown as IPayment[]) — zeus резолвил items слабее, чем
нужно шаблону, и поля row.id / username / quantity / etc. оставались
unknown.
- ExtensionPage.vue и ExtensionCard.vue: для AutoAvatar :username
добавлен fallback || '' — extension.name / extension.title могут
быть string | null | undefined, AutoAvatar требует string.
ESLint по четырём файлам — EXIT=0; tsc-прогон делегирован CI
(точечный vue-tsc по факту тащит весь проект и вешает машину, см.
memory feedback_no_full_vue_tsc_desktop).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Без delay nodemon реагировал на каждое fs-событие пачки (правки/сборка/git на хосте) десятками рестартов в секунду; pstree.remy без `ps` в образе виснет на обходе /proc (накапливались зомби-сканеры), холодный старт ts-node прерывался, порт 2998 не занимался — бэкенд молча лежал. delay:2000 схлопывает пачку в один рестарт, signal:SIGTERM даёт чистое завершение дочернего процесса.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
extensionRepository.update заменяет config JSONB целиком; chairman/powerup/meet-tracker писали захваченный при initialize снимок поверх свежих данных. Теперь read-modify-write по актуальному config из БД с наложением только владомых полей.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Boot-файл haptics: при reload страницы на устройстве с Vibration API (мобильный
браузер / PWA на Android) коротко вибрирует (15 мс). Reload определяется через
Navigation Timing (с фолбэком на legacy API), чтобы не срабатывать на первой
загрузке/навигации. Полностью безопасно для десктопа и обычных сайтов: где
Vibration API нет — вызов не делается, любая ошибка проглатывается, на загрузку
приложения не влияет. Только клиент (гард для SSR).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
В левом дровере был маленький горизонтальный скроллбар, и трекпадом его можно
было «оттянуть», обнажая правую границу — дровер выглядел скроллируемым, хотя не
должен. Причина: у .rail собственный border-right (1px), который при content-box
даёт ~1px overflow внутри контента дровера. Фикс в .app-left-drawer: overflow-x
hidden + overscroll-behavior-x contain на .q-drawer__content, .rail приведён к
box-sizing border-box и width 100%.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Если проголосовать и сразу нажать «Утвердить», утверждение падает с ошибкой —
бэкенд ещё не учёл голос из блокчейна. После успешного голоса держим состояние
загрузки пункта (а значит и кнопку «Утвердить» в :loading) ещё VOTE_SETTLE_MS
(3с), за это время голос успевает обработаться. На ошибке загрузка снимается
сразу. Заодно ручное мутирование processingDecisions + setTimeout-хак заменены
аккуратным реактивным хелпером setProcessing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Повестка совета показывает все НЕутверждённые вопросы. Голос «за»/«против» не
должен убирать пункт — он остаётся неутверждённым, лишь помечается отметкой
голоса. Убрал ошибочное actedDecisionIds.add из onVoteFor/onVoteAgainst
(оставлен только тихий рефетч). Скрытие через actedDecisionIds оставлено только
в onAuthorizeDecision — там пункт исполняется и должен уйти из повестки без
возврата от отстающего поллинга.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Две проблемы при голосовании/утверждении в повестке совета:
1. Моргание всей страницы: QuestionsTable подменял список тремя
скелетонами при любом loading=true, а пост-экшен loadDecisions вызывался
без hidden → loading=true → список «моргал» в скелетоны и обратно.
Скелетоны теперь показываются только на первой загрузке (loading &&
!decisions.length), а пост-экшен рефетчи переведены в тихий режим (hidden).
2. «Исчез → вернулся → исчез»: повестка на бэкенде уже не отдаёт пункт, по
которому проголосовал/утвердил, но данные из блокчейна доходят с задержкой,
и поллинг успевал вернуть отработанный пункт. Добавлен локальный набор
actedDecisionIds — проголосованный/утверждённый пункт прячется сразу и не
возвращается, что бы ни подтянул отстающий рефетч. Просто, без оптимистичных
merge-оверлеев.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Две независимые проблемы при выдаче разрешения на уведомления:
1. Фриз приложения: shouldShowDialog был computed с побочным эффектом —
внутри геттера вызывался updateSupport(), присваивавший store.support
новый объект на каждое чтение. После выдачи разрешения это давало каскад
инвалидаций/ре-рендеров, забивавший главный поток (роутер менял URL, DOM
«застывал»). Геттер сделан чистым, updateSupport() вынесен в showDialog().
2. Красная ошибка «Service Worker не готов в течение 5 секунд»: в dev без PWA
SW намеренно не регистрируется, поэтому navigator.serviceWorker.ready
никогда не резолвится. Теперь сначала проверяем getRegistrations() — при
отсутствии SW выходим сразу и тихо (warn вместо error), а subscribe()
возвращает false без FailAlert. Push при недоступности SW полностью
изолирован и не влияет на работу приложения.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Кнопки взноса и возврата на столе пайщика недоступны, пока пайщик не
подписал главное соглашение цифрового кошелька (type='wallet') — как и
сама карточка кошелька, которая не появляется без подписанного соглашения.
Геттер isWalletAgreementSigned в сторе Wallet читает тот же список
соглашений пайщика, что и RequireAgreements.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
«(опционально)» в заголовок + пояснение в описании: если есть пайщики —
импортируйте CSV, если нет — двигайтесь дальше. Чтобы шаг не воспринимался
обязательным.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
readonly outlined q-input рендерится с dashed-рамкой (дефолт Quasar) и
обрезал значение (…UJR4) — выпадало из канона и выглядело плоско.
Сделал ключ surface-2 панелью: eyebrow-лейбл + copy-иконка в шапке,
mono-значение целиком (word-break) ниже. Даёт вес и убирает «ёлочку».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Прошлый вариант разъезжался: «Скопировать» висел сиротой справа, провалы
между блоками, узкая потерянная кнопка, карточка 560px — разрежено.
Переделал по канону:
- копирование ключа — иконкой content_copy в append самого поля (+tooltip)
- кнопка «Установить ключ» — full-width (block), как «Войти» в LoginForm
- ширина карточки 480 как у остальных auth-карточек
- чекбокс слева, плотный ритм без лишних margin (reserve-hint-space)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Голый «Потребительский Кооператив» в примере провоцировал вводить просто
ОПФ без расширения. Дал полный пример «...Социального Комплекса» во всех
трёх падежах — чтобы вводили расширенную ОПФ+, а не базовую форму.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Это не переменные, а постоянные параметры, которыми настраивается фабрика
документов кооператива. Переименовал label и intro шага.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Микроотступ стал лишним и снова раздвигал поля слишком широко — откатываю.
Разделение полей обеспечивает сам reserve-hint-space, дополнительный
глобальный отступ не нужен.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Постоянный hint под полем занимал место и отвлекал. Перенёс примеры
наименований ОПФ+ в placeholder (видны только в пустом поле), убрал hint.
Добавил reserve-hint-space всем q-input формы — строка под ошибку
резервируется без текста, валидация не вызывает layout shift.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Без зазора текст hint/error верхнего поля почти касался границы нижнего.
Вернул маленький отступ, но системно и один раз — глобально в quasar-canon:
margin-bottom: var(--p-1) только полям .q-field--with-bottom (те, что
резервируют нижнюю строку). Работает везде (BaseForm, формы установки,
регистрация), в стеках не складывается в дыру, инлайновые поля не трогает.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
reserve-hint-space у q-input/BaseInput уже даёт ~24px снизу под error/hint;
дополнительный gap в стеках полей складывался с ним → избыточное расстояние.
Убрал gap (канон BaseForm__body):
- CreateOrganizationDataForm / IndividualDataForm (.user-data-stack)
- SetVariablesForm (.vars-section__fields)
- RequestKeyForm (.request-key)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Канон-плотность полей (dense) была только в BaseInput; общие формы
CreateOrganizationDataForm и IndividualDataForm + поля шагов установки
рендерили крупные не-dense q-input. Добавил dense:
- CreateOrganizationDataForm (22 поля) — данные организации
- IndividualDataForm (6 полей) — члены совета
- email-инпуты SetInitForm/SetSovietForm и поля SetVariablesForm
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- InstallCooperativePage: VerticalStepper вместо q-stepper, canon-панель
с accent-стрипом, экран завершения в каноне (без градиентов/shimmer)
- RequestKeyForm: BaseInput (mono) + BaseButton
- SetInitForm: canon info-нотки, кнопки Назад/Далее на BaseButton
- SetSovietForm: canon-карточки членов совета вместо q-card/q-badge
- SetVariablesForm: canon-секции, q-input outlined с правилами, BaseButton
- Invite: осмысленное тело AuthCard при отсутствии токена приглашения
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
.extension-page уже внутри .catalog-shell__content с padding var(--p-6);
собственный padding давал двойной зазор от шапки до кнопки «Назад».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Весь контент страницы расширения обёрнут в канон-панель (surface+border),
больше не лежит на голом фоне
- В режиме настроек добавлена кнопка «Отменить» (CancelButton) рядом с
«Сохранить» — явный выход без сохранения; кнопка «Назад» страницы тоже работает
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Кнопка «Назад» убрана из шапки (useBackButton снят), добавлена канон-кнопка
под шапкой на самой странице
- Логотип AutoAvatar центрирован в колонке
- DesktopsList: центрированный inset-блок (выделяется), а не плоский список
- ext-actions toggle: «включено» и «удалить» снова в одной строке
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Триггер ринг-анимации перенесён с hover самого SVG на hover карточки
(.app-card:hover :deep(.ring-seg)).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AutoAvatar получил режим animated: инлайн-SVG вместо <img>, каждый
сегмент-дуга обёрнут в .ring-seg. По hover логотипа сегменты на ~1.3с
расходятся — прямые и зеркальные (matrix-flip) от одного поворота идут
в разные стороны, со стаггером по индексу. CoopCard не затронут (img/blob).
Уважает prefers-reduced-motion.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Статус идёт сразу под заголовком (динамично) — без дыры под однострочными
названиями. Обрезку в 2 строки оставили, чтобы длинный заголовок не ломал сетку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Заголовок резервирует 2 строки (clamp+min-height), аватар по верху —
статус и описание на всех карточках начинаются на одной линии, не прыгают.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Верхний ряд: логотип + (заголовок, под ним статус); описание во всю
ширину ниже; футер «Подробнее» прижат к низу для выравнивания карточек.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Палитра колец → мягкие приглушённые тона + CSS saturate(.5)/opacity .85,
чтобы знак читался как тихая текстура, а не неоновое пятно.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- AutoAvatar параметризован: props size/radius/background/ringColor
с обратно-совместимыми дефолтами (CoopCard не затронут)
- ExtensionCard: буква-монограмма заменена на уникальный rings-логотип,
seed = имя расширения; цвет кольца детерминирован по seed из палитры
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- PaymentProviderForm: убраны вложенные q-gutter (кривой левый отступ
селекта), standout=bg-teal → outlined, text-grey-7 → токен; чистый
канон-столбец (селект max-width 480, hint ink-2, действия в ряд, no-caps).
- ChangeContacts: текст уточнён — контакты видны всем (раздел «Контакты
кооператива» пайщикам + подвал сайта незарегистрированным), не только
незарегистрированным.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Кооперативные участки, Регистрационные взносы, Ключ кооператива,
Провайдер платежей, Контакты кооператива:
- убран дублирующий заголовок страницы (есть крошка в шапке);
- описание перенесено с голого фона на канон-поверхность .banner;
- page-shell/hero/surface-card с хардкод-px → канон-отступы и токены;
- q-input/q-select standout=bg-teal → outlined color=primary.
Также удалён лишний хвост текста на Стартовых страницах.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Заголовок страницы уже показан крошкой в шапке — повтор h2 убран.
Пояснительный текст вынесен с голого фона на канон-поверхность (.banner
с инфо-иконкой). ApprovalsPage не трогаем — там заголовка страницы нет.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ApprovalsPage: канон-отступы страницы, фильтр-селект teal standout → outlined.
- SystemSettingsPage: hero-card/surface-card с хардкод-px → канон page-head
(h2 + sub на токенах) + q-card(flat); канон-отступы.
- MembersPage: тот же канон-паттерн вместо hero-card с хардкодами.
- DefaultPagesForm: 4× q-select standout=bg-teal → outlined color=primary.
Виджеты таблиц/состава совета без экзотики — канон-тема через quasar-canon.css.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Шаблон двухкорневой (кнопка + Teleport затемнения), class из родителя
(LeftDrawerMenu) не наследовался автоматически. inheritAttrs:false +
v-bind=$attrs на корневую кнопку — атрибуты направлены явно.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Подписи секций «Вопрос на повестке»/«Проект решения» переведены в
надстрочные метки (uppercase, трекинг, ink-3) — отдельный ярус ниже
заголовка окна, чтобы не казаться конкурирующим заголовком рядом с
крупным заголовком самого документа. Текст вопроса — читаемый body.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ConnectPage: добавлены канон-отступы страницы (var(--p-6)/var(--p-4)) —
класс .padding в проекте не определён, контент липнул к краям.
- OnboardingStepsCard: убран q-card (двойной паддинг), текст-интро
растянут на полную ширину (снят max-width 70ch), вокруг кнопки шага
убрана лишняя рамка (.step__content → .step__action, только отступ).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- OnboardingStepsCard: под заголовком «Адаптируйте кооператив…» добавлен
поясняющий текст (что происходит дальше: решения совета в электронной
форме, вступление в «Восход», импорт пайщиков, общее собрание) + плашка
срока адаптации с отсчётом.
- Чек-лист шагов переведён на канон-вертикальный степпер (.stepper--v):
пройденные — галочка/заливка primary, текущий — подсветка номера,
действия — канон BaseButton; agenda/import/meet типы сохранены.
- Диалог «Предложение повестки» (объявить собрание совета) переведён
с ModalBase на канон BaseDialog + BaseButton.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- CommonHeader: крошка строится из route.matched (путь внутри стола),
напр. «Отчётность › Календарь»; корень стола не выводится; плоские
страницы дают одну крошку как прежде; pageTitleOverride сохранён.
- CoopWalletsPage: переводы временно скрыты под флагом transferEnabled=false
(открывались с L3-кошельков пайщиков; вернём для кооп-кошельков позже).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- editor-container/action-panel/backdrop/validation-badge/mark-hint: hex и rgba → --p-* токены
- text-grey-8 → t-muted; box-shadow/overlay через токены с fallback
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- SecondLevelTabs на глобальных .tabbar/.tab — sub-навигация между топбаром и контентом
- WalletsPage/DocumentsPage shell-страницы: вкладки в .tabbar вместо RouteMenuButton в топбаре
- TransferWalletsButton: канон header-кнопка (q-btn primary)
- WalletTransferDialog: на BaseDialog + BaseButton, токены вместо rgba
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ZodForm: убран standout="bg-teal text-white" с динамического поля —
QInput/QSelect теперь канон (outlined+dense+color=primary+reserve-hint-space),
как BaseInput. SCSS на канон-токены (вместо неопределённого --q-primary-rgb).
ExtensionSettings: снят заголовок «Настройки» внутри карточки — он дублировал
«Настройки аренды» в шапке.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Канон-инпут вместо q-input: BaseInput расширен поддержкой type="date"
и clearable (+ emit clear) — это нужно для фильтра по датам. Фильтры
в ExtensionLogsList переведены на BaseInput.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ExtensionSettings: EmptyState + BaseCard вокруг ZodForm вместо самописных
header/empty-state. ExtensionLogsList (используется только powerup):
q-table → список канон-карточек с фильтром по датам, скелетоном,
«Загрузить ещё» и EmptyState; слот #log-item и API сохранены.
Канон-падинги страниц настроек/лога аренды.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Монитор ресурсов: баннер-предупреждение на всю ширину сверху, ряд
«Как это работает» (2/3) + кошелёк AXON (1/3), ряд CPU/NET/RAM.
Убран 3D-флип карт (он же «прыгал») — детали раскрываются inline.
Все виджеты/страницы/лог сведены к канон-токенам surface/line/ink.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Реестр платежей: кнопки действий больше не block (не заполняют ячейку
от левого края, не липнут к бейджу статуса). Стопка inline-flex,
равная ширина кнопок (align-items:stretch), прижата к правому краю
колонки (col-action 168px, text-align:right) — слева остаётся воздух.
- Реестр документов: col-date 116→156px — дата «26.05.2026 08:25» больше
не налезает на заголовок документа; min-width 720→780.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Bank.vue: панель платежа max-width 380px по центру — длинное «Назначение»
больше не раздувает диалог «Совершите взнос» до max-content; значение
переносится по строкам (overflow-wrap: anywhere).
- Кнопки статуса платежа SetOrderPaid/RefundedStatusButton → BaseButton
(primary/danger, block), подписи «Подтвердить»/«Отклонить»; диалог
вынесен из-под кнопки.
- Реестр платежей: кнопки действий друг под другом (.cell-actions),
колонка действий 156px, дата без переноса (.col-date 132px), min-width
таблицы уменьшен.
- Реестр документов: сужены колонки подписей (220→150), действий
(110→56), id/дата; min-width 880→720 — освобождено место под
наименование, таблица не вылазит за страницу.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Таблицы документов и платежей уже на канон-токенах и самообрамлены
(.table-wrap). Не хватало полей страницы как в реестре пайщиков:
- PaymentsPage рендерил виджет голым, впритык к краям → q-page + 24/16px.
- ListOfDocumentsPage: q-page.padding (16px) → канон-24px.
- Убрана лишняя .row.justify-center обёртка в ListOfDocumentsWidget
(.col-12 и так во всю ширину).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AddUserDialog перестроен в канон-визард по образцу CreateMeetForm:
шапка-bar (surface+line) вместо bg-gradient-dark, VerticalStepper из трёх
шагов — Электронная почта → Тип и данные → Вступительный взнос, BaseButton
в подвале, outlined-инпуты. UserDataForm переиспользован целиком (общий,
не тронут). Опция начисления взноса — канон-блок вместо q-item/q-card.
ParticipantsImportDialog: добавлено описание-интро (раньше отсутствовало),
ModalBase/bg-gradient-dark заменён на канон-bar, выбор типа аккаунтов через
BaseRadioCard, dropzone на токенах вместо хардкод-цветов, секции и действия
в канон-обёртках, BaseButton вместо цветных q-btn. Логика парсинга/импорта
и таблицы превью/результатов сохранены.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Страница получила канон-отступ (--p-6 / --p-4 на мобилке), таблица обёрнута
в обрамлённую surface-карточку (--p-line, --p-r-lg) вместо edge-to-edge.
Развёрнутые данные пайщика — спокойная вложенная панель на --p-surface-2
с отступами --p-5/--p-6, читаемой шириной формы (640px) и вертикальным
ритмом между полями. Снят full-height у таблицы (мешал внутри карточки).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Развёрнутая строка пайщика теперь показывает только его данные;
документы пайщика живут в отдельном «Реестре документов» и здесь не дублируются.
ParticipantDetails упрощён (без q-tabs/ListOfDocumentsWidget), tab-логика
вычищена из таблицы и страницы. Мобильная ParticipantCard переведена на канон-токены
(убраны хардкод-цвета, CardStyles и q-badge статуса → disabled-checkbox).
Таблица сохранена, сортировка по дате вступления (DESC) без изменений.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Убран EntityIdBadge. Зелёная плашка-аватар слева теперь несёт сам номер вопроса
(он же Decision ID): клик по плашке копирует id (тултип «Скопировать №»), hover —
инверсия в сплошной teal. Заголовок снова просто текст вопроса.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EntityIdBadge перенесён из отдельной строки под ФИО в начало строки заголовка:
[#id] → сразу текст вопроса. Inline, vertical-align middle. Раньше висел снизу
и пусто растягивал верхнюю строку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EntityIdBadge с id вопроса под заголовком/ФИО: отображает #<id>, по клику копирует
чистый id (copy-on-click). Канон-компонент, клик не раскрывает карточку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
«Истекает через месяц» перенесён из строки под заголовком в постоянную нижнюю
полоску (footer): срок слева, «Утвердить» справа (только председателю). У обычного
пайщика — только срок, узкая панелька.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Срок теперь подписан «Истекает через месяц» (было просто «через месяц» — неясно).
Удалён статус-чип «Вы за/против» — направление голоса уже видно по подсветке кнопок,
бейдж был избыточным. Убраны связанные computeds и стили.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Кнопки голосования прижаты к правому краю строки. «Утвердить» вынесена вниз
отдельной строкой-footer (справа, hairline сверху) — больше не теснит данные.
Срок «через месяц» и статус-чип «Вы за» перенесены влево под заголовок/ФИО.
Клик по строке раскрывает документ, по кнопкам голосования и footer — нет.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Карточка вопроса перестроена в единую горизонтальную строку: иконка + вопрос/ФИО
слева (flex), компактный блок органов управления (голосование + «Утвердить»),
срок + статус-чип и шеврон — справа, друг рядом с другом. Убрана центрированная
vote-зона с пустотой по бокам. Клик по строке раскрывает документ, по органам
управления (@click.stop) — нет. Подсказка «Утвердить» возвращена в tooltip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Шапка карточки (вопрос/ФИО/срок/статус) кликабельна целиком и раскрывает документ,
справа шеврон-индикатор. Органы управления голосованием — отдельная зона ниже шапки
(сиблинг, не аккордеон): клик по ним голосует/утверждает и документ не раскрывает.
Убрана отдельная кнопка «Документ» — раскрытие теперь по клику на шапку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
QuestionCard: VotingButtons + «Утвердить» вынесены на верхний уровень карточки —
голосовать можно без раскрытия; раскрытие («Документ») открывает только содержимое
документа. Истечение срока остаётся в шапке.
VotingButtons: вернул чек-индикатор «принято советом» (закрашивается при принятии)
вместо иконки verified.
Header-CTA совета (Предложить/Добавить/Импорт): убран push (q-btn--push не попадал
под канон-правило заливки primary → белый шрифт на нетиловом фоне), подписи с заглавной.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
QuestionsTable: q-table → карточный список (скелетоны + EmptyState).
QuestionCard: канон-поверхность, BaseButton «Утвердить», токен-чип статуса
вместо q-badge, локальное состояние раскрытия, без @import CardStyles и хардкод-hex.
VotingButtons: токены pos/neg вместо .text-red/.text-green/#666.
Страница: канон-padding вместо q-card flat.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Названия столов в реестре workspace'ов заведены вразнобой («Стол
благороста», «Стол вычислительных ресурсов» vs «Стол Совета»).
Добавил text-transform: capitalize на подписи в WorkspaceSwitcher
(заголовок текущего стола + пункты выпадающего меню) и WorkspaceMenu
(карусель + диалог выбора) — первая буква каждого слова заглавная,
одинаково для всех и для будущих столов, без правки строк-источников.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
По фидбеку переработана композиция:
- убраны заголовки секций «Контакты»/«Руководство» и per-строчные
иконки телефона/почты/адреса (из-за них значения «прыгали» вправо
относительно ИНН/ОГРН);
- ИНН и ОГРН теперь в одну строку (адаптивная сетка полей);
- председатель поднят наверх к реквизитам, поле названо просто
«Председатель совета»;
- контакты — те же поля, телефон/email как ссылки (tel/mailto), всё на
единой левой кромке; одна тонкая линия делит реквизиты и контакты.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Реквизиты/Руководство больше не разносят подпись и значение к
противоположным краям (на полной ширине это давало пустой провал и
оторванные значения). Все секции теперь как ContactSheet: подпись
сверху, значение под ней, слева, hairline между строками — один ритм
по всей карточке.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ContactsPage: три цветные ColorCard (orange/indigo/teal/blue) заменены
на единую спокойную canon-поверхность с секциями через hairline
(Реквизиты / Контакты / Руководство). Контакты — через canon
ContactSheet (копирование, mailto/tel). Заголовок и строки на токенах
(--p-ink/-2/-3, --p-surface/--p-line/--p-r-lg), полная ширина (padding
24/16px). Убраны rgba-фоны и .q-dark-хаки.
- Удалён неиспользуемый виджет MeetQuorumIndicator (нет импортов;
явка/кворум живут в MeetInfoCard).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ListOfMeetsPage / MeetDetailsPage: убран max-width:960px center — контент
во всю ширину с canon-паддингом (24px/16px), как документы/платежи.
- MeetInfoCard: переписан с токсичного teal на canon — единая calm-поверхность
с тремя секциями через hairline (Даты / Ведущие / Явка и кворум) вместо
трёх вложенных цветных карточек; заголовки ink, проценты ink (не teal);
дубль заголовка «Общее собрание № N» убран (он в шапке). Убраны
var(--q-primary), color-mix, .body--dark, хардкод-rgba.
- MeetDetailsInfo: контейнер с .card-container → canon.
- MeetDetailsActions: q-btn color=primary → canon BaseButton.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- CreateMeetForm: повестка на шаге «Проверка» — аккуратные canon-карточки
(Вопрос / Проект решения / Приложения) вместо скомканного списка.
- MeetCompactCard: переписана на canon — нейтральный hover (без
токсичного teal-свечения), ink-заголовок, canon-плитки/иконка;
убраны @extend .card-container, var(--q-primary), .body--dark, color-mix.
- MeetStatusBanner: нейтральный canon-контейнер (surface-2 + line),
цвет статуса несёт иконка; убраны хардкод-rgba и .body--dark.
- MeetCardsList: empty-state на canon EmptyState, skeleton на .skel.
- Детали собрания: кнопка «Назад» убрана из топбара (снят useBackButton),
добавлен canon back-link под шапкой слева; название собрания выводится
в заголовок шапки через новый desktopStore.pageTitleOverride
(приоритет над route.meta.title; транзиентно, чистится при уходе).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Добавлен canon-компонент TableSkeleton (shared/ui/base) — повторяет
структуру .table-wrap/.table с реальными заголовками и мерцающими
плейсхолдерами (.skel) в ячейках. Каркас не дёргается при подгрузке
данных (calm-data, UX-DR2/UX-DR23). Заменил перекрывающий q-spinner
в DocumentsTable и ListOfPaymentsWidget.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- DocumentsTable: наименование берём из meta.title (чистое, без даты/.pdf
суффикса full_title); отдельная колонка Дата (meta.created_at) с сортировкой
по block_num, по умолчанию свежие сверху; колонка Документ — только заголовок.
- ID — копируемый EntityIdBadge (показывает короткий хеш, копирует полный
doc_hash по клику + иконка-affordance).
- Подписи — отдельные BaseBadge на подписанта (новый helper
getSignersListFromDocumentPackage возвращает массив).
- EntityIdBadge канонизирован: токены вместо rgba/--q-accent/.q-dark; добавлен
copyValue (показать одно — скопировать другое).
- SearchHeaderAction: на мобильном — минимальная round-dense иконка без подписи
«Поиск» (label только на десктопе).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Переделка после ревью: q-table давал не-канон вид, дёрганье при раскрытии
(virtual-scroll пересчитывал размеры + colspan не совпадал с числом колонок)
и уродливые мобильные карточки в grid-режиме.
- Статическая канон-таблица .table-wrap/.table (бордер+радиус+surface как
карточка), table-layout:fixed → колонки не разъезжаются при раскрытии.
- Раскрытие строки — CSS-only (expand-row td colspan по числу колонок),
без virtual-scroll → нет дёрганья шапки/колонок.
- Пагинация load-more (.table-foot + BaseButton «Загрузить ещё» + «1–N из M»)
вместо infinite virtual-scroll.
- Мобильный: горизонтальный скролл таблицы (.table-scroll) вместо grid-карточек
PaymentCard/DocumentCard — карточки удалены.
- Статусы платежей — BaseBadge (pos/warn/neg/info/neutral); направление —
иконка + цвет --p-pos/--p-neg; хеш документа — mono.
- Действия платежей (SetOrderPaid/Refunded) скрыты на столе пайщика
(hideActions=true) — они в реестре платежей; download документов сохранён.
- EmptyState + спиннер первой загрузки.
Виджеты общие с админ-контуром: load-more и горизонтальный скролл там тоже
уместнее jittery virtual-scroll.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Когда страница не телепортирует действия, .topbar__actions скрывается
display:none через :has(пустой host), но adjacent-селектор
.topbar__actions + .topbar__right всё равно матчился и обнулял margin-left
правой группы — она уезжала влево к крошке. Возвращаем auto в :has-правиле
(его специфичность выше). Также убрал лид-надпись на странице реквизитов.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ProfilePage (Удостоверение): IdentityPanel-шапка + BaseCard-секции (Учётная
запись/Личные данные/Документы и реквизиты) на DataRow; убран CardStyles,
хардкод rgba и .q-dark.
- PaymentMethods widget (Реквизиты): BaseCard на метод + DataRow + EmptyState
вместо .info-label/.info-value и хардкод-цветов.
- PaymentMethodsPage: кнопка добавления реквизитов переведена с useHeaderActions
store на canon Teleport (#header-actions-host), micro на мобильном.
- AddPaymentButton: триггер под canon micro-паттерн (как Deposit/WithdrawButton).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Симлинк node_modules -> /home/admin/mono-ai-2/node_modules был случайно
закоммичен и ломал pnpm install (ENOTDIR) на любой машине без этого пути.
Правило .gitignore 'node_modules/' (со слешем) ловит только каталог, не
симлинк-файл — добавлено правило 'node_modules' без слеша.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Крошка: в #crumb рядом с BackButton выводится route.meta.title текущей
страницы (тот же, что подсвечен в меню) — именно для неё действия и
сдвинуты вправо. Длинное название обрезается ellipsis в .topbar__crumb b.
ModalBase: q-bar получил авто-высоту, заголовок переносится (white-space
normal + overflow-wrap), крестик прижат к верху — на узком экране титул
больше не обрезается сверху.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: полноразмерные DepositButton/WithdrawButton в узкой мобильной шапке
раздувались — текст переносился в 2 строки, кнопки вылезали за высоту
topbar. micro-вариант (иконка + tooltip, flat/dense) и предназначен
для слота шапки.
What: Teleport-кнопки получают :micro='isMobile' (useWindowSize, <768px).
Мобильный — компактные иконки; десктоп — полные кнопки с подписью.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Кошельки (по фидбэку — переносы выглядят плохо):
- .wallet-programs из grid (repeat auto-fill minmax) → flex-column: карточки
идут списком во всю ширину страницы.
- canon .wallet__title/__sub: возвращён nowrap + ellipsis (откат wrap-фикса);
на широкой строке текст почти всегда влезает, иначе — ellipsis.
- WalletCard + минимум-карточка: нативный tooltip `title` — при наведении
виден полный текст. Убран .wallet--row reset (базовый снова nowrap).
Действия шапки (кнопки взноса/возврата пропали; нужна новая механика):
- Новый canon-механизм: страница телепортирует свои действия в шапку через
<Teleport to="#header-actions-host">. Host — постоянный span с
display:contents в #actions слоте CommonHeader (при loggedIn).
- :has()-правило прячет .topbar__actions, когда внутри только пустой host
(нет ни store-кнопок, ни телепорта) — чтобы не было пустого разделителя.
- WalletPage переведён на Teleport (DepositButton/WithdrawButton),
useHeaderActions store-механизм убран со страницы.
- Старый useHeaderActions оставлен для прочих 10 страниц — мигрируем
и удалим отдельно. DepositButton сам скрыт, пока пайщик не принят
(status !== 'active') — это защита, не баг.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: на странице без логина логотип был <img :src='logo.svg'> — img рендерит
SVG в изоляции, не наследует currentColor, поэтому показывался чёрно-белым
и не реагировал на смену темы. В личном кабинете (WorkspaceSwitcher)
тот же logo.svg рендерится inline через v-html в зелёном квадрате
и наследует color (logo.svg на fill:currentColor) — «зелёненький
на зелёном фоне», одинаковый в обеих темах.
What: CommonHeader brand-slot переведён на тот же приём —
`logo.svg?raw` + v-html внутри .app-q-header__logo
(background var(--p-primary-soft), color var(--p-primary), 28px квадрат,
16px svg). Переключение темы больше не требуется — зелёный константен.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: предыдущий фикс `overflow-wrap: anywhere; word-break: break-word`
ломал слова в любом месте даже когда колонка достаточно широкая —
«Минимальный неснижаемый остаток» рендерился по одному слову на строку,
несмотря на ~480px доступной ширины. Реально нужен только wrap по пробелам;
agressive break-word оправдан только для URL-подобных нерасчленяемых строк.
What: убраны `overflow-wrap` и `word-break` из .wallet__title/__sub —
браузер делает естественный wrap по пробелам, длинные заголовки переносятся
только когда не помещаются. `.wallet--row` reset для compact-варианта
оставлен — там по-прежнему single-line+ellipsis.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: canon-стиль .wallet__title и .wallet__sub был с
`white-space: nowrap; overflow: hidden; text-overflow: ellipsis;` — на full-
варианте это резало длинные подписи («Минимальны…», «Возвращается п…»)
даже на просторных экранах, потому что grid-колонка `.wallet__main` сжимается
ради `.wallet__amount` справа. Эта обрезка будет всплывать на любой
длинной строке (метки программ, статусы пайщика, длинные subtitle).
Compact-вариант `.wallet--row` (слот шапки) должен остаться одной строкой.
What:
- .wallet__title/__sub: убран nowrap/ellipsis; добавлено
`overflow-wrap: anywhere; word-break: break-word; hyphens: auto` (для title)
и `overflow-wrap: anywhere; word-break: break-word` (для sub) — длинные
заголовки переносятся на 2+ строки.
- .wallet--row: явно возвращает nowrap+ellipsis (с reset word-break/
overflow-wrap), чтобы compact-ряды в шапке оставались строго одной строкой.
- WalletProgramWidget: карточка минимального неснижаемого остатка
перемещена в начало grid'а (была после программ) — это базовая защита
средств пайщика, логичнее видеть её первой.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why:
- Заголовок «Кошелёк» для program='wallet' путал — в столе пайщика этот
кошелёк семантически главный, и в любых других местах canon (MicroWallet,
WalletCardMini, _dev/ui) он тоже должен называться полно — «Главный
кошелёк». Лучше один canon-default, чем локальный override в каждом
потребителе. Согласовано — переименование canon DEFAULT_TITLES.
- Минимальный неснижаемый остаток — НЕ баланс кошелька, а самостоятельная
сущность пайщика (паевой взнос, возвращается при выходе). Пристегивать
его DataRow-строкой под grid'ом — нелогично, как было и раньше в legacy.
Правильнее — отдельной карточкой в той же сетке кошельков.
What:
- WalletCard.vue: DEFAULT_TITLES.wallet 'Кошелёк' → 'Главный кошелёк'.
Глобально для всех потребителей canon-компонента.
- WalletProgramWidget.vue: убран TITLE_OVERRIDE и DataRow-блок минимального
остатка. Карточка остатка теперь рендерится в общем .wallet-programs
grid'е canon-разметкой `.wallet` с иконкой `savings`, заголовком
«Минимальный неснижаемый остаток», подзаголовком «Возвращается при выходе
из кооператива» и зарезервированной суммой. Нейтральная подсветка иконки
через `.wallet--minimum { --prog-bg, --prog-fg }`, чтобы визуально
отличалась от программ, но встала в общую сетку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: на старом WalletWidget пайщик видел свой минимальный неснижаемый остаток
(паевой взнос, возвращается при выходе из кооператива) — самостоятельная
сущность пайщика, не баланс кошелька. При переходе на canon я её упустил.
Канон-default WalletCard 'wallet' = «Кошелёк» — для стола пайщика этот
кошелёк семантически является главным (свободный остаток ЦК), поэтому
локально перекрываем заголовок на «Главный кошелёк».
What:
- TITLE_OVERRIDE['wallet'] = 'Главный кошелёк' — локальное перекрытие
заголовка только в этом widget'е, canon DEFAULT_TITLES не трогаем
(другие потребители WalletCard могут использовать общий «Кошелёк»).
- DataRow «Минимальный неснижаемый остаток» под grid'ом программ
(только когда session.participantAccount.minimum_amount > 0)
с hint «Возвращается пайщику при выходе из кооператива».
- Источник остатка — session.participantAccount?.minimum_amount,
как и в legacy WalletWidget.
Note: locked-line уже рендерится самим canon WalletCard, когда
locked-balance > 0 (logic: hasBlocked ? blocked.amount : undefined
в WalletProgramWidget). Отдельная разметка под «Заблокировано» не нужна.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: type predicate `e is CanonProgramEntry` не сходился — optional `locked?: string`
в interface vs required `locked: string | undefined` в литерале map'а. TS считает
эти типы несовместимыми для predicate, хотя они эквивалентны при присваивании.
What: переход на `flatMap<CanonProgramEntry>` с возвратом `[]` для исключаемых
программ. Predicate не нужен, generic flatMap даёт точный тип результата.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: первая страница стола пайщика (default route 'wallet') использовала legacy
обвязку — CardStyles import, scoped SCSS с .body--dark и hardcoded rgba цветами,
ColorCard с произвольным цветом по индексу. Канон уже знает программы платформы
(blagorost/wallet/generator) через WalletCard + токены --prog-*; на нём и строим.
What:
- WalletPage.vue: убран import 'src/shared/ui/CardStyles', scoped SCSS на
токенах --p-6/--p-4; вырезана легаси-карточка «Минимальный остаток»
(она не отображалась — не было разметки в template). useHeaderActions
для Deposit/Withdraw оставлен — канон поддерживает actions через #actions slot.
- WalletProgramWidget.vue: переписан на canon WalletCard + EmptyState.
Фильтр на канон-набор программ через ZEUS_TO_CANON
(MAIN→wallet, BLAGOROST→blagorost, GENERATOR→generator);
MARKETPLACE и прочие исключены — это не из основной тройки платформы
и должны рендериться отдельным виджетом.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: на главную без логина нужно показывать бренд кооператива (логотип + имя)
рядом с глобальными действиями шапки; раньше выводилось только текстовое
название через title-проп без логотипа.
What:
- AppHeader.vue: опциональный slot #brand перед .topbar__crumb;
hasBrand computed по slots.brand. Когда slot заполнен — крошка не рендерится.
- components.css: стили .topbar__brand (desktop + .topbar--mobile вариант)
с canon-токенами (--p-fs-body, --p-ink, --p-fs-meta).
- CommonHeader.vue: на !loggedIn заполняет #brand src/assets/logo.svg
+ <b>{coopTitle}</b>; передача title-пропа убрана.
- default.vue: .fixed-top-right { top: 51px } → top: var(--p-topbar-h)
(canon 56px) — выравнивание FAB под точную высоту шапки.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Существующая модель cmdk в проекте (entities/CmdkMenu/model/store.ts) —
иерархия рабочих столов и их страниц. Активный стол sticky сверху с
бейджем «Активный», без запроса показывается иерархия (стол + indented
страницы), с запросом — плоский список со столом-префиксом у каждой
страницы; стол отдельной строкой появляется только если запрос явно
начинается с его имени или содержит «стол»/«workspace».
Переписал canon CommandPalette под эту модель:
- Props: `workspaces: CommandPaletteWorkspace[]` вместо
`commands: CommandItem[]`. Каждый workspace = `{ name, title, icon,
isActive?, pages: CommandPalettePage[] }`. Page = `{ name, title,
icon?, shortcut? }`.
- Emits: `select-workspace(name)`, `select-page(workspaceName, pageName)`
— props-only, навигация и filtering по ролям/conditions остаются
заботой connected-обёртки (миграция legacy CmdkMenu — отдельная story).
- Sticky-баннер активного стола, plus accent-soft фон + outline-обводка
на selected, ↑↓ работает плоско поверх иерархии (стол → страницы → стол
→ страницы).
Mock-data в /_dev/ui/index.vue обновлён: три стола (Председатель/Пайщик/
Отчётность) со своими страницами вместо плоского списка команд.
Старый widgets/Desktop/CmdkMenu пока живёт параллельно — переключим в
ходе миграции Wave 3.
Реализованы три props-only доменных компонента из E11:
- NotificationCenter — panel-content для popover в шапке: группировка
notifications по category (system/financial/voting/message), unread-bullet
через BaseBadge, кнопка «Прочитать все», EmptyState и «Показать все».
Relative-date форматирование с русским склонением.
- CommandPalette — ⌘K/Ctrl+K с fuzzy-поиском, секции recent/pages/actions,
↑↓ навигация, Enter/Esc обработка. localStorage недавних — в connected
обёртке, компонент props-only.
- DetailsDrawer — side-sheet справа 480px (override через :width), slots
default/actions/footer, Esc и backdrop close, на xs — fullscreen.
Все три зарегистрированы в boot/ui.ts и локально импортированы в /_dev/ui
с mock-data в секциях 36-38.
E11.4 RailUserCard отложен из-за конфликта имён — существующий компонент
имеет другую роль; нужно согласовать канон-нейминг.
Заменил `.full-width.text-center` + flex без выравнивания на flex-column
со `min-height: 360px` — лоадер больше не прижат к левому верху карточки.
Заодно поправил опечатку «подговка» → «Формируем документ».
Корень: я положил символ валюты в #append slot своим <span>, в обход
встроенного prop suffix. Quasar имеет правила позиционирования именно
для родного .q-field__suffix (см. revert d0a8dc0080 от 2026-05-19,
где было решено оставить Quasar дефолт — «нас устраивает»). Мой span
в append-slot не подчинялся этим правилам и сидел в произвольной
позиции относительно цифр.
Фикс:
- :suffix='symbol' — символ валюты идёт нативным механизмом Quasar.
- Удалил .amount-input__symbol класс и template #append вообще.
- Удалил override align-items: center на q-field__control/__append/
__after (тоже мешал, как было показано в d0a8dc0080).
- font-weight 500 на цифрах оставил — нормальный вес поля.
Совет на будущее: использовать встроенные q-input props (suffix,
prefix), а не #append/#prepend slots, если можно — Quasar для них
держит готовое выравнивание, проверенное пользователем.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Корень: font-weight: 600 + tabular-nums + Quasar dense нативный input
имеют чуть смещённую baseline относительно append-слота, где сидит
RUB. Визуально цифры лежали ниже суффикса.
Фикс:
- font-weight 600 → 500 (нормальный вес поля ввода, без bold-акцента
на цифрах).
- Явный font-size + line-height на нативном input.
- align-items: center на q-field__control / __append / __after, чтобы
суффикс и any after-слот (кнопка «макс») центрировались по высоте
входной полосы.
- align-self: center на самом __symbol — на случай если q-field__append
кто-то переопределит как stretch.
TODO на следующий подход: BaseDocument loader («Формируем документ…»)
выравнивать по центру рамки документа (сейчас стоит сверху).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[Quasar] boot error: SyntaxError: Unexpected identifier 'as' — runtime-парсер
обрабатывает выражения в pug-template как чистый JS, без TypeScript. Касты
вида `el as HTMLInputElement | null`, `e as InputEvent`, `e as KeyboardEvent`
прямо в атрибутах `:ref` / `@input` / `@keydown` — синтаксическая ошибка во
время бутстрапа Vue, из-за которой /_dev/ui целиком не грузился.
Фикс:
- :ref='(el) => setRef(idx, el)' + функция setRef(idx, el: Element |
ComponentPublicInstance | null) с кастом в TS-скрипте.
- @input='(e) => onInput(idx, e)' + сигнатура onInput(idx, event: Event)
с внутренним кастом target.
- @keydown оставил передавать event «как есть» — KeyboardEvent — никакой
cast не нужен.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Quasar boot-файлы подхватываются только при рестарте dev-сервера. После
коммита 1af0f9c06f глобальные регистрации AmountInput/OtpInput/FilterBar/
FileUploader/VerticalStepper не подтянулись на лету — теги рендерились как
unknown components (пусто внутри секций 31–35 на /_dev/ui).
Фикс: добавил локальные импорты прямо в script setup _dev/ui/index.vue —
тот же паттерн, что у WalletCard, RailUserCard, AuthCard. Глобальная
регистрация в boot/ui.ts остаётся для боевого использования из других
страниц (после следующего рестарта dev'а).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Пять props-only доменных компонентов из shared/ui/domain/:
- AmountInput — денежный ввод с символом валюты, форматированием тысячных,
precision из marketplace asset config, кнопкой «макс» по balance, опциональной
подписью баланса. Tabular-nums, right-align, font-weight 600.
- OtpInput — 6 ячеек с автопереходом фокуса, Backspace откатывает на
предыдущую, paste 6-значного кода распределяется по ячейкам.
Регулярка /^\d$/, состояния error/disabled.
- FilterBar — search (debounce 300мс) + dropdown-фильтры + chip'ы активных
значений с remove + «сбросить всё». v-model для values, v-model:search для
поиска. Активные chip'ы рендерятся под рядом фильтров.
- FileUploader — drag&drop + клик по зоне; валидация accept/maxSize/maxFiles
→ emit error; список загруженных с иконкой типа, именем, размером,
кнопкой ×; слот progress для connected-обёртки.
- VerticalStepper — состояния pending/current/completed/error; completed
кликабельны для возврата назад (опционально); опциональные/disabled шаги;
слот active под телом текущего шага.
Все компоненты — pug, canon-токены --p-*, без store/router/api. Демо-секции
31–35 на /_dev/ui (наш стенд-витрина).
Зарегистрированы в boot/ui.ts и shared/ui/domain/index.ts. ESLint точечно
прошёл, vue-tsc не запускался полностью на desktop (запрет).
Wave 2 / E10 закрыт.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Удалены типы 'txt' и 'image' из DocumentPreviewType + соответствующие
ветки рендера (pre.document-preview__txt и img.document-preview__image)
и стили. В реальных потоках платформы документ кооператива всегда
HTML (рендерится в ShadowHtml внутри BaseDocument) или PDF — plain-
text фрагмент в моноширинном pre не используется и выглядит как
техническая ерунда. Аналогично image.
_dev: удалён previewTxtDemo (мусорный EOSIO chain id из debugging
notes → перешитый в фрагмент протокола → теперь полностью убран
по требованию пользователя). Секция 30 показывает только HTML +
loading + error состояния.
vue-tsc:
- _dev: убрал txId/explorerUrl из signatureSignedDemo — оба поля
удалены из Signature ранее (общего эксплорера нет).
- BaseDocument: canonSignatures.map — нормализую is_valid через
'?? undefined' (бэкенд может вернуть null, canon-компонент ждёт
boolean | undefined).
DocumentPreview txt demo: вместо мусорного 'EOSIO chain id …
dirty window' (мой случайный кусок из debugging notes) — фрагмент
протокола собрания пайщиков ПК «Восход».
ComplexDocument: .col-md-7 → .col-md-10. Это контейнер, в котором
лежит BaseDocument в реальных страницах документов. 7/12 = 58%
ширины было визуально 'приплюснуто'.
Реальный формат подписей кооперативного документа — это не «pending/
signed/rejected» из абстрактного SignatureCard, а IDocumentAggregate с
полями doc_hash + signatures[] (signer_certificate, public_key,
signature, is_valid). Каждая подпись разворачивается в детали.
Что сделано:
- Новый canon-компонент DocumentSignatures (story 9.5) в shared/ui/
domain — props-only, принимает уже резолвнутые signerName и hash-
совпадение, эмитит download/verify. Поверх Quasar — собственный
expand на ref<Set<number>>, чтобы стиль был полностью canon.
- BaseDocument теперь рендерит DocumentSignatures вместо своего
q-card.verify-card + q-list + q-expansion-item на teal/red badges.
Адаптер canonSignatures маппит signer_certificate → ФИО через
getNameFromCertificate, чтобы canon-компонент не знал про сертификаты.
- SignatureCard: удалены поля txId/explorerUrl и ссылка «Открыть в
explorer» — общего эксплорера в платформе нет.
Demo: секция 29 → DocumentSignatures (валидный + с битой подписью),
секция 30 → DocumentPreview (HTML заголовок сокращён, чтобы не
обрывался при узкой колонке).
4 props-only canon-компонента в src/shared/ui/domain/, регистрация
в boot/ui.ts, demo-секции 26-29 в _dev/ui:
- DocumentRow (story 9.1) — строка документа в списке: иконка типа
с tint'ом (pdf neg-soft, docx info-soft, html primary-soft), title,
status через BaseBadge, дата/автор/описание, slot actions, emit open.
- SignatureCard (story 9.3) — подписавший (Avatar+ФИО+AccountBadge),
статус (BaseChip pending/signed/rejected), для signed — хеш в моно-
блоке + ссылка на explorer, для rejected — BaseBanner с причиной.
- ActivityTimeline (story 9.4) — вертикальный таймлайн событий с
цветными иконками по типу (sign/reject/create/update/comment/
transfer), groupByDate группирует по «Сегодня/Вчера/конкретная
дата».
- DocumentPreview (story 9.2) — html (через DOMPurify), pdf через
iframe, image через img, txt через pre. Стейты loading/error.
Все props-only, темо-зависимые значения только через --p-* токены.
Раньше compact-вариант рендерился без обрамления — avatar упирался
в левый край контейнера, визуально «проваливался» из общего стека
full-карточек (Screenshot_2026-05-21_21-51-09).
Compact теперь: padding 8/12px (тоньше чем full 16px), тот же border
+ surface + radius. В одну строку avatar + ФИО + AccountBadge + status,
но визуально это всё-таки карточка, не голая строка.
Body раньше был обычным block-контейнером, а .head и .value-row —
оба display: inline-flex. Без явного block-форматирования inline-flex
элементы становятся inline и рендерились в одну строку. Визуально:
Email⭐ivanov@example.ru, Телефон+7 (903)..., Telegram@ivanov — слипшиеся.
Body теперь display: flex + flex-direction: column, gap canon — 8px
в comfortable, 4px в compact. Head/value-row переведены на обычный
display: flex (вместо inline-flex), gap внутри сохранён.
Прецедент 2026-05-21 — Screenshot_2026-05-21_21-42-16, 21-42-31.
- AccountBadge: copy-кнопка не сжимается (flex-shrink: 0), размер 20×20 + icon 14px — раньше 18×18 + 12px тонула рядом с текстом badge.
- DataRow: column-gap var(--p-3, 12px) → var(--p-5, 20px) + min-label-width 140 → 160px + padding-right на label. Между label и value был визуально слипшийся стык.
- ContactSheet: comfortable margin-top между label и value 2px → 8px (compact 4px). Раньше label «налипал» на значение.
- IdentityPanel compact: переделан в flat flex-row (avatar + ФИО + AccountBadge + status badge) — раньше grid с .body загонял имя и AccountBadge в две строки и avatar «уезжал» от имени. Spec 8.1 требует «только avatar + ФИО + EntityIdBadge в одну строку».
5 props-only canon-компонентов в src/shared/ui/domain/, регистрация
в boot/ui.ts, demo-секции 21-25 в _dev/ui:
- AccountBadge (story 8.5) — on-chain account name в mono-шрифте,
copyable + опциональный explorer link. Назван AccountBadge вместо
EntityIdBadge: имя EntityIdBadge занято под numeric ID пайщика
в capital. Decision зафиксирован в epics.md и planning-artifacts.
- DataRow (story 8.3) — `label: value` пара для реестровых карточек,
mono-режим, copyable, slot value-override, hint.
- ContactSheet (story 8.2) — email/phone/address/tg/web с автоиконками,
кликабельными mailto:/tel:/t.me ссылками, copy и verified-чек.
- IdentityPanel (story 8.1) — Avatar + ФИО + AccountBadge + статус
(active/blocked/pending) + role + actions-slot; compact/full.
- PersonCard (story 8.4) — Avatar + ФИО + role + AccountBadge +
ContactSheet + slot meta; compact/comfortable.
Все props-only: useStore/useRouter/useApi внутри запрещены. Темо-зависимые
значения только через --p-* токены, никаких body--dark селекторов.
q-checkbox по умолчанию красит .q-checkbox__bg в --q-primary, который в
dark теме равен #2dd4bf — на маленьком квадрате 18px этот яркий бирюзовый
смотрится ядовито. Переопределяем заливку filled-состояния на
--p-primary-press (#134e4a light / #14b8a6 dark) — глубже и спокойнее,
чек-символ остаётся белым.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Старое значение --p-accent (#D84315 light / #FF7043 dark) — слишком
насыщенное, выглядит ядовито рядом с тёплой палитрой ink/canvas. Меняем
на тёплую терракоту: #B85C38 (light) / #E89472 (dark) — тот же warm-tone,
но без перевозбуждения красного канала.
.agreement-link (псевдо-ссылки на просмотр документа в согласиях) была
hardcoded в #1c64f2 (синий) light / #ff9f43 (жёлто-оранжевый) dark и не
совпадала со ссылкой «Устав кооператива». Унифицируем: все ссылки внутри
согласий теперь var(--p-accent), визиt-цвет тот же — без фиолета.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BaseCheckbox block-вариант: убран собственный display:flex + align-items:
flex-start, который перебивал внутренний flex Quasar и сдвигал label вверх
относительно inner. Теперь Quasar сам центрирует inner на первой строке.
Добавлен padding-left: 8px на .q-checkbox__label — больше воздуха между
галочкой и текстом согласия.
ReadStatement: ссылка «Устав кооператива» — --p-primary → --p-accent
(тёплый оранжевый #D84315 / #FF7043 dark) как акцентный цвет палитры.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
UserDataForm/UserDataForm.vue:
- Выбор типа аккаунта: три q-btn(glossy, height:75px) → три BaseRadioCard
с title/description; больше нет teal-«кнопок-плиток»
- Возврат к выбору типа: flat q-btn → BaseButton(ghost) с arrow_back
Sub-forms (Individual / Entrepreneur / Organization + Create*):
- q-input/q-select: standout="bg-teal text-white" → outlined color='primary'
(canon-tone, единый primary-акцент вместо устаревшего teal)
- .q-gutter-sm.q-mt-md → .user-data-stack (flex column, gap токены)
- OrganizationDataForm: кнопка «совпадает» — color='teal' → flat primary
Валидация q-form.validate() и :rules сохранены — это всё ещё q-input
с rules-массивом, только tone и контейнер переехали на canon-токены.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- BaseCheckbox: обёртка q-checkbox с canon-стилями, block-вариант для длинных
согласий (чекбокс прижат к верху, label многострочный)
- BaseRadioCard: карточка-опция с радио-индикатором справа, props title /
description / meta + slot fallback'и; используется для выбора программы
Миграции SignUp:
- ReadStatement: q-checkbox → BaseCheckbox(block); согласия в .agreements-стек
- SelectProgram: q-list+q-radio → список BaseRadioCard
- GenerateAccount: q-input → BaseInput(readonly, mono) с copy в #append;
q-checkbox → BaseCheckbox; автоселект через querySelector('input').select()
вместо ref.select() (BaseInput не пробрасывает внутренний API q-input)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Воздух над заголовком стола внутри WorkspaceSwitcher — заголовок не упирается
в верхнюю границу caption «ПК «ВОСХОД»».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- .rail__top: padding 18px 20px → 16px 8px (унификация боковых отступов)
- WorkspaceSwitcher: убраны :disabled-атрибут и hover:not(:disabled) — у пайщика, которому доступен только один стол, кнопка выглядит как обычно (без not-allowed-cursor, без приглушённого цвета). Меню q-menu просто не открывается через v-if='workspaces.length > 1'. Тихое игнорирование клика вместо явного запрета.
WorkspaceSwitcher показывал ВСЕ workspaceMenus, включая столы выше роли пользователя (например, «Стол председателя» видел рядовой пайщик), что приводило к ошибке access-denied при попытке переключения.
- Добавлена фильтрация по иерархии: chairman ⊇ member ⊇ user. Председатель видит столы любых ролей; член совета — user+member; пайщик — только user. Workspace без meta.roles (или с пустым массивом) — публичный для всех (та же логика, что в legacy WorkspaceMenu, плюс корректное наследование прав).
- Title допускает перенос на 3 строки (line-clamp 3) — «Стол вычислительных ресурсов» теперь умещается полностью
- Расширена кнопка switcher'а (margin сторон 8→4 в LeftDrawerMenu), даёт больше места под текст
WorkspaceSwitcher:
- Brand-строка теперь собирается из vars.short_abbr + «vars.name» (получается «ПК «ВОСХОД»»). Раньше показывал только short_abbr — название кооператива было «потеряно»
- Title допускает перенос на 2 строки (line-clamp 2) — длинные названия типа «Стол вычислительных ресурсов» больше не обрезаются ellipsis'ом с одного слова
- Chevron приклеен к верху строки (align-self start) — корректно при двухстрочном title
- Меню workspace: canon padding (8/12) вокруг q-item, отступ между avatar-section и текстом — иконка и название больше не упираются в края
- Затемнение фона при открытом меню (rgba(9,9,11,0.32) overlay через Teleport в body) — меню больше не сливается с контентом за drawer'ом
Bank.vue (карточка платежа):
- Убрали q-expansion-item (раскрытие вниз внутри карточки выглядело перегружено)
- Кнопка-toggle: «Показать реквизиты» рядом с «Скачать QR». Клик переключает зону под сводкой между QR и списком 9 BaseInput'ов с copy-кнопками
- В режиме реквизитов: кнопки меняются на «Скопировать всё» (primary) + «Показать QR» (secondary)
- QR-canvas использует v-show чтобы сохранять состояние при toggle назад; реквизиты — v-if чтобы не держать BaseInput'ы в DOM
ReadStatement: backend-HTML генерирует с inline-стилями и Quasar text-h* классами, которые перебивали :deep правила. Усилили все ключевые селекторы !important + добавили охват .text-h1/h2/h3/h4/h5/h6 и .text-right (под мета-блок «УТВЕРЖДЕНО…»).
Bank.vue (карточка платежа):
- убрали отдельные surface-карточки вокруг сводки и QR (получалось «карточка-в-карточке-в-карточке»)
- сводка → плоский <dl> со строкой-разделителем снизу (border-bottom var(--p-line))
- QR → просто центрированный canvas без обёртки/фона/рамки; белый цвет внутри canvas — функциональное требование контрастного сканирования, не дизайн-выбор
- detail-expansion → без своего фона, только border-top/bottom
WorkspaceSwitcher (новый widget src/widgets/Desktop/WorkspaceSwitcher/):
- Карточка-кнопка в шапке drawer: иконка кооп + caption «ПК «ВОСХОД»» + bold «Стол совета» (текущий) + chevron
- Клик → q-menu со списком всех workspaceMenus (Стол пайщика, Стол совета, Стол председателя и т.д.); выбор → selectWorkspace + goToDefaultPage
- Подключён через #brand slot AppDrawer'а в LeftDrawerMenu, заменяет дефолтный brand-row
ReadStatement (этап 6.1):
- Backend генерирует HTML документа — локально нормализуем через :deep
- h1 уменьшен с монструозного дефолта до canon h3 (20px), центрирован
- h2/h3, p, strong, ul/ol, table, hr — типографика по var(--p-*)
- мета-блок .approved/.meta-right (УТВЕРЖДЕНО…) — ink-2, мельче, плотнее
- Чекбоксы соглашений снизу не трогали (как просил пользователь)
Bank (этап 6.2 — карточка платежа PayWithProvider):
- QR-код вынесен в фокус: сначала сводка (получатель/сумма/назначение) → QR → действия → реквизиты для ручного перевода скрыты под q-expansion-item
- Полные 9 полей (ИНН/БИК/КПП/корр-счёт/...) переведены на BaseInput(readonly, mono) с copy-кнопкой в #append; раскрываются только при необходимости
- Кнопки «Скачать QR» (primary) и «Скопировать реквизиты» (secondary) — BaseButton
- Удалены: q-btn(push), case-mixed «скопировать реквизиты», стиль #qr через global селектор
- .signature-container: 3px primary border + 3-слойный box-shadow glow → 1px dashed по var(--p-line-2), surface-2 фон, без свечения; на hover лишь окрашивается рамка
- .signature-hint: переехала в центр контейнера, canon ink-2 цвет, убран frosted-chip с blur/shadow — теперь спокойная метка «Оставьте собственноручную подпись в рамке», pointer-events: none пропускает клики на canvas
- min-height снижен 300→220, padding на canon-токены
- SignUp.vue: добавлена обёртка .signup-page (padding + center + min-height) — как у SignInPage; раньше страница была прижата к шапке
- AuthCard maxWidth: 1000 → 720 — историческое 1000 для трёх-колоночной UserDataForm; для остальных шагов выглядит абсурдно широко
- q-stepper: убраны собственный фон/тень/padding (не «карточка в карточке»), линии-разделители переведены на var(--p-line)
- EmailInput: max-width поля 360px — email не должен растягиваться во всю ширину карточки
- canon `shared/ui/domain/AuthCard` переписан на pug
- SignUp.vue переключён со старого «глянцевого» AuthCard на canon (title prop вместо CAPS-заголовка)
- старый `shared/ui/AuthCard` удалён (использовался только в SignUp)
- 7 подэкранов SignUp (EmailInput, SetUserData, SelectProgram, GenerateAccount, SelectBranch, ReadStatement, SignStatement): q-btn → BaseButton (ghost для «назад», primary для «Продолжить»)
- EmailInput: q-input → BaseInput, валидация rules переведена на computed :error
- q-input в GenerateAccount оставлен из-за зависимости от ref.select()
- og-image.png — взят brand-постер ~/blago/production/shared/poster-logo-horizontal.png,
отресайзен до 1000px по ширине и центрирован на полотне 1200×630
с белыми краями по вертикали. Шрифт/композиция родные бренда.
- description / og:description / twitter:description: «Цифровой Кооператив —
система управления хозяйством. Регистрация пайщиков, заказы на поставку
и приобретение имущества, собрания совета, общие собрания пайщиков,
взаимные расчёты на блокчейне, автоматический документооборот и бухбаланс
на основе простой электронной подписи.»
- og-image.png перерисован: «Цифровой Кооператив» / «Система управления
хозяйством» / «на платформе БЛАГО — кооперативной экономики для жизни» /
«Регистрация пайщиков · заказы на поставку и приобретение имущества ·
расчёты на блокчейне».
- description / og:description / twitter:description выровнены под этот
же копирайт. Слово «взаимоотношения с пайщиками» убрано как слишком
узкое — управление хозяйством включает в себя ещё заказы, реестры
и документы.
- src/assets/logo.svg, public/favicon.svg, public/logo.svg — единая SVG
логотипа кооператива (path с «ц» внутри). Скачано с
https://цифровой-кооператив.рф из шапки сайта.
- src/assets/logo.svg: fill -> currentColor чтобы лого внутри
canon .rail__brand наследовал --p-primary (для тёмной темы тоже).
- LeftDrawerMenu подключает лого через slot #brand-icon AppDrawer
(импорт ?raw + v-html). Внутри зелёного квадрата шапки рейла теперь
наш системный знак, а не дефолтный q-icon "dashboard".
- index.html: favicon → /favicon.svg, apple-touch-icon тоже на SVG.
Удалены неработающие ссылки на /favicon.ico и /apple-touch-icon-*.png.
- index.html: добавлены description + расширенные og:* и twitter:*
с конкретным описанием и og-image (1200×630), чтобы ссылки в чатах
выглядели прилично.
- public/og-image.png — превью 1200×630, ImageMagick-сгенерированный
композит из логотипа + текста.
- Удалены неиспользуемые ассеты:
- public/icons/, public/pwa/ — старые favicon/PWA-PNG'и всех размеров;
- src/assets/* — 40 легаси SVG/PNG (anime, blockchain, dacom*, flow*,
header-logo, quasar-logo-vertical, system*, club*, welcome.jpeg и т.д.).
Оставлены только src/assets/pin.svg (используется в Map.vue) и
src/assets/logo.svg (новый).
Pug-template передаёт выражения в Vue compiler как строки; конструкции
вида (entry as RailItem).route не превращаются в JS и падают
SyntaxError в браузере: «Unexpected identifier 'as'».
Касты унесены в <script>: добавлены computed flatItems и
normalizedGroups, шаблон работает с уже типизированными массивами без
inline-TS.
- LeftDrawerMenu: импорт useCmdkStore → useCmdkMenuStore (правильное имя).
Без этого ломался ESM build всего drawer'а: «no exported member
useCmdkStore» — RailUserCard не монтировался, поэтому и chevron свёртки
«не работал», и Найти/Пополнить не реагировали.
- AppDrawer.vue, AppHeader.vue, RailUserCard.vue переведены на pug
(lang="pug"). В проекте везде pug — никакого исторического HTML в
canon-компонентах быть не должно.
- q-drawer width 240 → 248px (совпадает с canon --p-rail-w, кошелёк больше
не уплывает справа из-за обрезки на 8px)
- LeftDrawerMenu: убран весь :deep override на .rail__usercard/.rail__signout,
возвращаем canon margin/padding как есть
- LeftDrawerMenu: добавлен v-model:collapsed на RailUserCard с сохранением
в localStorage — chevron «свернуть/развернуть» снова работает
- LeftDrawerMenu: кнопка Найти теперь дёргает cmdkStore.openDialog() напрямую
(фейковый KeyboardEvent не доходил до глобального обработчика)
- LeftDrawerMenu: «Пополнить» → useDepositDialog().open(); DepositButton +
WithdrawButton рендерятся скрыто как держатели q-dialog (q-portal в body)
- Header.vue и LeftDrawerMenu.vue переписаны на pug (как остальной проект)
- AppDrawer: плоский items оборачивается в .rail__nav, появляется gap 4-8px до cmdk
- cmdk margin 16→8px, чтоб выровнять с rail__nav (8px от стенки)
- LeftDrawerMenu footer: WalletCardMini+LogoutButton → один canon RailUserCard
(avatar+balance в primary-soft + Пополнить + встроенный signout)
- :deep override на .rail__usercard margin 16→12/8 и .rail__signout padding 12/20→12/12
- quasar-canon: body--light/dark + q-layout/q-page-container/q-page красятся
через --p-canvas, чтобы фон страницы соответствовал шапке и рейлу
LeftDrawerMenu:
- legacy `MicroWallet` (ColorCard teal с большой карточкой пайщика + ИП-badge
+ Deposit/Withdraw micro-кнопки) → canon `WalletCardMini` из
widgets/wallet-card-mini (тонкая карточка из shared/ui/domain/WalletCard
compact-variant, читает walletStore сама).
- Убран toggle «свернуть/развернуть нижнюю секцию» — нижняя секция теперь
всегда видна (короткий компактный блок: WalletCardMini + Выйти).
- Удалены неиспользуемые `onMounted`/`ref` импорты, slide-анимация и CSS
toggle-кнопки.
LogoutButton:
- pug + q-item с красной полупрозрачной плашкой → плоский ghost-button в
стиле rail-пункта AppDrawer: прозрачный фон в покое, на hover —
`--p-neg-soft` фон + `--p-neg` текст/иконка.
- Из uppercase «ВЫЙТИ» в title-case «Выйти» (canon — никаких uppercase).
Визуально левый drawer теперь полностью в canon-семье: единый rail с
пунктами + поиском + компактным wallet-балансом + ghost-кнопкой выхода.
Закрывает этап 3 из 5 в волне «приземление DS на реальный layout».
Дальше — SignUp (этап 4) и Invite (этап 5).
widgets/Header/CommonHeader/Header.vue теперь использует canon AppHeader
из shared/ui/layout. Высота шапки 56px (--p-topbar-h), border-bottom
hairline, без shadow и фоновых градиентов.
Слоты:
- #crumb: BackButton (для авторизованных), либо title с названием
кооператива из system.info.vars для гостей/install.
- #actions: headerActions injection через useHeaderActionsReader (всё что
страницы инжектят — Deposit/Withdraw, SettingsDropdown и пр. — рендерим
как было).
- #notifications: NotificationCenter (только loggedIn + isClient).
- #theme: ToogleDarkLight — пока legacy, сохраняет storage + PWA-цвет;
наш canon ThemeToggle их не делает (привяжу позже отдельным эпиком).
- #profile: BaseButton primary для гостей — Регистрация/Вход (в зависимости
от текущего route).
Удалены:
- MainHeader.vue (вся логика scroll arrows + carousel actions group ушла —
canon `.topbar__actions { gap: 8px }` достаточно; overflow-scroll
будет вернут отдельно если потребуется на узких экранах).
- HeaderStyles.scss (стили scroll-arrow и q-toolbar overrides больше
не нужны — canon-разметка через `<header class="topbar">`).
q-header Quasar остаётся wrapper'ом (sticky-poзиционирование в q-layout),
внутри — AppHeader даёт canon-разметку.
Переписан widgets/Desktop/LeftDrawerMenu/LeftDrawerMenu.vue: внутри теперь
canon `AppDrawer` из shared/ui/layout с rail-видом MONO v2 (логотип ПК +
короткое имя кооператива в шапке рейла, ⌘K-поиск, пункты-router-link'и
с иконкой/бэйджем, sticky-footer).
Адаптер store→canon:
- items: desktop.activeSecondLevelRoutes → RailItem[], с прежней
фильтрацией по roles + meta.conditions + meta.hidden (один-к-одному
логика из SecondLevelMenuList).
- activeKey: вычисляется из router.currentRoute с поддержкой группового
паттерна project-* → projects-list.
- @select: router.push({name, params:{coopname}}) или
actionsStore.executeAction(meta.action), закрытие drawer на mobile.
- @cmdk: эмиттим keyboard event ⌘K → CmdkMenu (он смонтирован глобально
в default.vue) подхватывает.
Footer-слот: пока сохранён legacy MicroWallet + LogoutButton + toggle
свернуть/развернуть (заменим на WalletCardMini в этапе 3).
default.vue: q-drawer width 200 → 240 (canon-ширина rail'а).
Что НЕ затронуто этим коммитом: верхняя шапка (legacy MainHeader),
правый drawer для actions, q-footer для гостей. Их меняем отдельными
этапами 2–3.
Корневая причина «оторванной» галочки в ResetKeyForm: я ранее задал в
mono-platform/components.css глобальное `.row { display: flex;
align-items: center; gap: 12px; }`. Quasar внутри `q-checkbox` (а также
q-radio, q-toggle, q-btn-toggle и др.) сам добавляет class="row" на
корень компонента и использует его как inline-flex — мой `gap: 12px`
раздвигал `.q-checkbox__inner` и `.q-checkbox__label` на 12px по всему
приложению.
Решение:
- Переименовать утилиту `.row` → `.u-row` (та же семантика, тот же набор
модификаторов --wrap, --gap-2/4/6). У Quasar теперь свободный .row.
- Обновить все 6 использований в /_dev/ui.
- Откатить предыдущий :deep(.q-checkbox__label) padding-left:4px workaround
в ResetKeyForm — лечил симптом, причина устранена.
Затрагивает только нашу dev-витрину и canon-utility; legacy pug-виджеты
с `.row.justify-center` продолжают работать через стандартный Quasar
flex-grid класс.
Quasar по умолчанию даёт ~12–16px padding на .q-checkbox__label, в
compact-форме сохранения ключа это смотрелось «оторвано». Уменьшаем до 4px
через :deep override на классе .rk-form__confirm.
Три кадра в одной секции: LostKey (ввод email), ResetKeyForm check-mail
(уведомление «письмо отправлено»), ResetKeyForm save-key (демонстрация
сохранения ключа с моковым сгенерированным аккаунтом). Submit на третьем
кадре регенерирует мок-ключ для повторной проверки.
Заменяет ошибочную секцию ChangeKey из предыдущей итерации.
ChangeKey-фича (формы current/new/confirm WIF + успешный диалог) была
ошибочной интерпретацией E7. Реальный flow «сменить ключ» в продукте —
это **двухэтапный ResetKey**:
1. LostKey: пайщик вводит email (он потерял ключ — текущего WIF нет),
бэк присылает письмо со ссылкой `?token=...`.
2. ResetKey: при заходе по ссылке клиент сам генерирует новый ключ
прямо в браузере (через `useCreateUser().generateAccount()`),
показывает приватный ключ readonly с «Скопировать», требует
подтверждения «Я сохранил ключ» и вызывает on-chain
`resetKey({token, public_key})`.
Текущий ключ ввести нельзя (его нет). Новый ключ ввести нельзя (только
генерируется). Никакого подтверждения нового ключа — он один и тот же.
Что сделано:
- Удалена ошибочная `features/User/ChangeKey/`.
- Старый `widgets/Registrator/ResetKey/ui/ResetKey.vue` (pug + сырые
q-input/q-checkbox + кастомные градиентные стили) разбит на:
* `ResetKeyForm.vue` — presentation (props-driven, режимы
`check-mail` / `save-key`), без useRouter/useApi — пригоден для
/_dev/ui витрины.
* `ResetKey.vue` — connected обёртка, читает `route.query.token`,
генерит account через `useCreateUser`, диспатчит `resetKey` через
`useResetKey`, на успех → router.push на signin.
- Новая UI собрана на BaseForm + BaseInput readonly mono + BaseButton +
BaseBanner + AuthCard.
/_dev/ui секция 20 теперь показывает три кадра подряд: LostKey (email
шаг), ResetKey check-mail, ResetKey save-key с моковым сгенерированным
ключом.
FR11, UX-DR9, UX-DR15.
Generic-проброс `<template v-for="(_, slotName) in $slots" #[slotName]>`
рушится в Quasar 2.19 рендере: при наличии scoped-слота #append вылетает
`Cannot read properties of null (reading 'key')` в renderSlot + Vue warns
по `_isVue`/`constructor` свойствам. Заменяем на явный список слотов
(prepend / append / before / after / hint у q-input; +option, selected-item
у q-select) — синтаксис, который Quasar держит корректно.
Кейс всплыл на ChangeKeyForm: #append с q-icon (toggle visibility) ронял
страницу /_dev/ui целиком.
Подключение ChangeKeyForm + ChangeKeySuccessDialog к dev-витрине.
Submit запускает заглушку useChangeKey().changeKey() (≈600 мс), по успеху
открывает диалог с новым WIF. Generate генерирует валидный K1 ключ через
SDK и заполняет оба новых поля. Confirmed сбрасывает форму.
Диалог успеха смены ключа: новый WIF в моноширинном блоке с copy-кнопкой,
BaseDialog с closeOnBackdrop=false, closeOnEscape=false, hideCloseButton=true.
Закрытие только через primary-кнопку «Я сохранил ключ»; до копирования
кнопка показывает guard-баннер вместо немедленного закрытия. По confirmed
эмитим событие — реальный flow подключит router.push на дашборд.
FR11, UX-DR11, UX-DR15. Story 7.2 из epics.md.
Форма смены ключа пайщика на новых компонентах дизайн-системы.
BaseBanner severity=warn с предупреждением о потере, checklist последствий,
поля current/new/confirm WIF моноширинные с toggle visibility, ghost-кнопка
«Сгенерировать» через PrivateKey.generate из @wharfkit/session.
Бизнес-логика — заглушка с TODO на привязку on-chain change-key action
(pattern см. features/User/ResetKey/api) и TODO на OtpInput шаг подтверждения
(приходит в Эпике 10).
FR11, UX-DR9, UX-DR15. Story 7.1 из epics.md.
Bug после rename --p-accent → --p-primary: класс .avatar--accent смотрел
на --p-primary-soft (canon teal), а должен был — на --p-accent-soft
(warm orange по новой семантике). И вариант ink (белый bg + dark text)
визуально выпадал из палитры — крайний xl-аватар на демо-витрине казался
«не пришей к звезде рукав».
Новая семантика AvatarTone:
- neutral (default) — серый surface-3 + ink-1 (нейтральный)
- primary — canon-teal soft + canon-teal text (основной выделение)
- accent — warm-orange soft + warm-orange text (редкое выделение,
например подсветка пайщика по batch_id)
Удалён вариант ink — не использовался в реальном коде.
В dev-витрине xl-аватар: tone="ink" → tone="primary".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Override (padding-top:0 + align-self:center на dense) не отрабатывал —
Quasar в dense держит prefix/suffix по baseline вводимого значения,
и пользователю это устраивает. Убираем неработающий override, чтобы
не мешался в дальнейших правках.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Inline `style="margin:0"` на <p>-описании перебивал мой margin-bottom
через CSS (специфичность inline > selector). Переключаюсь на
`> :first-child + *` margin-top — отступ крепится ко второму ребёнку,
не зависит от inline-стилей первого.
Работает универсально: <p> + <BaseInput>, <p> + <BaseForm>, любая
другая пара «описание + контент».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. --p-accent (canon token): #D84315 (light) / #FF7043 (dark).
Совпадает с legacy Quasar $accent (warm orange, Material Deep Orange).
Проброшен в --q-accent через CSS var — legacy color="accent",
var(--q-accent), text-accent продолжают работать без изменений,
но теперь следуют за canon-темой (light/dark). Резерв под редкое
выделение: ссылки batch_id и т.п.
2. quasar-canon.css: .q-field--dense .q-field__suffix/prefix —
padding-top:0 + align-self:center. Quasar по умолчанию ставит
padding-top: 24px под floating-label, отчего «RUB» визуально
опускался к низу control'а. Теперь в одну линию со значением.
3. BaseForm: убран gap 12px из .base-form__body — reserve-hint-space
у q-input даёт ~24px снизу под error/hint, дополнительный gap
делал расстояние между инпутами неприятно большим.
4. BaseDialog: убран универсальный gap из body, оставлен margin-bottom
только на первом <p>/.intro — между описанием и первым инпутом
воздух нужен, между остальными детьми reserve-hint-space хватает.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Промахнулся `git add -A` — захватил worktree-symlink node_modules
(он pnpm-shared с основным репо). .gitignore покрывает node_modules/
но симлинк-файл — отдельный путь.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Контекст: после проброса --q-primary = var(--p-primary) (4fddfe2343)
наш токен --p-accent и Quasar primary стали обозначать ОДИН и тот же
canon-teal — два имени для одного цвета это когнитивный шум, и при
росте платформы будет путать. Имя `accent` оставляем свободным под
будущий ВТОРОЙ цвет (warm-orange, для редкого выделения вроде ссылок
по batch_id) — это отдельная задача когда понадобится.
Переименовано везде (109 ссылок в 6 файлах):
- --p-accent → --p-primary
- --p-accent-hover → --p-primary-hover
- --p-accent-press → --p-primary-press
- --p-accent-soft → --p-primary-soft
- --p-accent-line → --p-primary-line
- --p-accent-strong → --p-primary-strong
- --p-ink-on-accent → --p-ink-on-primary
Файлы: tokens.css, quasar-canon.css, components.css, RailUserCard,
AuthCard, _dev/ui (включая user-facing labels палитры «Accent → Primary»
и заголовок секции «Акцент → Основной цвет»).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- tokens.css: проброс canon-палитры в Quasar brand-переменные
(--q-primary/negative/positive/warning/info = соответствующие
--p-* токены). Резолвится lazy → автоматически следует за темой.
Теперь Quasar САМ красит focus q-field, q-btn color="primary",
bg-primary utility-классы в canon-цвета без CSS-overrides;
- quasar-canon.css: убраны border-color overrides на ::before
(default/focus/error) — Quasar управляет цветом через --q-primary
и собственный механизм --focused/--highlighted. Оставлены только
background, text-color и rounded углы (border-radius var(--p-r-sm));
- BaseInput/Select: вернули `dense` (компактный 40px) и явный
`color="primary"` (canon teal через --q-primary).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Между нижним инпутом и footer-кнопками был ~32px (q-card-section
padding-bottom 20 + q-card-actions padding-top 12) — выглядело
растянуто. Сводим к 16px (8+8) — кнопки сидят ближе к контенту.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Регрессия мерцания границы и приплюснутости — следствие борьбы наших
overrides с встроенной Quasar анимацией. Возвращаем дефолт: Quasar
сам решает геометрию (height/padding/border-radius/font), анимации,
opacity ::before и focus-индикацию. Наша задача — только покрасить
в canon-цвета.
Удалено из .q-field--outlined:
- height/padding/border-radius/font-size/font-family overrides;
- transition border-color (Quasar сам делает свой transition);
- opacity:1 !important на ::before (был костыль от мерцания);
- box-shadow:none на --focused/--highlighted;
- display:none на ::after underline;
- override .q-field--dense;
- override .q-field--float .q-field__label (Quasar анимирует label сам);
- focus-shadow / outline reset на native input.
Оставлено (только цвет):
- background: var(--p-surface);
- border-color: line-1 / accent (focus) / neg (error);
- color текста, placeholder, label, prefix/suffix, hint/error.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Quasar при focus анимирует opacity ::before (прячет старую границу
перед показом ::after underline). Так как ::after мы спрятали,
получался видимый gap ~200-400 ms «граница исчезла → появилась
с подсветкой». Фикс: opacity:1 !important на ::before,
transition только по border-color.
- BaseTable.vue: убран неиспользуемый импорт BaseTableColumn.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- BaseInput/BaseSelect: убран `dense` — возвращаемся к дефолтному
outlined Quasar (control ~56px). Floating-label получает достаточно
воздуха, текст значения визуально центрируется без сжатия;
- BaseDialog: `.base-dialog__body` теперь flex-column с gap 16px —
описание/инпуты/таблицы внутри диалога получают единый воздух
без правки потребителя (пример: «Паевой взнос: укажите сумму…» →
отступ до инпута появляется автоматически);
- BaseForm: `.base-form__body` gap 4px → 12px — те же требования
внутри формы;
- WalletCard: `.wallet__metric-val` 19→17px — итоговое значение
для аккуратного баланс-блока FULL.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. Stack-label: BaseInput/BaseSelect → только `dense`. Quasar
стандартно сам корректно держит floating-label в dense+outlined,
без CSS-override метка работает по документации Quasar.
2. Двойная focus-подсветка: убран наш box-shadow var(--p-focus-ring)
поверх Quasar внутренней focus-индикации (виден был как «белый
ринг» на тёмной + наш teal). Теперь только border-color меняется
через ::before (canon-минимализм). Дополнительно `box-shadow: none
!important` на --focused/--highlighted control и `display:none` на
::after underline (на случай dense Quasar variants).
3. Mini-wallet в правом верхнем углу AppHeader: слот #wallet удалён
из API (баланс уже отображается в RailUserCard в drawer'е,
дубль в шапке избыточен). Убран из dev-витрины каркаса.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Регрессия после Quasar wrapper-pivot: метрики кошельков выглядели
крупно по сравнению с заголовком/sub-label, а padding'и были
избыточны для compact-варианта.
- .wallet (FULL): padding 18→14px по верт, иконка 44→40px,
metric-val 22→19px, ccy 13→12px;
- .wallet--row (compact): padding 14→10px, иконка 36→32px,
metric-val 18→16px — выглядит как row в списке, а не как карточка;
- .rail__balance-val (mini-wallet в drawer): 22→18px — теперь не
доминирует над identity-блоком пайщика.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Корень регрессии: жёсткий height: 40px в .q-field--outlined раздавил
вертикальную разметку q-input (Quasar держит padding-top:24px под
floating-label слот; при сжатии текст значения сдвигается). Те же
эффекты для select.
- BaseInput/BaseSelect → dense + stack-label по умолчанию;
Quasar сам корректно держит 40px в dense, метка статически стоит
над инпутом без floating-механики;
- .q-field--outlined: убран принудительный height; .q-field--dense
получает только padding-x, высоту контролирует Quasar;
- .q-btn: font-weight 500→450, min-height 40→36px, padding 16→14px —
кнопки больше не выглядят «жирно/тяжело»;
- .q-btn--outline: hairline-граница через ::before (Quasar pattern),
без двойной рамки;
- размеры sm/lg/dense пропорционально уменьшены;
- .q-chip: высота 24→26px, padding 10→12px, font-weight 500→450;
+ .q-chip__icon размер 14px (для точечных чипов).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BaseSelect → q-select (outlined, reserve-hint-space, map-options/emit-value
для совместимости с {value,label} options API).
BaseCard → q-card flat с q-card-section head/body. Variants flat/inset/quiet
override-ят через scoped style.
BaseChip → q-chip с color mapping (variant → primary/positive/negative/
warning/info). Square для canon-look.
BaseBadge → q-badge с тем же color mapping. Dot-вариант через scoped style
(8×8 круг).
BaseBanner → q-banner rounded dense с border-left по variant.
BaseDialog → q-dialog + q-card с size→maxWidth mapping. closeOnBackdrop/
closeOnEscape пробрасываются через :persistent / :no-*-dismiss.
BaseTable → q-table с трансформацией columns в QTableProps['columns'].
Hide-pagination, binary-state-sort. Cell-slots работают через body-cell-{key}.
AuthCard (domain) — был на native canon `.card` div. Переведён на q-card flat
для консистентности. Усиленный shadow + accent-stripe сверху сохранены.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BaseInput теперь рендерит <q-input outlined reserve-hint-space no-error-icon>
с проброшенными slots. reserve-hint-space от Quasar резервирует место
под error/hint встроенно — раньше делали вручную через NBSP. Все Quasar
features (rules через v-bind \$attrs, mask, ref API, native slots) работают
прозрачно. Props API совместим (modelValue, label, hint, error, prefix,
suffix, mono, readonly, disabled, autocomplete, name, id, type).
BaseButton — q-btn с variant→Quasar mapping:
primary → :unelevated color="primary" (заливка accent)
secondary → :outline (border + surface)
ghost → :flat (transparent)
danger → :flat color="negative" (transparent + neg-cвет)
:no-caps :ripple=false для canon-look. Props API совместим.
BaseForm — q-form-обёртка с сводным error-баннером (q-banner) и slot footer.
banner—neg styling прилетает из quasar-canon.css через .bg-negative-soft.
Глобальная стилизация Quasar в quasar-canon.css даёт canon-look всем
этим компонентам и существующим q-input/q-btn/q-card по всему проекту.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Архитектурный pivot: вместо native HTML5-компонентов под canon-классами
стилизуем Quasar поверх. Причины:
- Сохраняется весь Quasar API из коробки (rules/mask/dense/slots/refs)
- Существующий код (сотни q-input, q-btn, q-card, q-select) автоматом
получает canon-look без правок
- Меньше регрессий — Quasar отвечает за accessibility/keyboard/focus
Файл src/css/mono-platform/quasar-canon.css содержит overrides под canon
для: q-field--outlined (q-input/q-select), q-btn (unelevated/outline/flat),
q-card, q-chip, q-badge, q-banner, q-dialog/q-menu, q-item, q-table,
q-checkbox/q-toggle.
Подключен в quasar.config.cjs css[] после components.css. tokens.css
без изменений — все цвета/радиусы/тени тянутся как переменные.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Истинный источник «прыгалки» AuthCard на hover: глобальное правило
`.card:hover { transform: scale(1.03) }` в src/app/styles/style.css
(и его дубль в pages/Marketplace/MainPage/ui/MainPage.vue, тоже не scoped)
бьёт по любому элементу с классом card. AuthCard имеет корневой
<div class="card auth-card"> — каждый hover дёргает её на 3%.
Поиск показал: класс `card` без модификаторов в templates никто кроме
новых canon-компонентов (BaseCard, AuthCard) не использует. Этот глобал —
мёртвый legacy, удаляем безопасно.
NBSP-фикс layout-shift из предыдущего коммита (2a2d9a786a) остаётся
актуальным — он лечил другую (на уровне поля) проблему.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BaseInput: контейнер field__message теперь рендерится всегда с min-height
calc(--p-fs-meta * 1.4) — раньше div появлялся/исчезал на каждом keystroke
при валидации (см. LostKey emailError), форма прыгала вверх-вниз.
Когда нет ни ошибки, ни хинта — рендерится NBSP, высота сохраняется.
AuthCard: добавлен box-shadow поверх canon-минималистского .card —
canvas #fafafa и surface #ffffff на светлой почти неразличимы, без тени
карточка выглядит плоской. На тёмной отдельный shadow усилен (поверх
canon `--p-shadow-card`, который оптимизирован под data-cards, не hero).
border-color подкручен с --p-line на --p-line-1 для большей чёткости.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Scope-adjustment: plan'овая Story 6.3 предполагала миграцию SignUp как
второго auth-flow, но Register — multi-step wizard (10+ файлов в
pages/Registrator/SignUp/), который требует VerticalStepper из Эпика 10
и не помещается в Волну 1. Вместо этого мигрируем LostKey — простую
одно-полевую auth-страницу, того же auth-домена.
LostKey (widget): pug → html, старый AuthCard (q-card с shimmer) →
canon AuthCard. q-input → BaseInput с inline email-валидацией;
q-btn → BaseButton; всё обёрнуто в BaseForm с loading/error.
Бизнес-логика (useLostKey.startResetKey + router redirect на resetkey)
сохранена 1:1.
LostKeyPage: footer-кнопка «Назад» через ghost BaseButton в footer-slot
AuthCard; центровка через flexbox.
Не мигрировано в Волне 1 (известный gap):
- SignUp wizard (требует VerticalStepper из E10)
- ResetKey / Invite (на старом AuthCard, мигрируются в Волне 2)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
LoginForm (feature): q-input → BaseInput, q-btn → BaseButton, обёрнут в
BaseForm с управлением loading/error. Бизнес-логика (useLoginUser, redirect,
OpenReplay tracking, NotificationPermissionDialog) сохранена 1:1.
Ошибки входа теперь показываются через сводный error BaseForm
(плюс legacy FailAlert остаётся как fallback).
SignIn (widget): импорт AuthCard переключён на новый canon-композит
(src/shared/ui/domain/AuthCard), убран pug-template и градиентные стили.
Добавлен footer-slot — пробрасывается в AuthCard.
SignInPage: переписан с pug → html, footer-кнопки «Потеряли ключ?» /
«Нет аккаунта?» вынесены в footer-slot AuthCard через BaseButton ghost-sm.
Центровка через flexbox вместо QRow grid.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AuthCard (src/shared/ui/domain/AuthCard) — canon .card + center-aligned
head (title/subtitle/slot head) + body-slot + опциональный footer-slot.
Старый src/shared/ui/AuthCard (q-card с shimmer-градиентом) остаётся
для не-мигрированных страниц (SignUp wizard, ResetKey, Invite).
AuthLayout (src/app/layouts/AuthLayout.vue) — wrapper-обёртка для full-page
auth-страниц: фон var(--p-canvas), центрирование, тогл темы в правом верхнем.
Витрина: dev-секция 19 с примером login-формы (BaseForm + BaseInput ×2 +
BaseButton + footer-ссылки).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
WalletCardMini подключает WalletCard к Pinia (useWalletStore.program_wallets) +
System store (info.symbols.root_govern_symbol) + useRouter (клик →
name: 'wallet'). Программный mapping: wallet→MAIN, blagorost→BLAGOROST,
generator→GENERATOR (Zeus.ProgramType). Loading определяется автоматически
по наличию данных в store (или пробрасывается через prop).
Подключено в слот wallet AppHeader на dev-витрине (секция 16 — каркас) +
выделенная секция 18 с тремя программами. На dev-странице покажет
skeleton-loading т.к. loadUserWallet не вызывался; на реальных страницах
после init процесса — настоящий баланс.
Со-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Благорост — капитализация РИД (не «накопления на жильё»), Генератор —
генерация РИД (не «доходы от программы»). Кошелёк остаётся «свободный
остаток». Черновые ярлыки риском попадают в production через копипасту.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Канонический .wallet с компактным (--row) и full-вариантами, программные
акценты через --prog-blagorost / --prog-wallet / --prog-generator (UX-DR20).
Витрина на /_dev/ui секция 17: три программы × оба варианта + loading +
empty + locked-line.
Compact (.wallet--row): 36px icon, 18px sum, 14px padding — для слота шапки.
Full (.wallet): 44px icon, 22px sum, 18px padding — для дашборда.
Connected-обёртка widgets/wallet-card-mini (Story 5.2) делается отдельным
коммитом, требует интеграции с реальным wallet-store и MARKETPLACE_ASSET_CONFIG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Анимация max-height + opacity при свёртке давала видимый прыжок крупной
суммы баланса (`font-size: 22px` + `display: flex; align-items: baseline`):
во время промежуточных кадров baseline переcчитывался относительно
схлопывающегося контейнера → текст «прыгал в середину» и обратно.
Возвращаемся к canon `display: none`. Resize моментальный, без skipping.
Поведение rail (top опускается, bottom-chevron остаётся через flex-spacer)
работает и без анимации — пользователь это и подтвердил в фидбеке.
Chevron rotation через :deep(.q-icon) и balance-route не трогаем —
они независимы.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. **Палитра системы.** Новая секция «00» в шапке dev-страницы со всеми
основными --p-* токенами:
- Акцент (accent / hover / press)
- Поверхности (canvas / canvas-2 / surface / 2 / 3)
- Текст (ink / ink-on-accent)
- Линии (line / 1 / 2) — текстовый input для rgba
- Статусы (pos / neg / warn / info)
- Программные тинты (blagorost / wallet / generator)
Color-picker для hex-токенов, текстовый ввод для rgba. Live-override
через `document.documentElement.style.setProperty()` — inline-style на
:root перебивает CSS-правила, перекрашивание мгновенное. Подсветка
переопределённых токенов + счётчик в шапке + кнопка «Сбросить» (через
removeProperty). Watch на Quasar Dark — при смене темы пикеры
обновляются на новые resolved-значения.
2. **Chevron в свёрнутом виде — full-width.** Из-за того что для плавной
анимации primary остаётся в DOM (max-height:0 вместо display:none),
у него сохранялся `flex: 1` и он съедал ширину карточки → chevron не
мог растянуться через canon-правило `width: 100%`. В `.is-collapsed`
добавили `flex: 0 0 0` + `width: 0` для primary — chevron теперь
корректно занимает всю ширину.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
По фидбеку:
1. **Chevron не переворачивается.** Canon-правило
`.rail__usercard__collapse svg { transform: rotate(180deg) }` рассчитано
на `<svg>`, а Quasar `<q-icon>` рендерит `<i class="q-icon">`. Прокидываем
transition + rotate через scoped `:deep(.q-icon)` — chevron теперь крутится
при свёртке/развёртке.
2. **«Карточка улетает»** — резкий jump при `display:none` создаёт визуальный
прыжок. Подменяем display:none на `max-height: 0` + `opacity: 0` с
transition'ом. Логика canon-селектора `.is-collapsed` не нарушается,
просто добавлена плавность. Если структурное поведение rail (top
опускается, bottom-chevron остаётся на месте) всё равно неудобно —
перевёрстаем во вторую итерацию.
3. **Клик по балансу → маршрут кошелька.** Новый prop `balanceRoute` —
если задан, блок `.rail__balance` оборачивается в `<router-link :to>`.
Параллельно эмитим `balance-click` — для аналитики или функционального
handler'а. На витрине привязал к `/_dev/ui#wallet` чтобы проверить.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. `RouteMeta.icon` объявлен обязательным в `src/env.d.ts` — добавили
`icon: 'fa-solid fa-flask'` для dev-роута, иначе vue-tsc валится.
2. `RailUserCard` (`shared/ui/domain/RailUserCard`) — доменный
общеплатформенный компонент мини-кошелька в нижней части drawer'а.
Это будущий E11 (Волна 2), но логично вытащить раньше, чтобы основной
layout можно было заменить целиком уже сейчас.
Реализован по canon строго: rail__usercard с usertop+balance+actions,
collapse-chevron справа с поворотом arrow при свёртке (через
`.is-collapsed` модификатор + canon CSS), отдельный `rail__signout`
блок (выходит вне `.rail__usercard` — рендерится сразу после, в том
же `aside.rail` через footer-slot).
Контракт строго dumb по архитектуре: только props/emits/slots.
- props: name, role, avatarSrc, balance, symbol, balanceLabel,
lockedBalance, lockedLabel, primaryActionLabel, collapsed,
showSignout, signoutLabel
- emits: primary-action, update:collapsed (v-model), signout
- slots: usertop-extra, actions
Coнnected-обёртки (с инжектом store/auth) — на уровне страницы,
не здесь.
3. Витрина `/_dev/ui`:
- Секция 12 (AppDrawer): заменили временный огрызок rail-demo__user
на полноценный `<RailUserCard show-signout>` через footer-slot.
- Секция 16 (Каркас приложения): аналогично — теперь у rail полноценный
footer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
E4 «Layout-каркас» Волны 1: 4 структурные обёртки в `src/shared/ui/layout/`.
По архитектуре — только props/emits/slots, без store/router/api, без знания
о данных. Cтраницы передают menu-items, breadcrumbs, табы и активные ключи
сверху.
Состав:
- `AppDrawer` — canon `.rail`: solid surface, активный пункт soft-accent
пилюлей + 2-px рейл слева. Поддерживает плоский список + секции
(RailSection с eyebrow-меткой), opt-in ⌘K-кнопку, slot `footer` для
user-card / logout.
- `AppHeader` — canon `.topbar`: бургер · crumb · `topbar__actions` ·
`topbar__right`. Поддерживает простой title и multi-level breadcrumb.
Глобальные действия через слоты `notifications`/`theme`/`wallet`/`profile`
или общий `right`.
- `PageHead` — canon `.page-head`: eyebrow + title + subtitle + slot
`actions`. Используется когда страница БЕЗ подстраниц.
- `PageTabs` — canon `.tabbar` + `.tab`: вкладки подстраниц с tab__count,
опциональный hairline-разделитель и `tabbar__actions` справа.
Также в составе коммита:
- Убрали AuthCard из base — он композит (cap + body + head + footer),
переедет в E6 Auth flow рядом с SignUp/ResetKey/Invite.
- BaseForm: gap полей 16→10px (тесные формы как в каноне).
- Витрина `/_dev/ui`: 4 новые секции (12-15) на каждый layout-компонент +
секция 16 «Каркас приложения» — AppDrawer + AppHeader + PageTabs в живой
связке без legacy QLayout/QDrawer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Скрипт извлечения из bundled HTML оставил ` <style>...` в начале и
`</style>` в конце обоих файлов. CSS-парсер ловил `<` как ошибочный
селектор и пропускал блок до ближайшей `}` — съедая весь первый блок
правил.
В tokens.css первым блоком был `:root, [data-theme="light"]` со светлой
палитрой — она целиком игнорировалась. `[data-theme="dark"]` ниже
парсился нормально, поэтому тёмная тема работала, а светлая — нет:
кнопки primary/ghost/danger оставались с transparent background и
сливались с canvas.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Заносим эталонную дизайн-систему в проект — только базовые элементы для
дальнейшей компоновки. Source — MONO Design System.html.
Архитектура (по `_bmad-output/planning-artifacts/architecture.md`):
- `src/css/mono-platform/{tokens.css,components.css}` — canon токены и стили
компонентов 1:1 из эталона; импортируются первыми в quasar.config css[].
- `src/shared/ui/base/<Имя>/{Vue,types,index}` — 14 базовых обёрток (Vue 3 +
TS, native HTML над canon BEM, no Quasar-deps кроме Dark API в ThemeToggle).
FSD-граница: только props/emits/slots, никаких store/router/api.
- `src/boot/ui.ts` — глобальная регистрация 14 базовых компонентов.
- `src/boot/theme.ts` — sync `html[data-theme]` ↔ Quasar Dark.
- Inter + JetBrains Mono в index.html.
14 базовых:
BaseButton, BaseInput, BaseSelect, BaseCard, BaseTable, BaseChip, BaseBadge,
BaseDialog, BaseBanner, BaseForm, EmptyState, Avatar, ThemeToggle, AuthCard.
Витрина `/_dev/ui` (dev-only) — каждая обёртка отдельной секцией над чистым
canon-канвасом, без layout-обёрток. Маршрут смонтирован на корневом уровне,
ВНЕ DynamicLayoutWrapper, чтобы legacy default.vue не перебивал стили.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Boot-файл haptics: при reload страницы на устройстве с Vibration API (мобильный
браузер / PWA на Android) коротко вибрирует (15 мс). Reload определяется через
Navigation Timing (с фолбэком на legacy API), чтобы не срабатывать на первой
загрузке/навигации. Полностью безопасно для десктопа и обычных сайтов: где
Vibration API нет — вызов не делается, любая ошибка проглатывается, на загрузку
приложения не влияет. Только клиент (гард для SSR).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
В левом дровере был маленький горизонтальный скроллбар, и трекпадом его можно
было «оттянуть», обнажая правую границу — дровер выглядел скроллируемым, хотя не
должен. Причина: у .rail собственный border-right (1px), который при content-box
даёт ~1px overflow внутри контента дровера. Фикс в .app-left-drawer: overflow-x
hidden + overscroll-behavior-x contain на .q-drawer__content, .rail приведён к
box-sizing border-box и width 100%.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Если проголосовать и сразу нажать «Утвердить», утверждение падает с ошибкой —
бэкенд ещё не учёл голос из блокчейна. После успешного голоса держим состояние
загрузки пункта (а значит и кнопку «Утвердить» в :loading) ещё VOTE_SETTLE_MS
(3с), за это время голос успевает обработаться. На ошибке загрузка снимается
сразу. Заодно ручное мутирование processingDecisions + setTimeout-хак заменены
аккуратным реактивным хелпером setProcessing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Повестка совета показывает все НЕутверждённые вопросы. Голос «за»/«против» не
должен убирать пункт — он остаётся неутверждённым, лишь помечается отметкой
голоса. Убрал ошибочное actedDecisionIds.add из onVoteFor/onVoteAgainst
(оставлен только тихий рефетч). Скрытие через actedDecisionIds оставлено только
в onAuthorizeDecision — там пункт исполняется и должен уйти из повестки без
возврата от отстающего поллинга.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Две проблемы при голосовании/утверждении в повестке совета:
1. Моргание всей страницы: QuestionsTable подменял список тремя
скелетонами при любом loading=true, а пост-экшен loadDecisions вызывался
без hidden → loading=true → список «моргал» в скелетоны и обратно.
Скелетоны теперь показываются только на первой загрузке (loading &&
!decisions.length), а пост-экшен рефетчи переведены в тихий режим (hidden).
2. «Исчез → вернулся → исчез»: повестка на бэкенде уже не отдаёт пункт, по
которому проголосовал/утвердил, но данные из блокчейна доходят с задержкой,
и поллинг успевал вернуть отработанный пункт. Добавлен локальный набор
actedDecisionIds — проголосованный/утверждённый пункт прячется сразу и не
возвращается, что бы ни подтянул отстающий рефетч. Просто, без оптимистичных
merge-оверлеев.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Две независимые проблемы при выдаче разрешения на уведомления:
1. Фриз приложения: shouldShowDialog был computed с побочным эффектом —
внутри геттера вызывался updateSupport(), присваивавший store.support
новый объект на каждое чтение. После выдачи разрешения это давало каскад
инвалидаций/ре-рендеров, забивавший главный поток (роутер менял URL, DOM
«застывал»). Геттер сделан чистым, updateSupport() вынесен в showDialog().
2. Красная ошибка «Service Worker не готов в течение 5 секунд»: в dev без PWA
SW намеренно не регистрируется, поэтому navigator.serviceWorker.ready
никогда не резолвится. Теперь сначала проверяем getRegistrations() — при
отсутствии SW выходим сразу и тихо (warn вместо error), а subscribe()
возвращает false без FailAlert. Push при недоступности SW полностью
изолирован и не влияет на работу приложения.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Кнопки взноса и возврата на столе пайщика недоступны, пока пайщик не
подписал главное соглашение цифрового кошелька (type='wallet') — как и
сама карточка кошелька, которая не появляется без подписанного соглашения.
Геттер isWalletAgreementSigned в сторе Wallet читает тот же список
соглашений пайщика, что и RequireAgreements.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
«(опционально)» в заголовок + пояснение в описании: если есть пайщики —
импортируйте CSV, если нет — двигайтесь дальше. Чтобы шаг не воспринимался
обязательным.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
readonly outlined q-input рендерится с dashed-рамкой (дефолт Quasar) и
обрезал значение (…UJR4) — выпадало из канона и выглядело плоско.
Сделал ключ surface-2 панелью: eyebrow-лейбл + copy-иконка в шапке,
mono-значение целиком (word-break) ниже. Даёт вес и убирает «ёлочку».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
matrixRoomId — это одновременно идентичность и capability: зная его, можно
попытаться войти в комнату. Раздавая его на фронт (страница «Комнаты секретаря»
и список транскрипций отдавали его в каждой строке), мы по сути публиковали
handle для входа в приватные комнаты любому, кто видит раздел. Приватность тогда
теряет смысл.
Комнаты секретаря:
- ChatcoopSecretaryRoom и RemoveSecretaryRoomInput переведены с matrixRoomId на
внутренний opaque id реестра (DB-ключ, по нему войти в Matrix-комнату нельзя);
- удаление резолвится по id → matrixRoomId на бэкенде; мутация больше не
возвращает matrixRoomId;
- фронт: row-key/удаление по id, убран показ matrixRoomId в таблице;
- попутно фикс латентного бага из PR #34: parseKind не знал про 'secretary' и
мапил такие комнаты в capital_project — из-за чего editable=false и удаление
падало с «удалять можно только комнаты секретаря».
Транскрипции:
- CallTranscription больше не отдаёт matrixRoomId (на фронте он не отображался,
но уходил в payload по всем комнатам, включая приватные). Остаётся roomId
(LiveKit room name — внутренний, не Matrix-handle);
- input-фильтр GetTranscriptionsInput.matrixRoomId сохранён: это вход, не утечка,
и им пользуется blago-cli;
- blago-cli: рендер транскрипций больше не зависит от tr.matrixRoomId (строка
«Matrix room» убрана, иначе pull и restore рассогласовались бы — restore не
имеет контекста комнаты). Заодно поправлен fallback курсоров.
Вне scope намеренно: chatcoopListNonProjectCommunicationRooms и per-room запросы
оставлены с matrixRoomId — их зовёт только blago-cli (доверенный инструмент
председателя), на фронт они не уходят, и matrixRoomId там — операционный ключ
синхронизации.
logger.ts: при CONTROLLER_SCHEMA_GEN не подключаем файловые транспорты winston
(codegen рядом с работающим контейнером падал на EACCES в root-овый logs/).
schema.gql + zeus (sdk и controller) перегенерированы и закоммичены — иначе CI
typecheck собирает sdk против старого zeus и падает.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Прошлый вариант разъезжался: «Скопировать» висел сиротой справа, провалы
между блоками, узкая потерянная кнопка, карточка 560px — разрежено.
Переделал по канону:
- копирование ключа — иконкой content_copy в append самого поля (+tooltip)
- кнопка «Установить ключ» — full-width (block), как «Войти» в LoginForm
- ширина карточки 480 как у остальных auth-карточек
- чекбокс слева, плотный ритм без лишних margin (reserve-hint-space)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Голый «Потребительский Кооператив» в примере провоцировал вводить просто
ОПФ без расширения. Дал полный пример «...Социального Комплекса» во всех
трёх падежах — чтобы вводили расширенную ОПФ+, а не базовую форму.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Это не переменные, а постоянные параметры, которыми настраивается фабрика
документов кооператива. Переименовал label и intro шага.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Микроотступ стал лишним и снова раздвигал поля слишком широко — откатываю.
Разделение полей обеспечивает сам reserve-hint-space, дополнительный
глобальный отступ не нужен.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Постоянный hint под полем занимал место и отвлекал. Перенёс примеры
наименований ОПФ+ в placeholder (видны только в пустом поле), убрал hint.
Добавил reserve-hint-space всем q-input формы — строка под ошибку
резервируется без текста, валидация не вызывает layout shift.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Без зазора текст hint/error верхнего поля почти касался границы нижнего.
Вернул маленький отступ, но системно и один раз — глобально в quasar-canon:
margin-bottom: var(--p-1) только полям .q-field--with-bottom (те, что
резервируют нижнюю строку). Работает везде (BaseForm, формы установки,
регистрация), в стеках не складывается в дыру, инлайновые поля не трогает.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
reserve-hint-space у q-input/BaseInput уже даёт ~24px снизу под error/hint;
дополнительный gap в стеках полей складывался с ним → избыточное расстояние.
Убрал gap (канон BaseForm__body):
- CreateOrganizationDataForm / IndividualDataForm (.user-data-stack)
- SetVariablesForm (.vars-section__fields)
- RequestKeyForm (.request-key)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Канон-плотность полей (dense) была только в BaseInput; общие формы
CreateOrganizationDataForm и IndividualDataForm + поля шагов установки
рендерили крупные не-dense q-input. Добавил dense:
- CreateOrganizationDataForm (22 поля) — данные организации
- IndividualDataForm (6 полей) — члены совета
- email-инпуты SetInitForm/SetSovietForm и поля SetVariablesForm
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- InstallCooperativePage: VerticalStepper вместо q-stepper, canon-панель
с accent-стрипом, экран завершения в каноне (без градиентов/shimmer)
- RequestKeyForm: BaseInput (mono) + BaseButton
- SetInitForm: canon info-нотки, кнопки Назад/Далее на BaseButton
- SetSovietForm: canon-карточки членов совета вместо q-card/q-badge
- SetVariablesForm: canon-секции, q-input outlined с правилами, BaseButton
- Invite: осмысленное тело AuthCard при отсутствии токена приглашения
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
.extension-page уже внутри .catalog-shell__content с padding var(--p-6);
собственный padding давал двойной зазор от шапки до кнопки «Назад».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Весь контент страницы расширения обёрнут в канон-панель (surface+border),
больше не лежит на голом фоне
- В режиме настроек добавлена кнопка «Отменить» (CancelButton) рядом с
«Сохранить» — явный выход без сохранения; кнопка «Назад» страницы тоже работает
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Кнопка «Назад» убрана из шапки (useBackButton снят), добавлена канон-кнопка
под шапкой на самой странице
- Логотип AutoAvatar центрирован в колонке
- DesktopsList: центрированный inset-блок (выделяется), а не плоский список
- ext-actions toggle: «включено» и «удалить» снова в одной строке
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Без delay nodemon реагировал на каждое fs-событие пачки (правки/сборка/git на хосте) десятками рестартов в секунду; pstree.remy без `ps` в образе виснет на обходе /proc (накапливались зомби-сканеры), холодный старт ts-node прерывался, порт 2998 не занимался — бэкенд молча лежал. delay:2000 схлопывает пачку в один рестарт, signal:SIGTERM даёт чистое завершение дочернего процесса.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Триггер ринг-анимации перенесён с hover самого SVG на hover карточки
(.app-card:hover :deep(.ring-seg)).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AutoAvatar получил режим animated: инлайн-SVG вместо <img>, каждый
сегмент-дуга обёрнут в .ring-seg. По hover логотипа сегменты на ~1.3с
расходятся — прямые и зеркальные (matrix-flip) от одного поворота идут
в разные стороны, со стаггером по индексу. CoopCard не затронут (img/blob).
Уважает prefers-reduced-motion.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Статус идёт сразу под заголовком (динамично) — без дыры под однострочными
названиями. Обрезку в 2 строки оставили, чтобы длинный заголовок не ломал сетку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Заголовок резервирует 2 строки (clamp+min-height), аватар по верху —
статус и описание на всех карточках начинаются на одной линии, не прыгают.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Верхний ряд: логотип + (заголовок, под ним статус); описание во всю
ширину ниже; футер «Подробнее» прижат к низу для выравнивания карточек.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Палитра колец → мягкие приглушённые тона + CSS saturate(.5)/opacity .85,
чтобы знак читался как тихая текстура, а не неоновое пятно.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- AutoAvatar параметризован: props size/radius/background/ringColor
с обратно-совместимыми дефолтами (CoopCard не затронут)
- ExtensionCard: буква-монограмма заменена на уникальный rings-логотип,
seed = имя расширения; цвет кольца детерминирован по seed из палитры
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
extensionRepository.update заменяет config JSONB целиком; chairman/powerup/meet-tracker писали захваченный при initialize снимок поверх свежих данных. Теперь read-modify-write по актуальному config из БД с наложением только владомых полей.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- PaymentProviderForm: убраны вложенные q-gutter (кривой левый отступ
селекта), standout=bg-teal → outlined, text-grey-7 → токен; чистый
канон-столбец (селект max-width 480, hint ink-2, действия в ряд, no-caps).
- ChangeContacts: текст уточнён — контакты видны всем (раздел «Контакты
кооператива» пайщикам + подвал сайта незарегистрированным), не только
незарегистрированным.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Кооперативные участки, Регистрационные взносы, Ключ кооператива,
Провайдер платежей, Контакты кооператива:
- убран дублирующий заголовок страницы (есть крошка в шапке);
- описание перенесено с голого фона на канон-поверхность .banner;
- page-shell/hero/surface-card с хардкод-px → канон-отступы и токены;
- q-input/q-select standout=bg-teal → outlined color=primary.
Также удалён лишний хвост текста на Стартовых страницах.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Заголовок страницы уже показан крошкой в шапке — повтор h2 убран.
Пояснительный текст вынесен с голого фона на канон-поверхность (.banner
с инфо-иконкой). ApprovalsPage не трогаем — там заголовка страницы нет.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ApprovalsPage: канон-отступы страницы, фильтр-селект teal standout → outlined.
- SystemSettingsPage: hero-card/surface-card с хардкод-px → канон page-head
(h2 + sub на токенах) + q-card(flat); канон-отступы.
- MembersPage: тот же канон-паттерн вместо hero-card с хардкодами.
- DefaultPagesForm: 4× q-select standout=bg-teal → outlined color=primary.
Виджеты таблиц/состава совета без экзотики — канон-тема через quasar-canon.css.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Шаблон двухкорневой (кнопка + Teleport затемнения), class из родителя
(LeftDrawerMenu) не наследовался автоматически. inheritAttrs:false +
v-bind=$attrs на корневую кнопку — атрибуты направлены явно.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Подписи секций «Вопрос на повестке»/«Проект решения» переведены в
надстрочные метки (uppercase, трекинг, ink-3) — отдельный ярус ниже
заголовка окна, чтобы не казаться конкурирующим заголовком рядом с
крупным заголовком самого документа. Текст вопроса — читаемый body.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ConnectPage: добавлены канон-отступы страницы (var(--p-6)/var(--p-4)) —
класс .padding в проекте не определён, контент липнул к краям.
- OnboardingStepsCard: убран q-card (двойной паддинг), текст-интро
растянут на полную ширину (снят max-width 70ch), вокруг кнопки шага
убрана лишняя рамка (.step__content → .step__action, только отступ).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- OnboardingStepsCard: под заголовком «Адаптируйте кооператив…» добавлен
поясняющий текст (что происходит дальше: решения совета в электронной
форме, вступление в «Восход», импорт пайщиков, общее собрание) + плашка
срока адаптации с отсчётом.
- Чек-лист шагов переведён на канон-вертикальный степпер (.stepper--v):
пройденные — галочка/заливка primary, текущий — подсветка номера,
действия — канон BaseButton; agenda/import/meet типы сохранены.
- Диалог «Предложение повестки» (объявить собрание совета) переведён
с ModalBase на канон BaseDialog + BaseButton.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- CommonHeader: крошка строится из route.matched (путь внутри стола),
напр. «Отчётность › Календарь»; корень стола не выводится; плоские
страницы дают одну крошку как прежде; pageTitleOverride сохранён.
- CoopWalletsPage: переводы временно скрыты под флагом transferEnabled=false
(открывались с L3-кошельков пайщиков; вернём для кооп-кошельков позже).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- editor-container/action-panel/backdrop/validation-badge/mark-hint: hex и rgba → --p-* токены
- text-grey-8 → t-muted; box-shadow/overlay через токены с fallback
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- SecondLevelTabs на глобальных .tabbar/.tab — sub-навигация между топбаром и контентом
- WalletsPage/DocumentsPage shell-страницы: вкладки в .tabbar вместо RouteMenuButton в топбаре
- TransferWalletsButton: канон header-кнопка (q-btn primary)
- WalletTransferDialog: на BaseDialog + BaseButton, токены вместо rgba
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ZodForm: убран standout="bg-teal text-white" с динамического поля —
QInput/QSelect теперь канон (outlined+dense+color=primary+reserve-hint-space),
как BaseInput. SCSS на канон-токены (вместо неопределённого --q-primary-rgb).
ExtensionSettings: снят заголовок «Настройки» внутри карточки — он дублировал
«Настройки аренды» в шапке.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Канон-инпут вместо q-input: BaseInput расширен поддержкой type="date"
и clearable (+ emit clear) — это нужно для фильтра по датам. Фильтры
в ExtensionLogsList переведены на BaseInput.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ExtensionSettings: EmptyState + BaseCard вокруг ZodForm вместо самописных
header/empty-state. ExtensionLogsList (используется только powerup):
q-table → список канон-карточек с фильтром по датам, скелетоном,
«Загрузить ещё» и EmptyState; слот #log-item и API сохранены.
Канон-падинги страниц настроек/лога аренды.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Монитор ресурсов: баннер-предупреждение на всю ширину сверху, ряд
«Как это работает» (2/3) + кошелёк AXON (1/3), ряд CPU/NET/RAM.
Убран 3D-флип карт (он же «прыгал») — детали раскрываются inline.
Все виджеты/страницы/лог сведены к канон-токенам surface/line/ink.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Реестр платежей: кнопки действий больше не block (не заполняют ячейку
от левого края, не липнут к бейджу статуса). Стопка inline-flex,
равная ширина кнопок (align-items:stretch), прижата к правому краю
колонки (col-action 168px, text-align:right) — слева остаётся воздух.
- Реестр документов: col-date 116→156px — дата «26.05.2026 08:25» больше
не налезает на заголовок документа; min-width 720→780.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Bank.vue: панель платежа max-width 380px по центру — длинное «Назначение»
больше не раздувает диалог «Совершите взнос» до max-content; значение
переносится по строкам (overflow-wrap: anywhere).
- Кнопки статуса платежа SetOrderPaid/RefundedStatusButton → BaseButton
(primary/danger, block), подписи «Подтвердить»/«Отклонить»; диалог
вынесен из-под кнопки.
- Реестр платежей: кнопки действий друг под другом (.cell-actions),
колонка действий 156px, дата без переноса (.col-date 132px), min-width
таблицы уменьшен.
- Реестр документов: сужены колонки подписей (220→150), действий
(110→56), id/дата; min-width 880→720 — освобождено место под
наименование, таблица не вылазит за страницу.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Таблицы документов и платежей уже на канон-токенах и самообрамлены
(.table-wrap). Не хватало полей страницы как в реестре пайщиков:
- PaymentsPage рендерил виджет голым, впритык к краям → q-page + 24/16px.
- ListOfDocumentsPage: q-page.padding (16px) → канон-24px.
- Убрана лишняя .row.justify-center обёртка в ListOfDocumentsWidget
(.col-12 и так во всю ширину).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AddUserDialog перестроен в канон-визард по образцу CreateMeetForm:
шапка-bar (surface+line) вместо bg-gradient-dark, VerticalStepper из трёх
шагов — Электронная почта → Тип и данные → Вступительный взнос, BaseButton
в подвале, outlined-инпуты. UserDataForm переиспользован целиком (общий,
не тронут). Опция начисления взноса — канон-блок вместо q-item/q-card.
ParticipantsImportDialog: добавлено описание-интро (раньше отсутствовало),
ModalBase/bg-gradient-dark заменён на канон-bar, выбор типа аккаунтов через
BaseRadioCard, dropzone на токенах вместо хардкод-цветов, секции и действия
в канон-обёртках, BaseButton вместо цветных q-btn. Логика парсинга/импорта
и таблицы превью/результатов сохранены.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Страница получила канон-отступ (--p-6 / --p-4 на мобилке), таблица обёрнута
в обрамлённую surface-карточку (--p-line, --p-r-lg) вместо edge-to-edge.
Развёрнутые данные пайщика — спокойная вложенная панель на --p-surface-2
с отступами --p-5/--p-6, читаемой шириной формы (640px) и вертикальным
ритмом между полями. Снят full-height у таблицы (мешал внутри карточки).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Развёрнутая строка пайщика теперь показывает только его данные;
документы пайщика живут в отдельном «Реестре документов» и здесь не дублируются.
ParticipantDetails упрощён (без q-tabs/ListOfDocumentsWidget), tab-логика
вычищена из таблицы и страницы. Мобильная ParticipantCard переведена на канон-токены
(убраны хардкод-цвета, CardStyles и q-badge статуса → disabled-checkbox).
Таблица сохранена, сортировка по дате вступления (DESC) без изменений.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Убран EntityIdBadge. Зелёная плашка-аватар слева теперь несёт сам номер вопроса
(он же Decision ID): клик по плашке копирует id (тултип «Скопировать №»), hover —
инверсия в сплошной teal. Заголовок снова просто текст вопроса.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EntityIdBadge перенесён из отдельной строки под ФИО в начало строки заголовка:
[#id] → сразу текст вопроса. Inline, vertical-align middle. Раньше висел снизу
и пусто растягивал верхнюю строку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EntityIdBadge с id вопроса под заголовком/ФИО: отображает #<id>, по клику копирует
чистый id (copy-on-click). Канон-компонент, клик не раскрывает карточку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
«Истекает через месяц» перенесён из строки под заголовком в постоянную нижнюю
полоску (footer): срок слева, «Утвердить» справа (только председателю). У обычного
пайщика — только срок, узкая панелька.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Срок теперь подписан «Истекает через месяц» (было просто «через месяц» — неясно).
Удалён статус-чип «Вы за/против» — направление голоса уже видно по подсветке кнопок,
бейдж был избыточным. Убраны связанные computeds и стили.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Кнопки голосования прижаты к правому краю строки. «Утвердить» вынесена вниз
отдельной строкой-footer (справа, hairline сверху) — больше не теснит данные.
Срок «через месяц» и статус-чип «Вы за» перенесены влево под заголовок/ФИО.
Клик по строке раскрывает документ, по кнопкам голосования и footer — нет.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Карточка вопроса перестроена в единую горизонтальную строку: иконка + вопрос/ФИО
слева (flex), компактный блок органов управления (голосование + «Утвердить»),
срок + статус-чип и шеврон — справа, друг рядом с другом. Убрана центрированная
vote-зона с пустотой по бокам. Клик по строке раскрывает документ, по органам
управления (@click.stop) — нет. Подсказка «Утвердить» возвращена в tooltip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Шапка карточки (вопрос/ФИО/срок/статус) кликабельна целиком и раскрывает документ,
справа шеврон-индикатор. Органы управления голосованием — отдельная зона ниже шапки
(сиблинг, не аккордеон): клик по ним голосует/утверждает и документ не раскрывает.
Убрана отдельная кнопка «Документ» — раскрытие теперь по клику на шапку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
QuestionCard: VotingButtons + «Утвердить» вынесены на верхний уровень карточки —
голосовать можно без раскрытия; раскрытие («Документ») открывает только содержимое
документа. Истечение срока остаётся в шапке.
VotingButtons: вернул чек-индикатор «принято советом» (закрашивается при принятии)
вместо иконки verified.
Header-CTA совета (Предложить/Добавить/Импорт): убран push (q-btn--push не попадал
под канон-правило заливки primary → белый шрифт на нетиловом фоне), подписи с заглавной.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
QuestionsTable: q-table → карточный список (скелетоны + EmptyState).
QuestionCard: канон-поверхность, BaseButton «Утвердить», токен-чип статуса
вместо q-badge, локальное состояние раскрытия, без @import CardStyles и хардкод-hex.
VotingButtons: токены pos/neg вместо .text-red/.text-green/#666.
Страница: канон-padding вместо q-card flat.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Названия столов в реестре workspace'ов заведены вразнобой («Стол
благороста», «Стол вычислительных ресурсов» vs «Стол Совета»).
Добавил text-transform: capitalize на подписи в WorkspaceSwitcher
(заголовок текущего стола + пункты выпадающего меню) и WorkspaceMenu
(карусель + диалог выбора) — первая буква каждого слова заглавная,
одинаково для всех и для будущих столов, без правки строк-источников.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
По фидбеку переработана композиция:
- убраны заголовки секций «Контакты»/«Руководство» и per-строчные
иконки телефона/почты/адреса (из-за них значения «прыгали» вправо
относительно ИНН/ОГРН);
- ИНН и ОГРН теперь в одну строку (адаптивная сетка полей);
- председатель поднят наверх к реквизитам, поле названо просто
«Председатель совета»;
- контакты — те же поля, телефон/email как ссылки (tel/mailto), всё на
единой левой кромке; одна тонкая линия делит реквизиты и контакты.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Реквизиты/Руководство больше не разносят подпись и значение к
противоположным краям (на полной ширине это давало пустой провал и
оторванные значения). Все секции теперь как ContactSheet: подпись
сверху, значение под ней, слева, hairline между строками — один ритм
по всей карточке.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ContactsPage: три цветные ColorCard (orange/indigo/teal/blue) заменены
на единую спокойную canon-поверхность с секциями через hairline
(Реквизиты / Контакты / Руководство). Контакты — через canon
ContactSheet (копирование, mailto/tel). Заголовок и строки на токенах
(--p-ink/-2/-3, --p-surface/--p-line/--p-r-lg), полная ширина (padding
24/16px). Убраны rgba-фоны и .q-dark-хаки.
- Удалён неиспользуемый виджет MeetQuorumIndicator (нет импортов;
явка/кворум живут в MeetInfoCard).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ListOfMeetsPage / MeetDetailsPage: убран max-width:960px center — контент
во всю ширину с canon-паддингом (24px/16px), как документы/платежи.
- MeetInfoCard: переписан с токсичного teal на canon — единая calm-поверхность
с тремя секциями через hairline (Даты / Ведущие / Явка и кворум) вместо
трёх вложенных цветных карточек; заголовки ink, проценты ink (не teal);
дубль заголовка «Общее собрание № N» убран (он в шапке). Убраны
var(--q-primary), color-mix, .body--dark, хардкод-rgba.
- MeetDetailsInfo: контейнер с .card-container → canon.
- MeetDetailsActions: q-btn color=primary → canon BaseButton.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- CreateMeetForm: повестка на шаге «Проверка» — аккуратные canon-карточки
(Вопрос / Проект решения / Приложения) вместо скомканного списка.
- MeetCompactCard: переписана на canon — нейтральный hover (без
токсичного teal-свечения), ink-заголовок, canon-плитки/иконка;
убраны @extend .card-container, var(--q-primary), .body--dark, color-mix.
- MeetStatusBanner: нейтральный canon-контейнер (surface-2 + line),
цвет статуса несёт иконка; убраны хардкод-rgba и .body--dark.
- MeetCardsList: empty-state на canon EmptyState, skeleton на .skel.
- Детали собрания: кнопка «Назад» убрана из топбара (снят useBackButton),
добавлен canon back-link под шапкой слева; название собрания выводится
в заголовок шапки через новый desktopStore.pageTitleOverride
(приоритет над route.meta.title; транзиентно, чистится при уходе).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Добавлен canon-компонент TableSkeleton (shared/ui/base) — повторяет
структуру .table-wrap/.table с реальными заголовками и мерцающими
плейсхолдерами (.skel) в ячейках. Каркас не дёргается при подгрузке
данных (calm-data, UX-DR2/UX-DR23). Заменил перекрывающий q-spinner
в DocumentsTable и ListOfPaymentsWidget.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- DocumentsTable: наименование берём из meta.title (чистое, без даты/.pdf
суффикса full_title); отдельная колонка Дата (meta.created_at) с сортировкой
по block_num, по умолчанию свежие сверху; колонка Документ — только заголовок.
- ID — копируемый EntityIdBadge (показывает короткий хеш, копирует полный
doc_hash по клику + иконка-affordance).
- Подписи — отдельные BaseBadge на подписанта (новый helper
getSignersListFromDocumentPackage возвращает массив).
- EntityIdBadge канонизирован: токены вместо rgba/--q-accent/.q-dark; добавлен
copyValue (показать одно — скопировать другое).
- SearchHeaderAction: на мобильном — минимальная round-dense иконка без подписи
«Поиск» (label только на десктопе).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Переделка после ревью: q-table давал не-канон вид, дёрганье при раскрытии
(virtual-scroll пересчитывал размеры + colspan не совпадал с числом колонок)
и уродливые мобильные карточки в grid-режиме.
- Статическая канон-таблица .table-wrap/.table (бордер+радиус+surface как
карточка), table-layout:fixed → колонки не разъезжаются при раскрытии.
- Раскрытие строки — CSS-only (expand-row td colspan по числу колонок),
без virtual-scroll → нет дёрганья шапки/колонок.
- Пагинация load-more (.table-foot + BaseButton «Загрузить ещё» + «1–N из M»)
вместо infinite virtual-scroll.
- Мобильный: горизонтальный скролл таблицы (.table-scroll) вместо grid-карточек
PaymentCard/DocumentCard — карточки удалены.
- Статусы платежей — BaseBadge (pos/warn/neg/info/neutral); направление —
иконка + цвет --p-pos/--p-neg; хеш документа — mono.
- Действия платежей (SetOrderPaid/Refunded) скрыты на столе пайщика
(hideActions=true) — они в реестре платежей; download документов сохранён.
- EmptyState + спиннер первой загрузки.
Виджеты общие с админ-контуром: load-more и горизонтальный скролл там тоже
уместнее jittery virtual-scroll.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Когда страница не телепортирует действия, .topbar__actions скрывается
display:none через :has(пустой host), но adjacent-селектор
.topbar__actions + .topbar__right всё равно матчился и обнулял margin-left
правой группы — она уезжала влево к крошке. Возвращаем auto в :has-правиле
(его специфичность выше). Также убрал лид-надпись на странице реквизитов.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ProfilePage (Удостоверение): IdentityPanel-шапка + BaseCard-секции (Учётная
запись/Личные данные/Документы и реквизиты) на DataRow; убран CardStyles,
хардкод rgba и .q-dark.
- PaymentMethods widget (Реквизиты): BaseCard на метод + DataRow + EmptyState
вместо .info-label/.info-value и хардкод-цветов.
- PaymentMethodsPage: кнопка добавления реквизитов переведена с useHeaderActions
store на canon Teleport (#header-actions-host), micro на мобильном.
- AddPaymentButton: триггер под canon micro-паттерн (как Deposit/WithdrawButton).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Симлинк node_modules -> /home/admin/mono-ai-2/node_modules был случайно
закоммичен и ломал pnpm install (ENOTDIR) на любой машине без этого пути.
Правило .gitignore 'node_modules/' (со слешем) ловит только каталог, не
симлинк-файл — добавлено правило 'node_modules' без слеша.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
При удалении publish-docs я снёс не только мёртвый gh-pages-push, но и реальную
публикацию ВТОРОЙ документации (доки mono) через DOCS_DEPLOY_WEBHOOK_URL —
она отдельная от coopenomics и должна уезжать синхронно. Возвращаю её отдельным
lean-джобом trigger-mono-docs (только webhook, без gh-pages и mkdocs-сборки —
приёмник деплоит сам).
Токен GITEA_DISPATCH_TOKEN → DOCS_DISPATCH_TOKEN: Gitea резервирует префикс
GITEA_ для имён секретов, такой секрет не создать.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Дефолтная ветка C9S/coopenomics — master; workflow_dispatch надо слать на
ref=master, иначе Gitea вернёт 404 по ветке.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
После переезда на Gitea два downstream-джоба были наследием GitHub:
- publish-docs пушил site/ в gh-pages github.com под secrets.GITHUB_TOKEN
(на Gitea — гитейный токен, невалиден) → Authentication failed. Pages
устарел, доки теперь деплоит C9S/coopenomics. Джоб удалён целиком.
- trigger-coopenomics-docs слал repository_dispatch на github.com через
peter-evans (COOPENOMICS_PAT пуст). Репо переехало в C9S/coopenomics на
Gitea, а Gitea не имеет API для repository_dispatch — только
workflow_dispatch. Заменено на curl к Gitea API workflow_dispatch с
secret GITEA_DISPATCH_TOKEN; целевой workflow получит входы mono_sha/mono_ref.
publish-packages не трогаю — там нужен лишь NPM_TOKEN в секретах (E404 на
scoped-пакетах = отсутствует npm-авторизация).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Крошка: в #crumb рядом с BackButton выводится route.meta.title текущей
страницы (тот же, что подсвечен в меню) — именно для неё действия и
сдвинуты вправо. Длинное название обрезается ellipsis в .topbar__crumb b.
ModalBase: q-bar получил авто-высоту, заголовок переносится (white-space
normal + overflow-wrap), крестик прижат к верху — на узком экране титул
больше не обрезается сверху.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: полноразмерные DepositButton/WithdrawButton в узкой мобильной шапке
раздувались — текст переносился в 2 строки, кнопки вылезали за высоту
topbar. micro-вариант (иконка + tooltip, flat/dense) и предназначен
для слота шапки.
What: Teleport-кнопки получают :micro='isMobile' (useWindowSize, <768px).
Мобильный — компактные иконки; десктоп — полные кнопки с подписью.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
coopback/cooparser/desktop монтируют ./:/app, а node:22-slim без USER пишет
от root — codegen (generate-client, schema.gql, logs, quasar-кэш) кладёт
root-owned файлы в дерево и ломает git checkout (git работает от ant без sudo).
Добавлен user: "1000:1000" + HOME=/tmp, чтобы вывод контейнеров принадлежал
хостовому ant. node_modules/.pnpm-store уже ant:ant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
generate-client пишет копию zeus и в components/controller/zeus — обновляю вместе
со schema.gql, чтобы не оставлять дрифт сгенерированных артефактов.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
generate-schema + generate-client после добавления резолверов SecretaryRooms и
chatcoopListNonProjectCommunicationRooms. Без этого @coopenomics/sdk:build падал
в CI на отсутствующих в zeus типах (ChatcoopSecretaryRoom / CreateSecretaryRoomInput
/ RemoveSecretaryRoomInput / ChatcoopNonProjectCommunicationRoom) — общий build-шаг
обеих джоб typecheck. Локально sdk build (prebuild tsc --noEmit) зелёный.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- entity SecretaryRoom (api/model/store) поверх SDK chatcoopListSecretaryRooms /
chatcoopCreateSecretaryRoom / chatcoopRemoveSecretaryRoom
- SecretaryRoomsPage: список комнат реестра (тип, наличие секретаря, шифрование),
создание комнаты (публичная/приватная) и удаление комнат секретаря
- маршрут chatcoop-secretary-rooms, доступ роли chairman+member
- системные/проектные комнаты — read-only (удаление только у kind secretary)
Затипизируется после generate-schema/generate-client (новые SDK-операции).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Задача 1 (синхронизация в blago):
- roomKind 'secretary' в реестре управляемых Matrix-комнат
- репозиторий: findAll + findNonProjectCommunicationRooms (всё кроме capital_project)
- inter-порт listNonProjectCommunicationRooms + тип InterNonProjectCommunicationRoomRef (kind)
- query chatcoopListNonProjectCommunicationRooms (members/council/secretary) для blago-cli
Задача 2 (комнаты секретаря, backend):
- SecretaryRoomManagementService: создание комнаты (public/private, всегда plaintext,
force-join только секретаря, создатель — модератор) и удаление (kick секретаря + дерегистрация)
- SecretaryRoomsResolver: chatcoopListSecretaryRooms / chatcoopCreateSecretaryRoom /
chatcoopRemoveSecretaryRoom; доступ chairman+member
- matrix-api: inviteUser / kickUser
- конфиг SECRETARY_ROOM_MATRIX
Принцип: секретарь присутствует только в backend-созданных комнатах; force-join в
произвольную чужую комнату запрещён (общий Synapse на все кооперативы).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
registry-1.docker.io на runner'е изредка не резолвится (DNS-таймаут к
127.0.0.53), из-за чего push валит весь релиз уже после успешной сборки
образов. Обёртка dpush() повторяет push до 3 раз с паузой 10с во всех трёх
push-шагах (контракты, base, сервисы).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
clang-9 падал "error while loading shared libraries: libz3.so.4". Пакет cdt
объявляет только libcurl4-gnutls-dev, но бинари тулчейна (objdump -p NEEDED)
требуют libz3.so.4/libtinfo.so.6/libxml2.so.2/libz.so.1. В образе
dicoop/blockchain они были из сборки исходников, из .deb не тянутся —
ставим libz3-4 libtinfo6 libxml2 zlib1g явно.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Шаг компиляции контрактов в 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>
Все три reboot-скрипта теперь идентичны, кроме строки pnpm run boot[:extra|:clean]:
- wipe blockchain-data контейнером (docker run alpine rm) вместо sudo rm —
работает без sudo на любой ноде независимо от владельца данных (на Pi нет
passwordless sudo; на проде nodeos пишет данные под root). Единый способ с reboot.sh.
- monoredis в down -v и up -d — без него coopback падает на старте
(getaddrinfo EAI_AGAIN monoredis → nodemon crash), и провайдер не получает
org-данные partner1.
- coopback через up -d --force-recreate (перечитывает env), как в reboot.sh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- booter.ts: installExtraData (регистрация partner1 с auto-approve → триггер аренды
провайдером) выполняется только под EXTRA_RENT=1. boot:extra используется и для
других задач (пересев совета/чейна), где аренда VM не нужна.
- infra.ts: partner1 подписывает wallet-соглашение (wallet::signagree, program_id=1)
при регистрации. Без членства в ЦПП Кошелька provider.performInitialTransfers
(150 AXON на partner1 перед ACTIVE) падает ассертом eosio.token::is_can_transfer
«Получатель не является участником целевой потребительской программы кошелька»,
и инстанс навсегда висит в INSTALL.
- blockchain/index.ts: activateFeature не роняет boot при protocol_feature_exception
(фича уже активна на не-обнулённом чейне) — ловим и продолжаем.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Штатный синк вместо точечной правки: schema.gql приведён в соответствие с
актуальными DTO контроллера (был протухший — накопленный дрейф полей,
не только blocked). zeus перегенерирован из новой схемы; оба
controller/zeus и sdk/src/zeus идентичны.
Результирующие изменения относительно L3-blocked-ветки:
- удалён blocked из Ledger2Wallet/ProgramWallet (цель PR);
- синхронизированы ранее не закоммиченные дрейфы: is_server_init в SystemInit,
enum-значение AWAITING_AUTHORIZATION, актуализация описаний/ролей @Field.
- blocked сохранён в ChartOfAccountsItem (legacy chart-of-accounts).
Проверено локально: sdk typecheck и controller typecheck зелёные.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Кошельки (по фидбэку — переносы выглядят плохо):
- .wallet-programs из grid (repeat auto-fill minmax) → flex-column: карточки
идут списком во всю ширину страницы.
- canon .wallet__title/__sub: возвращён nowrap + ellipsis (откат wrap-фикса);
на широкой строке текст почти всегда влезает, иначе — ellipsis.
- WalletCard + минимум-карточка: нативный tooltip `title` — при наведении
виден полный текст. Убран .wallet--row reset (базовый снова nowrap).
Действия шапки (кнопки взноса/возврата пропали; нужна новая механика):
- Новый canon-механизм: страница телепортирует свои действия в шапку через
<Teleport to="#header-actions-host">. Host — постоянный span с
display:contents в #actions слоте CommonHeader (при loggedIn).
- :has()-правило прячет .topbar__actions, когда внутри только пустой host
(нет ни store-кнопок, ни телепорта) — чтобы не было пустого разделителя.
- WalletPage переведён на Teleport (DepositButton/WithdrawButton),
useHeaderActions store-механизм убран со страницы.
- Старый useHeaderActions оставлен для прочих 10 страниц — мигрируем
и удалим отдельно. DepositButton сам скрыт, пока пайщик не принят
(status !== 'active') — это защита, не баг.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI Typecheck падал на @coopenomics/sdk:build (tsc): селекторы
programWalletSelector/ledger2WalletSelector и зависимые capital-селекторы
больше не содержат blocked, но zeus-типы (MakeAllFieldsRequired) всё ещё
требовали его как обязательное поле.
- schema.gql: убран `blocked: String!` из типов Ledger2Wallet и ProgramWallet
(точечно, без полной регенерации — она тянет несвязанный дрейф схемы).
- zeus (controller/zeus + sdk/src/zeus): убран blocked из Ledger2Wallet и
ProgramWallet во всех 4 секциях (ValueTypes/ResolverInputTypes/ModelTypes/
GraphQLTypes). blocked в ChartOfAccountsItem (legacy chart-of-accounts) и
статус-комментарии (accepted|blocked) сохранены.
- CoopCard.vue: удалён мёртвый закомментированный блок с wallet.blocked.
Проверено локально: sdk typecheck и controller typecheck зелёные.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Упраздняем субсчёт "заблокировано" на кошельках. Резерв средств под заявку
на возврат паевого теперь выражается переводом на отдельный COOPERATIVE-кошелёк
w.wal.wpend, а не блокировкой внутри w.wal.share. Все движения — только переводы
и сжигание, без BLOCK/UNBLOCK.
Контракты (ledger2/wallet/capital):
- wallets.hpp: новый кошелёк WITHDRAW_PENDING (w.wal.wpend, COOPERATIVE).
- operations.hpp: из WalletOp удалены BLOCK/UNBLOCK/BURN_BLOCKED (числовые
значения ISSUE/TRANSFER/BURN/NONE сохранены для совместимости истории).
Переключены 3 операции возврата: o.wal.wthreq -> TRANSFER share->wpend;
o.wal.wthdec -> TRANSFER wpend->share; o.wal.wthcpl -> BURN wpend.
- walletop.cpp/revert.cpp: удалены кейсы и валидации блокировки.
- migrate(): свёртка blocked->available по ВСЕМ коопам (выполняется автоматически
при деплое; сигнатура без аргументов). Поле blocked в структурах таблиц
оставлено deprecated — физическое удаление = смена layout таблицы на живых
коопах, отдельный cleanup-деплой.
- capital balances/importcontr: больше не читают blocked.
- p.wal.wthdrw.standard.yaml: стандарт приведён к модели "резерв на кошельке".
cooptypes/SDK:
- operations.ts: WalletOp без BLOCK/UNBLOCK/BURN_BLOCKED; 3 операции возврата
переключены; wallets.generated.ts регенерён (добавлен w.wal.wpend).
- selectors: убран blocked из programWalletSelector/ledger2WalletSelector.
Backend (controller): убрано поле blocked из GraphQL DTO ProgramWallet и
Ledger2Wallet. TypeORM-колонка/внутренние интерфейсы оставлены deprecated
(без DB-миграции).
Frontend (desktop): убраны все поля "Заблокировано" — WalletProgramWidget,
WalletWidget, ParticipantWalletsPage, CoopWalletsPage, CapitalWalletsCardsWidget,
ContributorsListWidget, optimistic blocked_delta.
Границы (не в этом PR): legacy soviet::progwallets blockbal/unblockbal и
donor-marketplace; legacy ledger v1 chart-of-accounts "Заблокированные средства";
полное удаление поля blocked из C++-структур таблиц.
Требует CI-шага: regen GraphQL schema + zeus client (generate-schema/-client)
для согласования schema.gql и zeus-типов с удалённым полем blocked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why: на странице без логина логотип был <img :src='logo.svg'> — img рендерит
SVG в изоляции, не наследует currentColor, поэтому показывался чёрно-белым
и не реагировал на смену темы. В личном кабинете (WorkspaceSwitcher)
тот же logo.svg рендерится inline через v-html в зелёном квадрате
и наследует color (logo.svg на fill:currentColor) — «зелёненький
на зелёном фоне», одинаковый в обеих темах.
What: CommonHeader brand-slot переведён на тот же приём —
`logo.svg?raw` + v-html внутри .app-q-header__logo
(background var(--p-primary-soft), color var(--p-primary), 28px квадрат,
16px svg). Переключение темы больше не требуется — зелёный константен.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: предыдущий фикс `overflow-wrap: anywhere; word-break: break-word`
ломал слова в любом месте даже когда колонка достаточно широкая —
«Минимальный неснижаемый остаток» рендерился по одному слову на строку,
несмотря на ~480px доступной ширины. Реально нужен только wrap по пробелам;
agressive break-word оправдан только для URL-подобных нерасчленяемых строк.
What: убраны `overflow-wrap` и `word-break` из .wallet__title/__sub —
браузер делает естественный wrap по пробелам, длинные заголовки переносятся
только когда не помещаются. `.wallet--row` reset для compact-варианта
оставлен — там по-прежнему single-line+ellipsis.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: canon-стиль .wallet__title и .wallet__sub был с
`white-space: nowrap; overflow: hidden; text-overflow: ellipsis;` — на full-
варианте это резало длинные подписи («Минимальны…», «Возвращается п…»)
даже на просторных экранах, потому что grid-колонка `.wallet__main` сжимается
ради `.wallet__amount` справа. Эта обрезка будет всплывать на любой
длинной строке (метки программ, статусы пайщика, длинные subtitle).
Compact-вариант `.wallet--row` (слот шапки) должен остаться одной строкой.
What:
- .wallet__title/__sub: убран nowrap/ellipsis; добавлено
`overflow-wrap: anywhere; word-break: break-word; hyphens: auto` (для title)
и `overflow-wrap: anywhere; word-break: break-word` (для sub) — длинные
заголовки переносятся на 2+ строки.
- .wallet--row: явно возвращает nowrap+ellipsis (с reset word-break/
overflow-wrap), чтобы compact-ряды в шапке оставались строго одной строкой.
- WalletProgramWidget: карточка минимального неснижаемого остатка
перемещена в начало grid'а (была после программ) — это базовая защита
средств пайщика, логичнее видеть её первой.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why:
- Заголовок «Кошелёк» для program='wallet' путал — в столе пайщика этот
кошелёк семантически главный, и в любых других местах canon (MicroWallet,
WalletCardMini, _dev/ui) он тоже должен называться полно — «Главный
кошелёк». Лучше один canon-default, чем локальный override в каждом
потребителе. Согласовано — переименование canon DEFAULT_TITLES.
- Минимальный неснижаемый остаток — НЕ баланс кошелька, а самостоятельная
сущность пайщика (паевой взнос, возвращается при выходе). Пристегивать
его DataRow-строкой под grid'ом — нелогично, как было и раньше в legacy.
Правильнее — отдельной карточкой в той же сетке кошельков.
What:
- WalletCard.vue: DEFAULT_TITLES.wallet 'Кошелёк' → 'Главный кошелёк'.
Глобально для всех потребителей canon-компонента.
- WalletProgramWidget.vue: убран TITLE_OVERRIDE и DataRow-блок минимального
остатка. Карточка остатка теперь рендерится в общем .wallet-programs
grid'е canon-разметкой `.wallet` с иконкой `savings`, заголовком
«Минимальный неснижаемый остаток», подзаголовком «Возвращается при выходе
из кооператива» и зарезервированной суммой. Нейтральная подсветка иконки
через `.wallet--minimum { --prog-bg, --prog-fg }`, чтобы визуально
отличалась от программ, но встала в общую сетку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: на старом WalletWidget пайщик видел свой минимальный неснижаемый остаток
(паевой взнос, возвращается при выходе из кооператива) — самостоятельная
сущность пайщика, не баланс кошелька. При переходе на canon я её упустил.
Канон-default WalletCard 'wallet' = «Кошелёк» — для стола пайщика этот
кошелёк семантически является главным (свободный остаток ЦК), поэтому
локально перекрываем заголовок на «Главный кошелёк».
What:
- TITLE_OVERRIDE['wallet'] = 'Главный кошелёк' — локальное перекрытие
заголовка только в этом widget'е, canon DEFAULT_TITLES не трогаем
(другие потребители WalletCard могут использовать общий «Кошелёк»).
- DataRow «Минимальный неснижаемый остаток» под grid'ом программ
(только когда session.participantAccount.minimum_amount > 0)
с hint «Возвращается пайщику при выходе из кооператива».
- Источник остатка — session.participantAccount?.minimum_amount,
как и в legacy WalletWidget.
Note: locked-line уже рендерится самим canon WalletCard, когда
locked-balance > 0 (logic: hasBlocked ? blocked.amount : undefined
в WalletProgramWidget). Отдельная разметка под «Заблокировано» не нужна.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: type predicate `e is CanonProgramEntry` не сходился — optional `locked?: string`
в interface vs required `locked: string | undefined` в литерале map'а. TS считает
эти типы несовместимыми для predicate, хотя они эквивалентны при присваивании.
What: переход на `flatMap<CanonProgramEntry>` с возвратом `[]` для исключаемых
программ. Predicate не нужен, generic flatMap даёт точный тип результата.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: первая страница стола пайщика (default route 'wallet') использовала legacy
обвязку — CardStyles import, scoped SCSS с .body--dark и hardcoded rgba цветами,
ColorCard с произвольным цветом по индексу. Канон уже знает программы платформы
(blagorost/wallet/generator) через WalletCard + токены --prog-*; на нём и строим.
What:
- WalletPage.vue: убран import 'src/shared/ui/CardStyles', scoped SCSS на
токенах --p-6/--p-4; вырезана легаси-карточка «Минимальный остаток»
(она не отображалась — не было разметки в template). useHeaderActions
для Deposit/Withdraw оставлен — канон поддерживает actions через #actions slot.
- WalletProgramWidget.vue: переписан на canon WalletCard + EmptyState.
Фильтр на канон-набор программ через ZEUS_TO_CANON
(MAIN→wallet, BLAGOROST→blagorost, GENERATOR→generator);
MARKETPLACE и прочие исключены — это не из основной тройки платформы
и должны рендериться отдельным виджетом.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: на главную без логина нужно показывать бренд кооператива (логотип + имя)
рядом с глобальными действиями шапки; раньше выводилось только текстовое
название через title-проп без логотипа.
What:
- AppHeader.vue: опциональный slot #brand перед .topbar__crumb;
hasBrand computed по slots.brand. Когда slot заполнен — крошка не рендерится.
- components.css: стили .topbar__brand (desktop + .topbar--mobile вариант)
с canon-токенами (--p-fs-body, --p-ink, --p-fs-meta).
- CommonHeader.vue: на !loggedIn заполняет #brand src/assets/logo.svg
+ <b>{coopTitle}</b>; передача title-пропа убрана.
- default.vue: .fixed-top-right { top: 51px } → top: var(--p-topbar-h)
(canon 56px) — выравнивание FAB под точную высоту шапки.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Существующая модель cmdk в проекте (entities/CmdkMenu/model/store.ts) —
иерархия рабочих столов и их страниц. Активный стол sticky сверху с
бейджем «Активный», без запроса показывается иерархия (стол + indented
страницы), с запросом — плоский список со столом-префиксом у каждой
страницы; стол отдельной строкой появляется только если запрос явно
начинается с его имени или содержит «стол»/«workspace».
Переписал canon CommandPalette под эту модель:
- Props: `workspaces: CommandPaletteWorkspace[]` вместо
`commands: CommandItem[]`. Каждый workspace = `{ name, title, icon,
isActive?, pages: CommandPalettePage[] }`. Page = `{ name, title,
icon?, shortcut? }`.
- Emits: `select-workspace(name)`, `select-page(workspaceName, pageName)`
— props-only, навигация и filtering по ролям/conditions остаются
заботой connected-обёртки (миграция legacy CmdkMenu — отдельная story).
- Sticky-баннер активного стола, plus accent-soft фон + outline-обводка
на selected, ↑↓ работает плоско поверх иерархии (стол → страницы → стол
→ страницы).
Mock-data в /_dev/ui/index.vue обновлён: три стола (Председатель/Пайщик/
Отчётность) со своими страницами вместо плоского списка команд.
Старый widgets/Desktop/CmdkMenu пока живёт параллельно — переключим в
ходе миграции Wave 3.
Реализованы три props-only доменных компонента из E11:
- NotificationCenter — panel-content для popover в шапке: группировка
notifications по category (system/financial/voting/message), unread-bullet
через BaseBadge, кнопка «Прочитать все», EmptyState и «Показать все».
Relative-date форматирование с русским склонением.
- CommandPalette — ⌘K/Ctrl+K с fuzzy-поиском, секции recent/pages/actions,
↑↓ навигация, Enter/Esc обработка. localStorage недавних — в connected
обёртке, компонент props-only.
- DetailsDrawer — side-sheet справа 480px (override через :width), slots
default/actions/footer, Esc и backdrop close, на xs — fullscreen.
Все три зарегистрированы в boot/ui.ts и локально импортированы в /_dev/ui
с mock-data в секциях 36-38.
E11.4 RailUserCard отложен из-за конфликта имён — существующий компонент
имеет другую роль; нужно согласовать канон-нейминг.
Заменил `.full-width.text-center` + flex без выравнивания на flex-column
со `min-height: 360px` — лоадер больше не прижат к левому верху карточки.
Заодно поправил опечатку «подговка» → «Формируем документ».
Корень: я положил символ валюты в #append slot своим <span>, в обход
встроенного prop suffix. Quasar имеет правила позиционирования именно
для родного .q-field__suffix (см. revert d0a8dc0080 от 2026-05-19,
где было решено оставить Quasar дефолт — «нас устраивает»). Мой span
в append-slot не подчинялся этим правилам и сидел в произвольной
позиции относительно цифр.
Фикс:
- :suffix='symbol' — символ валюты идёт нативным механизмом Quasar.
- Удалил .amount-input__symbol класс и template #append вообще.
- Удалил override align-items: center на q-field__control/__append/
__after (тоже мешал, как было показано в d0a8dc0080).
- font-weight 500 на цифрах оставил — нормальный вес поля.
Совет на будущее: использовать встроенные q-input props (suffix,
prefix), а не #append/#prepend slots, если можно — Quasar для них
держит готовое выравнивание, проверенное пользователем.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Корень: font-weight: 600 + tabular-nums + Quasar dense нативный input
имеют чуть смещённую baseline относительно append-слота, где сидит
RUB. Визуально цифры лежали ниже суффикса.
Фикс:
- font-weight 600 → 500 (нормальный вес поля ввода, без bold-акцента
на цифрах).
- Явный font-size + line-height на нативном input.
- align-items: center на q-field__control / __append / __after, чтобы
суффикс и any after-слот (кнопка «макс») центрировались по высоте
входной полосы.
- align-self: center на самом __symbol — на случай если q-field__append
кто-то переопределит как stretch.
TODO на следующий подход: BaseDocument loader («Формируем документ…»)
выравнивать по центру рамки документа (сейчас стоит сверху).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[Quasar] boot error: SyntaxError: Unexpected identifier 'as' — runtime-парсер
обрабатывает выражения в pug-template как чистый JS, без TypeScript. Касты
вида `el as HTMLInputElement | null`, `e as InputEvent`, `e as KeyboardEvent`
прямо в атрибутах `:ref` / `@input` / `@keydown` — синтаксическая ошибка во
время бутстрапа Vue, из-за которой /_dev/ui целиком не грузился.
Фикс:
- :ref='(el) => setRef(idx, el)' + функция setRef(idx, el: Element |
ComponentPublicInstance | null) с кастом в TS-скрипте.
- @input='(e) => onInput(idx, e)' + сигнатура onInput(idx, event: Event)
с внутренним кастом target.
- @keydown оставил передавать event «как есть» — KeyboardEvent — никакой
cast не нужен.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Quasar boot-файлы подхватываются только при рестарте dev-сервера. После
коммита 1af0f9c06f глобальные регистрации AmountInput/OtpInput/FilterBar/
FileUploader/VerticalStepper не подтянулись на лету — теги рендерились как
unknown components (пусто внутри секций 31–35 на /_dev/ui).
Фикс: добавил локальные импорты прямо в script setup _dev/ui/index.vue —
тот же паттерн, что у WalletCard, RailUserCard, AuthCard. Глобальная
регистрация в boot/ui.ts остаётся для боевого использования из других
страниц (после следующего рестарта dev'а).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Пять props-only доменных компонентов из shared/ui/domain/:
- AmountInput — денежный ввод с символом валюты, форматированием тысячных,
precision из marketplace asset config, кнопкой «макс» по balance, опциональной
подписью баланса. Tabular-nums, right-align, font-weight 600.
- OtpInput — 6 ячеек с автопереходом фокуса, Backspace откатывает на
предыдущую, paste 6-значного кода распределяется по ячейкам.
Регулярка /^\d$/, состояния error/disabled.
- FilterBar — search (debounce 300мс) + dropdown-фильтры + chip'ы активных
значений с remove + «сбросить всё». v-model для values, v-model:search для
поиска. Активные chip'ы рендерятся под рядом фильтров.
- FileUploader — drag&drop + клик по зоне; валидация accept/maxSize/maxFiles
→ emit error; список загруженных с иконкой типа, именем, размером,
кнопкой ×; слот progress для connected-обёртки.
- VerticalStepper — состояния pending/current/completed/error; completed
кликабельны для возврата назад (опционально); опциональные/disabled шаги;
слот active под телом текущего шага.
Все компоненты — pug, canon-токены --p-*, без store/router/api. Демо-секции
31–35 на /_dev/ui (наш стенд-витрина).
Зарегистрированы в boot/ui.ts и shared/ui/domain/index.ts. ESLint точечно
прошёл, vue-tsc не запускался полностью на desktop (запрет).
Wave 2 / E10 закрыт.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Удалены типы 'txt' и 'image' из DocumentPreviewType + соответствующие
ветки рендера (pre.document-preview__txt и img.document-preview__image)
и стили. В реальных потоках платформы документ кооператива всегда
HTML (рендерится в ShadowHtml внутри BaseDocument) или PDF — plain-
text фрагмент в моноширинном pre не используется и выглядит как
техническая ерунда. Аналогично image.
_dev: удалён previewTxtDemo (мусорный EOSIO chain id из debugging
notes → перешитый в фрагмент протокола → теперь полностью убран
по требованию пользователя). Секция 30 показывает только HTML +
loading + error состояния.
vue-tsc:
- _dev: убрал txId/explorerUrl из signatureSignedDemo — оба поля
удалены из Signature ранее (общего эксплорера нет).
- BaseDocument: canonSignatures.map — нормализую is_valid через
'?? undefined' (бэкенд может вернуть null, canon-компонент ждёт
boolean | undefined).
DocumentPreview txt demo: вместо мусорного 'EOSIO chain id …
dirty window' (мой случайный кусок из debugging notes) — фрагмент
протокола собрания пайщиков ПК «Восход».
ComplexDocument: .col-md-7 → .col-md-10. Это контейнер, в котором
лежит BaseDocument в реальных страницах документов. 7/12 = 58%
ширины было визуально 'приплюснуто'.
Реальный формат подписей кооперативного документа — это не «pending/
signed/rejected» из абстрактного SignatureCard, а IDocumentAggregate с
полями doc_hash + signatures[] (signer_certificate, public_key,
signature, is_valid). Каждая подпись разворачивается в детали.
Что сделано:
- Новый canon-компонент DocumentSignatures (story 9.5) в shared/ui/
domain — props-only, принимает уже резолвнутые signerName и hash-
совпадение, эмитит download/verify. Поверх Quasar — собственный
expand на ref<Set<number>>, чтобы стиль был полностью canon.
- BaseDocument теперь рендерит DocumentSignatures вместо своего
q-card.verify-card + q-list + q-expansion-item на teal/red badges.
Адаптер canonSignatures маппит signer_certificate → ФИО через
getNameFromCertificate, чтобы canon-компонент не знал про сертификаты.
- SignatureCard: удалены поля txId/explorerUrl и ссылка «Открыть в
explorer» — общего эксплорера в платформе нет.
Demo: секция 29 → DocumentSignatures (валидный + с битой подписью),
секция 30 → DocumentPreview (HTML заголовок сокращён, чтобы не
обрывался при узкой колонке).
4 props-only canon-компонента в src/shared/ui/domain/, регистрация
в boot/ui.ts, demo-секции 26-29 в _dev/ui:
- DocumentRow (story 9.1) — строка документа в списке: иконка типа
с tint'ом (pdf neg-soft, docx info-soft, html primary-soft), title,
status через BaseBadge, дата/автор/описание, slot actions, emit open.
- SignatureCard (story 9.3) — подписавший (Avatar+ФИО+AccountBadge),
статус (BaseChip pending/signed/rejected), для signed — хеш в моно-
блоке + ссылка на explorer, для rejected — BaseBanner с причиной.
- ActivityTimeline (story 9.4) — вертикальный таймлайн событий с
цветными иконками по типу (sign/reject/create/update/comment/
transfer), groupByDate группирует по «Сегодня/Вчера/конкретная
дата».
- DocumentPreview (story 9.2) — html (через DOMPurify), pdf через
iframe, image через img, txt через pre. Стейты loading/error.
Все props-only, темо-зависимые значения только через --p-* токены.
Раньше compact-вариант рендерился без обрамления — avatar упирался
в левый край контейнера, визуально «проваливался» из общего стека
full-карточек (Screenshot_2026-05-21_21-51-09).
Compact теперь: padding 8/12px (тоньше чем full 16px), тот же border
+ surface + radius. В одну строку avatar + ФИО + AccountBadge + status,
но визуально это всё-таки карточка, не голая строка.
Body раньше был обычным block-контейнером, а .head и .value-row —
оба display: inline-flex. Без явного block-форматирования inline-flex
элементы становятся inline и рендерились в одну строку. Визуально:
Email⭐ivanov@example.ru, Телефон+7 (903)..., Telegram@ivanov — слипшиеся.
Body теперь display: flex + flex-direction: column, gap canon — 8px
в comfortable, 4px в compact. Head/value-row переведены на обычный
display: flex (вместо inline-flex), gap внутри сохранён.
Прецедент 2026-05-21 — Screenshot_2026-05-21_21-42-16, 21-42-31.
- AccountBadge: copy-кнопка не сжимается (flex-shrink: 0), размер 20×20 + icon 14px — раньше 18×18 + 12px тонула рядом с текстом badge.
- DataRow: column-gap var(--p-3, 12px) → var(--p-5, 20px) + min-label-width 140 → 160px + padding-right на label. Между label и value был визуально слипшийся стык.
- ContactSheet: comfortable margin-top между label и value 2px → 8px (compact 4px). Раньше label «налипал» на значение.
- IdentityPanel compact: переделан в flat flex-row (avatar + ФИО + AccountBadge + status badge) — раньше grid с .body загонял имя и AccountBadge в две строки и avatar «уезжал» от имени. Spec 8.1 требует «только avatar + ФИО + EntityIdBadge в одну строку».
5 props-only canon-компонентов в src/shared/ui/domain/, регистрация
в boot/ui.ts, demo-секции 21-25 в _dev/ui:
- AccountBadge (story 8.5) — on-chain account name в mono-шрифте,
copyable + опциональный explorer link. Назван AccountBadge вместо
EntityIdBadge: имя EntityIdBadge занято под numeric ID пайщика
в capital. Decision зафиксирован в epics.md и planning-artifacts.
- DataRow (story 8.3) — `label: value` пара для реестровых карточек,
mono-режим, copyable, slot value-override, hint.
- ContactSheet (story 8.2) — email/phone/address/tg/web с автоиконками,
кликабельными mailto:/tel:/t.me ссылками, copy и verified-чек.
- IdentityPanel (story 8.1) — Avatar + ФИО + AccountBadge + статус
(active/blocked/pending) + role + actions-slot; compact/full.
- PersonCard (story 8.4) — Avatar + ФИО + role + AccountBadge +
ContactSheet + slot meta; compact/comfortable.
Все props-only: useStore/useRouter/useApi внутри запрещены. Темо-зависимые
значения только через --p-* токены, никаких body--dark селекторов.
q-checkbox по умолчанию красит .q-checkbox__bg в --q-primary, который в
dark теме равен #2dd4bf — на маленьком квадрате 18px этот яркий бирюзовый
смотрится ядовито. Переопределяем заливку filled-состояния на
--p-primary-press (#134e4a light / #14b8a6 dark) — глубже и спокойнее,
чек-символ остаётся белым.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Старое значение --p-accent (#D84315 light / #FF7043 dark) — слишком
насыщенное, выглядит ядовито рядом с тёплой палитрой ink/canvas. Меняем
на тёплую терракоту: #B85C38 (light) / #E89472 (dark) — тот же warm-tone,
но без перевозбуждения красного канала.
.agreement-link (псевдо-ссылки на просмотр документа в согласиях) была
hardcoded в #1c64f2 (синий) light / #ff9f43 (жёлто-оранжевый) dark и не
совпадала со ссылкой «Устав кооператива». Унифицируем: все ссылки внутри
согласий теперь var(--p-accent), визиt-цвет тот же — без фиолета.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BaseCheckbox block-вариант: убран собственный display:flex + align-items:
flex-start, который перебивал внутренний flex Quasar и сдвигал label вверх
относительно inner. Теперь Quasar сам центрирует inner на первой строке.
Добавлен padding-left: 8px на .q-checkbox__label — больше воздуха между
галочкой и текстом согласия.
ReadStatement: ссылка «Устав кооператива» — --p-primary → --p-accent
(тёплый оранжевый #D84315 / #FF7043 dark) как акцентный цвет палитры.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
UserDataForm/UserDataForm.vue:
- Выбор типа аккаунта: три q-btn(glossy, height:75px) → три BaseRadioCard
с title/description; больше нет teal-«кнопок-плиток»
- Возврат к выбору типа: flat q-btn → BaseButton(ghost) с arrow_back
Sub-forms (Individual / Entrepreneur / Organization + Create*):
- q-input/q-select: standout="bg-teal text-white" → outlined color='primary'
(canon-tone, единый primary-акцент вместо устаревшего teal)
- .q-gutter-sm.q-mt-md → .user-data-stack (flex column, gap токены)
- OrganizationDataForm: кнопка «совпадает» — color='teal' → flat primary
Валидация q-form.validate() и :rules сохранены — это всё ещё q-input
с rules-массивом, только tone и контейнер переехали на canon-токены.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- BaseCheckbox: обёртка q-checkbox с canon-стилями, block-вариант для длинных
согласий (чекбокс прижат к верху, label многострочный)
- BaseRadioCard: карточка-опция с радио-индикатором справа, props title /
description / meta + slot fallback'и; используется для выбора программы
Миграции SignUp:
- ReadStatement: q-checkbox → BaseCheckbox(block); согласия в .agreements-стек
- SelectProgram: q-list+q-radio → список BaseRadioCard
- GenerateAccount: q-input → BaseInput(readonly, mono) с copy в #append;
q-checkbox → BaseCheckbox; автоселект через querySelector('input').select()
вместо ref.select() (BaseInput не пробрасывает внутренний API q-input)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Воздух над заголовком стола внутри WorkspaceSwitcher — заголовок не упирается
в верхнюю границу caption «ПК «ВОСХОД»».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- .rail__top: padding 18px 20px → 16px 8px (унификация боковых отступов)
- WorkspaceSwitcher: убраны :disabled-атрибут и hover:not(:disabled) — у пайщика, которому доступен только один стол, кнопка выглядит как обычно (без not-allowed-cursor, без приглушённого цвета). Меню q-menu просто не открывается через v-if='workspaces.length > 1'. Тихое игнорирование клика вместо явного запрета.
WorkspaceSwitcher показывал ВСЕ workspaceMenus, включая столы выше роли пользователя (например, «Стол председателя» видел рядовой пайщик), что приводило к ошибке access-denied при попытке переключения.
- Добавлена фильтрация по иерархии: chairman ⊇ member ⊇ user. Председатель видит столы любых ролей; член совета — user+member; пайщик — только user. Workspace без meta.roles (или с пустым массивом) — публичный для всех (та же логика, что в legacy WorkspaceMenu, плюс корректное наследование прав).
- Title допускает перенос на 3 строки (line-clamp 3) — «Стол вычислительных ресурсов» теперь умещается полностью
- Расширена кнопка switcher'а (margin сторон 8→4 в LeftDrawerMenu), даёт больше места под текст
WorkspaceSwitcher:
- Brand-строка теперь собирается из vars.short_abbr + «vars.name» (получается «ПК «ВОСХОД»»). Раньше показывал только short_abbr — название кооператива было «потеряно»
- Title допускает перенос на 2 строки (line-clamp 2) — длинные названия типа «Стол вычислительных ресурсов» больше не обрезаются ellipsis'ом с одного слова
- Chevron приклеен к верху строки (align-self start) — корректно при двухстрочном title
- Меню workspace: canon padding (8/12) вокруг q-item, отступ между avatar-section и текстом — иконка и название больше не упираются в края
- Затемнение фона при открытом меню (rgba(9,9,11,0.32) overlay через Teleport в body) — меню больше не сливается с контентом за drawer'ом
Bank.vue (карточка платежа):
- Убрали q-expansion-item (раскрытие вниз внутри карточки выглядело перегружено)
- Кнопка-toggle: «Показать реквизиты» рядом с «Скачать QR». Клик переключает зону под сводкой между QR и списком 9 BaseInput'ов с copy-кнопками
- В режиме реквизитов: кнопки меняются на «Скопировать всё» (primary) + «Показать QR» (secondary)
- QR-canvas использует v-show чтобы сохранять состояние при toggle назад; реквизиты — v-if чтобы не держать BaseInput'ы в DOM
ReadStatement: backend-HTML генерирует с inline-стилями и Quasar text-h* классами, которые перебивали :deep правила. Усилили все ключевые селекторы !important + добавили охват .text-h1/h2/h3/h4/h5/h6 и .text-right (под мета-блок «УТВЕРЖДЕНО…»).
Bank.vue (карточка платежа):
- убрали отдельные surface-карточки вокруг сводки и QR (получалось «карточка-в-карточке-в-карточке»)
- сводка → плоский <dl> со строкой-разделителем снизу (border-bottom var(--p-line))
- QR → просто центрированный canvas без обёртки/фона/рамки; белый цвет внутри canvas — функциональное требование контрастного сканирования, не дизайн-выбор
- detail-expansion → без своего фона, только border-top/bottom
WorkspaceSwitcher (новый widget src/widgets/Desktop/WorkspaceSwitcher/):
- Карточка-кнопка в шапке drawer: иконка кооп + caption «ПК «ВОСХОД»» + bold «Стол совета» (текущий) + chevron
- Клик → q-menu со списком всех workspaceMenus (Стол пайщика, Стол совета, Стол председателя и т.д.); выбор → selectWorkspace + goToDefaultPage
- Подключён через #brand slot AppDrawer'а в LeftDrawerMenu, заменяет дефолтный brand-row
ReadStatement (этап 6.1):
- Backend генерирует HTML документа — локально нормализуем через :deep
- h1 уменьшен с монструозного дефолта до canon h3 (20px), центрирован
- h2/h3, p, strong, ul/ol, table, hr — типографика по var(--p-*)
- мета-блок .approved/.meta-right (УТВЕРЖДЕНО…) — ink-2, мельче, плотнее
- Чекбоксы соглашений снизу не трогали (как просил пользователь)
Bank (этап 6.2 — карточка платежа PayWithProvider):
- QR-код вынесен в фокус: сначала сводка (получатель/сумма/назначение) → QR → действия → реквизиты для ручного перевода скрыты под q-expansion-item
- Полные 9 полей (ИНН/БИК/КПП/корр-счёт/...) переведены на BaseInput(readonly, mono) с copy-кнопкой в #append; раскрываются только при необходимости
- Кнопки «Скачать QR» (primary) и «Скопировать реквизиты» (secondary) — BaseButton
- Удалены: q-btn(push), case-mixed «скопировать реквизиты», стиль #qr через global селектор
- .signature-container: 3px primary border + 3-слойный box-shadow glow → 1px dashed по var(--p-line-2), surface-2 фон, без свечения; на hover лишь окрашивается рамка
- .signature-hint: переехала в центр контейнера, canon ink-2 цвет, убран frosted-chip с blur/shadow — теперь спокойная метка «Оставьте собственноручную подпись в рамке», pointer-events: none пропускает клики на canvas
- min-height снижен 300→220, padding на canon-токены
- SignUp.vue: добавлена обёртка .signup-page (padding + center + min-height) — как у SignInPage; раньше страница была прижата к шапке
- AuthCard maxWidth: 1000 → 720 — историческое 1000 для трёх-колоночной UserDataForm; для остальных шагов выглядит абсурдно широко
- q-stepper: убраны собственный фон/тень/padding (не «карточка в карточке»), линии-разделители переведены на var(--p-line)
- EmailInput: max-width поля 360px — email не должен растягиваться во всю ширину карточки
- canon `shared/ui/domain/AuthCard` переписан на pug
- SignUp.vue переключён со старого «глянцевого» AuthCard на canon (title prop вместо CAPS-заголовка)
- старый `shared/ui/AuthCard` удалён (использовался только в SignUp)
- 7 подэкранов SignUp (EmailInput, SetUserData, SelectProgram, GenerateAccount, SelectBranch, ReadStatement, SignStatement): q-btn → BaseButton (ghost для «назад», primary для «Продолжить»)
- EmailInput: q-input → BaseInput, валидация rules переведена на computed :error
- q-input в GenerateAccount оставлен из-за зависимости от ref.select()
- og-image.png — взят brand-постер ~/blago/production/shared/poster-logo-horizontal.png,
отресайзен до 1000px по ширине и центрирован на полотне 1200×630
с белыми краями по вертикали. Шрифт/композиция родные бренда.
- description / og:description / twitter:description: «Цифровой Кооператив —
система управления хозяйством. Регистрация пайщиков, заказы на поставку
и приобретение имущества, собрания совета, общие собрания пайщиков,
взаимные расчёты на блокчейне, автоматический документооборот и бухбаланс
на основе простой электронной подписи.»
- og-image.png перерисован: «Цифровой Кооператив» / «Система управления
хозяйством» / «на платформе БЛАГО — кооперативной экономики для жизни» /
«Регистрация пайщиков · заказы на поставку и приобретение имущества ·
расчёты на блокчейне».
- description / og:description / twitter:description выровнены под этот
же копирайт. Слово «взаимоотношения с пайщиками» убрано как слишком
узкое — управление хозяйством включает в себя ещё заказы, реестры
и документы.
- src/assets/logo.svg, public/favicon.svg, public/logo.svg — единая SVG
логотипа кооператива (path с «ц» внутри). Скачано с
https://цифровой-кооператив.рф из шапки сайта.
- src/assets/logo.svg: fill -> currentColor чтобы лого внутри
canon .rail__brand наследовал --p-primary (для тёмной темы тоже).
- LeftDrawerMenu подключает лого через slot #brand-icon AppDrawer
(импорт ?raw + v-html). Внутри зелёного квадрата шапки рейла теперь
наш системный знак, а не дефолтный q-icon "dashboard".
- index.html: favicon → /favicon.svg, apple-touch-icon тоже на SVG.
Удалены неработающие ссылки на /favicon.ico и /apple-touch-icon-*.png.
- index.html: добавлены description + расширенные og:* и twitter:*
с конкретным описанием и og-image (1200×630), чтобы ссылки в чатах
выглядели прилично.
- public/og-image.png — превью 1200×630, ImageMagick-сгенерированный
композит из логотипа + текста.
- Удалены неиспользуемые ассеты:
- public/icons/, public/pwa/ — старые favicon/PWA-PNG'и всех размеров;
- src/assets/* — 40 легаси SVG/PNG (anime, blockchain, dacom*, flow*,
header-logo, quasar-logo-vertical, system*, club*, welcome.jpeg и т.д.).
Оставлены только src/assets/pin.svg (используется в Map.vue) и
src/assets/logo.svg (новый).
Pug-template передаёт выражения в Vue compiler как строки; конструкции
вида (entry as RailItem).route не превращаются в JS и падают
SyntaxError в браузере: «Unexpected identifier 'as'».
Касты унесены в <script>: добавлены computed flatItems и
normalizedGroups, шаблон работает с уже типизированными массивами без
inline-TS.
- LeftDrawerMenu: импорт useCmdkStore → useCmdkMenuStore (правильное имя).
Без этого ломался ESM build всего drawer'а: «no exported member
useCmdkStore» — RailUserCard не монтировался, поэтому и chevron свёртки
«не работал», и Найти/Пополнить не реагировали.
- AppDrawer.vue, AppHeader.vue, RailUserCard.vue переведены на pug
(lang="pug"). В проекте везде pug — никакого исторического HTML в
canon-компонентах быть не должно.
- q-drawer width 240 → 248px (совпадает с canon --p-rail-w, кошелёк больше
не уплывает справа из-за обрезки на 8px)
- LeftDrawerMenu: убран весь :deep override на .rail__usercard/.rail__signout,
возвращаем canon margin/padding как есть
- LeftDrawerMenu: добавлен v-model:collapsed на RailUserCard с сохранением
в localStorage — chevron «свернуть/развернуть» снова работает
- LeftDrawerMenu: кнопка Найти теперь дёргает cmdkStore.openDialog() напрямую
(фейковый KeyboardEvent не доходил до глобального обработчика)
- LeftDrawerMenu: «Пополнить» → useDepositDialog().open(); DepositButton +
WithdrawButton рендерятся скрыто как держатели q-dialog (q-portal в body)
- Header.vue и LeftDrawerMenu.vue переписаны на pug (как остальной проект)
- AppDrawer: плоский items оборачивается в .rail__nav, появляется gap 4-8px до cmdk
- cmdk margin 16→8px, чтоб выровнять с rail__nav (8px от стенки)
- LeftDrawerMenu footer: WalletCardMini+LogoutButton → один canon RailUserCard
(avatar+balance в primary-soft + Пополнить + встроенный signout)
- :deep override на .rail__usercard margin 16→12/8 и .rail__signout padding 12/20→12/12
- quasar-canon: body--light/dark + q-layout/q-page-container/q-page красятся
через --p-canvas, чтобы фон страницы соответствовал шапке и рейлу
LeftDrawerMenu:
- legacy `MicroWallet` (ColorCard teal с большой карточкой пайщика + ИП-badge
+ Deposit/Withdraw micro-кнопки) → canon `WalletCardMini` из
widgets/wallet-card-mini (тонкая карточка из shared/ui/domain/WalletCard
compact-variant, читает walletStore сама).
- Убран toggle «свернуть/развернуть нижнюю секцию» — нижняя секция теперь
всегда видна (короткий компактный блок: WalletCardMini + Выйти).
- Удалены неиспользуемые `onMounted`/`ref` импорты, slide-анимация и CSS
toggle-кнопки.
LogoutButton:
- pug + q-item с красной полупрозрачной плашкой → плоский ghost-button в
стиле rail-пункта AppDrawer: прозрачный фон в покое, на hover —
`--p-neg-soft` фон + `--p-neg` текст/иконка.
- Из uppercase «ВЫЙТИ» в title-case «Выйти» (canon — никаких uppercase).
Визуально левый drawer теперь полностью в canon-семье: единый rail с
пунктами + поиском + компактным wallet-балансом + ghost-кнопкой выхода.
Закрывает этап 3 из 5 в волне «приземление DS на реальный layout».
Дальше — SignUp (этап 4) и Invite (этап 5).
widgets/Header/CommonHeader/Header.vue теперь использует canon AppHeader
из shared/ui/layout. Высота шапки 56px (--p-topbar-h), border-bottom
hairline, без shadow и фоновых градиентов.
Слоты:
- #crumb: BackButton (для авторизованных), либо title с названием
кооператива из system.info.vars для гостей/install.
- #actions: headerActions injection через useHeaderActionsReader (всё что
страницы инжектят — Deposit/Withdraw, SettingsDropdown и пр. — рендерим
как было).
- #notifications: NotificationCenter (только loggedIn + isClient).
- #theme: ToogleDarkLight — пока legacy, сохраняет storage + PWA-цвет;
наш canon ThemeToggle их не делает (привяжу позже отдельным эпиком).
- #profile: BaseButton primary для гостей — Регистрация/Вход (в зависимости
от текущего route).
Удалены:
- MainHeader.vue (вся логика scroll arrows + carousel actions group ушла —
canon `.topbar__actions { gap: 8px }` достаточно; overflow-scroll
будет вернут отдельно если потребуется на узких экранах).
- HeaderStyles.scss (стили scroll-arrow и q-toolbar overrides больше
не нужны — canon-разметка через `<header class="topbar">`).
q-header Quasar остаётся wrapper'ом (sticky-poзиционирование в q-layout),
внутри — AppHeader даёт canon-разметку.
Переписан widgets/Desktop/LeftDrawerMenu/LeftDrawerMenu.vue: внутри теперь
canon `AppDrawer` из shared/ui/layout с rail-видом MONO v2 (логотип ПК +
короткое имя кооператива в шапке рейла, ⌘K-поиск, пункты-router-link'и
с иконкой/бэйджем, sticky-footer).
Адаптер store→canon:
- items: desktop.activeSecondLevelRoutes → RailItem[], с прежней
фильтрацией по roles + meta.conditions + meta.hidden (один-к-одному
логика из SecondLevelMenuList).
- activeKey: вычисляется из router.currentRoute с поддержкой группового
паттерна project-* → projects-list.
- @select: router.push({name, params:{coopname}}) или
actionsStore.executeAction(meta.action), закрытие drawer на mobile.
- @cmdk: эмиттим keyboard event ⌘K → CmdkMenu (он смонтирован глобально
в default.vue) подхватывает.
Footer-слот: пока сохранён legacy MicroWallet + LogoutButton + toggle
свернуть/развернуть (заменим на WalletCardMini в этапе 3).
default.vue: q-drawer width 200 → 240 (canon-ширина rail'а).
Что НЕ затронуто этим коммитом: верхняя шапка (legacy MainHeader),
правый drawer для actions, q-footer для гостей. Их меняем отдельными
этапами 2–3.
Корневая причина «оторванной» галочки в ResetKeyForm: я ранее задал в
mono-platform/components.css глобальное `.row { display: flex;
align-items: center; gap: 12px; }`. Quasar внутри `q-checkbox` (а также
q-radio, q-toggle, q-btn-toggle и др.) сам добавляет class="row" на
корень компонента и использует его как inline-flex — мой `gap: 12px`
раздвигал `.q-checkbox__inner` и `.q-checkbox__label` на 12px по всему
приложению.
Решение:
- Переименовать утилиту `.row` → `.u-row` (та же семантика, тот же набор
модификаторов --wrap, --gap-2/4/6). У Quasar теперь свободный .row.
- Обновить все 6 использований в /_dev/ui.
- Откатить предыдущий :deep(.q-checkbox__label) padding-left:4px workaround
в ResetKeyForm — лечил симптом, причина устранена.
Затрагивает только нашу dev-витрину и canon-utility; legacy pug-виджеты
с `.row.justify-center` продолжают работать через стандартный Quasar
flex-grid класс.
Quasar по умолчанию даёт ~12–16px padding на .q-checkbox__label, в
compact-форме сохранения ключа это смотрелось «оторвано». Уменьшаем до 4px
через :deep override на классе .rk-form__confirm.
Три кадра в одной секции: LostKey (ввод email), ResetKeyForm check-mail
(уведомление «письмо отправлено»), ResetKeyForm save-key (демонстрация
сохранения ключа с моковым сгенерированным аккаунтом). Submit на третьем
кадре регенерирует мок-ключ для повторной проверки.
Заменяет ошибочную секцию ChangeKey из предыдущей итерации.
ChangeKey-фича (формы current/new/confirm WIF + успешный диалог) была
ошибочной интерпретацией E7. Реальный flow «сменить ключ» в продукте —
это **двухэтапный ResetKey**:
1. LostKey: пайщик вводит email (он потерял ключ — текущего WIF нет),
бэк присылает письмо со ссылкой `?token=...`.
2. ResetKey: при заходе по ссылке клиент сам генерирует новый ключ
прямо в браузере (через `useCreateUser().generateAccount()`),
показывает приватный ключ readonly с «Скопировать», требует
подтверждения «Я сохранил ключ» и вызывает on-chain
`resetKey({token, public_key})`.
Текущий ключ ввести нельзя (его нет). Новый ключ ввести нельзя (только
генерируется). Никакого подтверждения нового ключа — он один и тот же.
Что сделано:
- Удалена ошибочная `features/User/ChangeKey/`.
- Старый `widgets/Registrator/ResetKey/ui/ResetKey.vue` (pug + сырые
q-input/q-checkbox + кастомные градиентные стили) разбит на:
* `ResetKeyForm.vue` — presentation (props-driven, режимы
`check-mail` / `save-key`), без useRouter/useApi — пригоден для
/_dev/ui витрины.
* `ResetKey.vue` — connected обёртка, читает `route.query.token`,
генерит account через `useCreateUser`, диспатчит `resetKey` через
`useResetKey`, на успех → router.push на signin.
- Новая UI собрана на BaseForm + BaseInput readonly mono + BaseButton +
BaseBanner + AuthCard.
/_dev/ui секция 20 теперь показывает три кадра подряд: LostKey (email
шаг), ResetKey check-mail, ResetKey save-key с моковым сгенерированным
ключом.
FR11, UX-DR9, UX-DR15.
Generic-проброс `<template v-for="(_, slotName) in $slots" #[slotName]>`
рушится в Quasar 2.19 рендере: при наличии scoped-слота #append вылетает
`Cannot read properties of null (reading 'key')` в renderSlot + Vue warns
по `_isVue`/`constructor` свойствам. Заменяем на явный список слотов
(prepend / append / before / after / hint у q-input; +option, selected-item
у q-select) — синтаксис, который Quasar держит корректно.
Кейс всплыл на ChangeKeyForm: #append с q-icon (toggle visibility) ронял
страницу /_dev/ui целиком.
Подключение ChangeKeyForm + ChangeKeySuccessDialog к dev-витрине.
Submit запускает заглушку useChangeKey().changeKey() (≈600 мс), по успеху
открывает диалог с новым WIF. Generate генерирует валидный K1 ключ через
SDK и заполняет оба новых поля. Confirmed сбрасывает форму.
Диалог успеха смены ключа: новый WIF в моноширинном блоке с copy-кнопкой,
BaseDialog с closeOnBackdrop=false, closeOnEscape=false, hideCloseButton=true.
Закрытие только через primary-кнопку «Я сохранил ключ»; до копирования
кнопка показывает guard-баннер вместо немедленного закрытия. По confirmed
эмитим событие — реальный flow подключит router.push на дашборд.
FR11, UX-DR11, UX-DR15. Story 7.2 из epics.md.
Форма смены ключа пайщика на новых компонентах дизайн-системы.
BaseBanner severity=warn с предупреждением о потере, checklist последствий,
поля current/new/confirm WIF моноширинные с toggle visibility, ghost-кнопка
«Сгенерировать» через PrivateKey.generate из @wharfkit/session.
Бизнес-логика — заглушка с TODO на привязку on-chain change-key action
(pattern см. features/User/ResetKey/api) и TODO на OtpInput шаг подтверждения
(приходит в Эпике 10).
FR11, UX-DR9, UX-DR15. Story 7.1 из epics.md.
Bug после rename --p-accent → --p-primary: класс .avatar--accent смотрел
на --p-primary-soft (canon teal), а должен был — на --p-accent-soft
(warm orange по новой семантике). И вариант ink (белый bg + dark text)
визуально выпадал из палитры — крайний xl-аватар на демо-витрине казался
«не пришей к звезде рукав».
Новая семантика AvatarTone:
- neutral (default) — серый surface-3 + ink-1 (нейтральный)
- primary — canon-teal soft + canon-teal text (основной выделение)
- accent — warm-orange soft + warm-orange text (редкое выделение,
например подсветка пайщика по batch_id)
Удалён вариант ink — не использовался в реальном коде.
В dev-витрине xl-аватар: tone="ink" → tone="primary".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Override (padding-top:0 + align-self:center на dense) не отрабатывал —
Quasar в dense держит prefix/suffix по baseline вводимого значения,
и пользователю это устраивает. Убираем неработающий override, чтобы
не мешался в дальнейших правках.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Inline `style="margin:0"` на <p>-описании перебивал мой margin-bottom
через CSS (специфичность inline > selector). Переключаюсь на
`> :first-child + *` margin-top — отступ крепится ко второму ребёнку,
не зависит от inline-стилей первого.
Работает универсально: <p> + <BaseInput>, <p> + <BaseForm>, любая
другая пара «описание + контент».
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. --p-accent (canon token): #D84315 (light) / #FF7043 (dark).
Совпадает с legacy Quasar $accent (warm orange, Material Deep Orange).
Проброшен в --q-accent через CSS var — legacy color="accent",
var(--q-accent), text-accent продолжают работать без изменений,
но теперь следуют за canon-темой (light/dark). Резерв под редкое
выделение: ссылки batch_id и т.п.
2. quasar-canon.css: .q-field--dense .q-field__suffix/prefix —
padding-top:0 + align-self:center. Quasar по умолчанию ставит
padding-top: 24px под floating-label, отчего «RUB» визуально
опускался к низу control'а. Теперь в одну линию со значением.
3. BaseForm: убран gap 12px из .base-form__body — reserve-hint-space
у q-input даёт ~24px снизу под error/hint, дополнительный gap
делал расстояние между инпутами неприятно большим.
4. BaseDialog: убран универсальный gap из body, оставлен margin-bottom
только на первом <p>/.intro — между описанием и первым инпутом
воздух нужен, между остальными детьми reserve-hint-space хватает.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Промахнулся `git add -A` — захватил worktree-symlink node_modules
(он pnpm-shared с основным репо). .gitignore покрывает node_modules/
но симлинк-файл — отдельный путь.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Контекст: после проброса --q-primary = var(--p-primary) (4fddfe2343)
наш токен --p-accent и Quasar primary стали обозначать ОДИН и тот же
canon-teal — два имени для одного цвета это когнитивный шум, и при
росте платформы будет путать. Имя `accent` оставляем свободным под
будущий ВТОРОЙ цвет (warm-orange, для редкого выделения вроде ссылок
по batch_id) — это отдельная задача когда понадобится.
Переименовано везде (109 ссылок в 6 файлах):
- --p-accent → --p-primary
- --p-accent-hover → --p-primary-hover
- --p-accent-press → --p-primary-press
- --p-accent-soft → --p-primary-soft
- --p-accent-line → --p-primary-line
- --p-accent-strong → --p-primary-strong
- --p-ink-on-accent → --p-ink-on-primary
Файлы: tokens.css, quasar-canon.css, components.css, RailUserCard,
AuthCard, _dev/ui (включая user-facing labels палитры «Accent → Primary»
и заголовок секции «Акцент → Основной цвет»).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- tokens.css: проброс canon-палитры в Quasar brand-переменные
(--q-primary/negative/positive/warning/info = соответствующие
--p-* токены). Резолвится lazy → автоматически следует за темой.
Теперь Quasar САМ красит focus q-field, q-btn color="primary",
bg-primary utility-классы в canon-цвета без CSS-overrides;
- quasar-canon.css: убраны border-color overrides на ::before
(default/focus/error) — Quasar управляет цветом через --q-primary
и собственный механизм --focused/--highlighted. Оставлены только
background, text-color и rounded углы (border-radius var(--p-r-sm));
- BaseInput/Select: вернули `dense` (компактный 40px) и явный
`color="primary"` (canon teal через --q-primary).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Между нижним инпутом и footer-кнопками был ~32px (q-card-section
padding-bottom 20 + q-card-actions padding-top 12) — выглядело
растянуто. Сводим к 16px (8+8) — кнопки сидят ближе к контенту.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Регрессия мерцания границы и приплюснутости — следствие борьбы наших
overrides с встроенной Quasar анимацией. Возвращаем дефолт: Quasar
сам решает геометрию (height/padding/border-radius/font), анимации,
opacity ::before и focus-индикацию. Наша задача — только покрасить
в canon-цвета.
Удалено из .q-field--outlined:
- height/padding/border-radius/font-size/font-family overrides;
- transition border-color (Quasar сам делает свой transition);
- opacity:1 !important на ::before (был костыль от мерцания);
- box-shadow:none на --focused/--highlighted;
- display:none на ::after underline;
- override .q-field--dense;
- override .q-field--float .q-field__label (Quasar анимирует label сам);
- focus-shadow / outline reset на native input.
Оставлено (только цвет):
- background: var(--p-surface);
- border-color: line-1 / accent (focus) / neg (error);
- color текста, placeholder, label, prefix/suffix, hint/error.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Quasar при focus анимирует opacity ::before (прячет старую границу
перед показом ::after underline). Так как ::after мы спрятали,
получался видимый gap ~200-400 ms «граница исчезла → появилась
с подсветкой». Фикс: opacity:1 !important на ::before,
transition только по border-color.
- BaseTable.vue: убран неиспользуемый импорт BaseTableColumn.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- BaseInput/BaseSelect: убран `dense` — возвращаемся к дефолтному
outlined Quasar (control ~56px). Floating-label получает достаточно
воздуха, текст значения визуально центрируется без сжатия;
- BaseDialog: `.base-dialog__body` теперь flex-column с gap 16px —
описание/инпуты/таблицы внутри диалога получают единый воздух
без правки потребителя (пример: «Паевой взнос: укажите сумму…» →
отступ до инпута появляется автоматически);
- BaseForm: `.base-form__body` gap 4px → 12px — те же требования
внутри формы;
- WalletCard: `.wallet__metric-val` 19→17px — итоговое значение
для аккуратного баланс-блока FULL.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. Stack-label: BaseInput/BaseSelect → только `dense`. Quasar
стандартно сам корректно держит floating-label в dense+outlined,
без CSS-override метка работает по документации Quasar.
2. Двойная focus-подсветка: убран наш box-shadow var(--p-focus-ring)
поверх Quasar внутренней focus-индикации (виден был как «белый
ринг» на тёмной + наш teal). Теперь только border-color меняется
через ::before (canon-минимализм). Дополнительно `box-shadow: none
!important` на --focused/--highlighted control и `display:none` на
::after underline (на случай dense Quasar variants).
3. Mini-wallet в правом верхнем углу AppHeader: слот #wallet удалён
из API (баланс уже отображается в RailUserCard в drawer'е,
дубль в шапке избыточен). Убран из dev-витрины каркаса.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Регрессия после Quasar wrapper-pivot: метрики кошельков выглядели
крупно по сравнению с заголовком/sub-label, а padding'и были
избыточны для compact-варианта.
- .wallet (FULL): padding 18→14px по верт, иконка 44→40px,
metric-val 22→19px, ccy 13→12px;
- .wallet--row (compact): padding 14→10px, иконка 36→32px,
metric-val 18→16px — выглядит как row в списке, а не как карточка;
- .rail__balance-val (mini-wallet в drawer): 22→18px — теперь не
доминирует над identity-блоком пайщика.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Корень регрессии: жёсткий height: 40px в .q-field--outlined раздавил
вертикальную разметку q-input (Quasar держит padding-top:24px под
floating-label слот; при сжатии текст значения сдвигается). Те же
эффекты для select.
- BaseInput/BaseSelect → dense + stack-label по умолчанию;
Quasar сам корректно держит 40px в dense, метка статически стоит
над инпутом без floating-механики;
- .q-field--outlined: убран принудительный height; .q-field--dense
получает только padding-x, высоту контролирует Quasar;
- .q-btn: font-weight 500→450, min-height 40→36px, padding 16→14px —
кнопки больше не выглядят «жирно/тяжело»;
- .q-btn--outline: hairline-граница через ::before (Quasar pattern),
без двойной рамки;
- размеры sm/lg/dense пропорционально уменьшены;
- .q-chip: высота 24→26px, padding 10→12px, font-weight 500→450;
+ .q-chip__icon размер 14px (для точечных чипов).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BaseSelect → q-select (outlined, reserve-hint-space, map-options/emit-value
для совместимости с {value,label} options API).
BaseCard → q-card flat с q-card-section head/body. Variants flat/inset/quiet
override-ят через scoped style.
BaseChip → q-chip с color mapping (variant → primary/positive/negative/
warning/info). Square для canon-look.
BaseBadge → q-badge с тем же color mapping. Dot-вариант через scoped style
(8×8 круг).
BaseBanner → q-banner rounded dense с border-left по variant.
BaseDialog → q-dialog + q-card с size→maxWidth mapping. closeOnBackdrop/
closeOnEscape пробрасываются через :persistent / :no-*-dismiss.
BaseTable → q-table с трансформацией columns в QTableProps['columns'].
Hide-pagination, binary-state-sort. Cell-slots работают через body-cell-{key}.
AuthCard (domain) — был на native canon `.card` div. Переведён на q-card flat
для консистентности. Усиленный shadow + accent-stripe сверху сохранены.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BaseInput теперь рендерит <q-input outlined reserve-hint-space no-error-icon>
с проброшенными slots. reserve-hint-space от Quasar резервирует место
под error/hint встроенно — раньше делали вручную через NBSP. Все Quasar
features (rules через v-bind \$attrs, mask, ref API, native slots) работают
прозрачно. Props API совместим (modelValue, label, hint, error, prefix,
suffix, mono, readonly, disabled, autocomplete, name, id, type).
BaseButton — q-btn с variant→Quasar mapping:
primary → :unelevated color="primary" (заливка accent)
secondary → :outline (border + surface)
ghost → :flat (transparent)
danger → :flat color="negative" (transparent + neg-cвет)
:no-caps :ripple=false для canon-look. Props API совместим.
BaseForm — q-form-обёртка с сводным error-баннером (q-banner) и slot footer.
banner—neg styling прилетает из quasar-canon.css через .bg-negative-soft.
Глобальная стилизация Quasar в quasar-canon.css даёт canon-look всем
этим компонентам и существующим q-input/q-btn/q-card по всему проекту.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Архитектурный pivot: вместо native HTML5-компонентов под canon-классами
стилизуем Quasar поверх. Причины:
- Сохраняется весь Quasar API из коробки (rules/mask/dense/slots/refs)
- Существующий код (сотни q-input, q-btn, q-card, q-select) автоматом
получает canon-look без правок
- Меньше регрессий — Quasar отвечает за accessibility/keyboard/focus
Файл src/css/mono-platform/quasar-canon.css содержит overrides под canon
для: q-field--outlined (q-input/q-select), q-btn (unelevated/outline/flat),
q-card, q-chip, q-badge, q-banner, q-dialog/q-menu, q-item, q-table,
q-checkbox/q-toggle.
Подключен в quasar.config.cjs css[] после components.css. tokens.css
без изменений — все цвета/радиусы/тени тянутся как переменные.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Истинный источник «прыгалки» AuthCard на hover: глобальное правило
`.card:hover { transform: scale(1.03) }` в src/app/styles/style.css
(и его дубль в pages/Marketplace/MainPage/ui/MainPage.vue, тоже не scoped)
бьёт по любому элементу с классом card. AuthCard имеет корневой
<div class="card auth-card"> — каждый hover дёргает её на 3%.
Поиск показал: класс `card` без модификаторов в templates никто кроме
новых canon-компонентов (BaseCard, AuthCard) не использует. Этот глобал —
мёртвый legacy, удаляем безопасно.
NBSP-фикс layout-shift из предыдущего коммита (2a2d9a786a) остаётся
актуальным — он лечил другую (на уровне поля) проблему.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BaseInput: контейнер field__message теперь рендерится всегда с min-height
calc(--p-fs-meta * 1.4) — раньше div появлялся/исчезал на каждом keystroke
при валидации (см. LostKey emailError), форма прыгала вверх-вниз.
Когда нет ни ошибки, ни хинта — рендерится NBSP, высота сохраняется.
AuthCard: добавлен box-shadow поверх canon-минималистского .card —
canvas #fafafa и surface #ffffff на светлой почти неразличимы, без тени
карточка выглядит плоской. На тёмной отдельный shadow усилен (поверх
canon `--p-shadow-card`, который оптимизирован под data-cards, не hero).
border-color подкручен с --p-line на --p-line-1 для большей чёткости.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Scope-adjustment: plan'овая Story 6.3 предполагала миграцию SignUp как
второго auth-flow, но Register — multi-step wizard (10+ файлов в
pages/Registrator/SignUp/), который требует VerticalStepper из Эпика 10
и не помещается в Волну 1. Вместо этого мигрируем LostKey — простую
одно-полевую auth-страницу, того же auth-домена.
LostKey (widget): pug → html, старый AuthCard (q-card с shimmer) →
canon AuthCard. q-input → BaseInput с inline email-валидацией;
q-btn → BaseButton; всё обёрнуто в BaseForm с loading/error.
Бизнес-логика (useLostKey.startResetKey + router redirect на resetkey)
сохранена 1:1.
LostKeyPage: footer-кнопка «Назад» через ghost BaseButton в footer-slot
AuthCard; центровка через flexbox.
Не мигрировано в Волне 1 (известный gap):
- SignUp wizard (требует VerticalStepper из E10)
- ResetKey / Invite (на старом AuthCard, мигрируются в Волне 2)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
LoginForm (feature): q-input → BaseInput, q-btn → BaseButton, обёрнут в
BaseForm с управлением loading/error. Бизнес-логика (useLoginUser, redirect,
OpenReplay tracking, NotificationPermissionDialog) сохранена 1:1.
Ошибки входа теперь показываются через сводный error BaseForm
(плюс legacy FailAlert остаётся как fallback).
SignIn (widget): импорт AuthCard переключён на новый canon-композит
(src/shared/ui/domain/AuthCard), убран pug-template и градиентные стили.
Добавлен footer-slot — пробрасывается в AuthCard.
SignInPage: переписан с pug → html, footer-кнопки «Потеряли ключ?» /
«Нет аккаунта?» вынесены в footer-slot AuthCard через BaseButton ghost-sm.
Центровка через flexbox вместо QRow grid.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AuthCard (src/shared/ui/domain/AuthCard) — canon .card + center-aligned
head (title/subtitle/slot head) + body-slot + опциональный footer-slot.
Старый src/shared/ui/AuthCard (q-card с shimmer-градиентом) остаётся
для не-мигрированных страниц (SignUp wizard, ResetKey, Invite).
AuthLayout (src/app/layouts/AuthLayout.vue) — wrapper-обёртка для full-page
auth-страниц: фон var(--p-canvas), центрирование, тогл темы в правом верхнем.
Витрина: dev-секция 19 с примером login-формы (BaseForm + BaseInput ×2 +
BaseButton + footer-ссылки).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
WalletCardMini подключает WalletCard к Pinia (useWalletStore.program_wallets) +
System store (info.symbols.root_govern_symbol) + useRouter (клик →
name: 'wallet'). Программный mapping: wallet→MAIN, blagorost→BLAGOROST,
generator→GENERATOR (Zeus.ProgramType). Loading определяется автоматически
по наличию данных в store (или пробрасывается через prop).
Подключено в слот wallet AppHeader на dev-витрине (секция 16 — каркас) +
выделенная секция 18 с тремя программами. На dev-странице покажет
skeleton-loading т.к. loadUserWallet не вызывался; на реальных страницах
после init процесса — настоящий баланс.
Со-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Благорост — капитализация РИД (не «накопления на жильё»), Генератор —
генерация РИД (не «доходы от программы»). Кошелёк остаётся «свободный
остаток». Черновые ярлыки риском попадают в production через копипасту.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Канонический .wallet с компактным (--row) и full-вариантами, программные
акценты через --prog-blagorost / --prog-wallet / --prog-generator (UX-DR20).
Витрина на /_dev/ui секция 17: три программы × оба варианта + loading +
empty + locked-line.
Compact (.wallet--row): 36px icon, 18px sum, 14px padding — для слота шапки.
Full (.wallet): 44px icon, 22px sum, 18px padding — для дашборда.
Connected-обёртка widgets/wallet-card-mini (Story 5.2) делается отдельным
коммитом, требует интеграции с реальным wallet-store и MARKETPLACE_ASSET_CONFIG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Анимация max-height + opacity при свёртке давала видимый прыжок крупной
суммы баланса (`font-size: 22px` + `display: flex; align-items: baseline`):
во время промежуточных кадров baseline переcчитывался относительно
схлопывающегося контейнера → текст «прыгал в середину» и обратно.
Возвращаемся к canon `display: none`. Resize моментальный, без skipping.
Поведение rail (top опускается, bottom-chevron остаётся через flex-spacer)
работает и без анимации — пользователь это и подтвердил в фидбеке.
Chevron rotation через :deep(.q-icon) и balance-route не трогаем —
они независимы.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. **Палитра системы.** Новая секция «00» в шапке dev-страницы со всеми
основными --p-* токенами:
- Акцент (accent / hover / press)
- Поверхности (canvas / canvas-2 / surface / 2 / 3)
- Текст (ink / ink-on-accent)
- Линии (line / 1 / 2) — текстовый input для rgba
- Статусы (pos / neg / warn / info)
- Программные тинты (blagorost / wallet / generator)
Color-picker для hex-токенов, текстовый ввод для rgba. Live-override
через `document.documentElement.style.setProperty()` — inline-style на
:root перебивает CSS-правила, перекрашивание мгновенное. Подсветка
переопределённых токенов + счётчик в шапке + кнопка «Сбросить» (через
removeProperty). Watch на Quasar Dark — при смене темы пикеры
обновляются на новые resolved-значения.
2. **Chevron в свёрнутом виде — full-width.** Из-за того что для плавной
анимации primary остаётся в DOM (max-height:0 вместо display:none),
у него сохранялся `flex: 1` и он съедал ширину карточки → chevron не
мог растянуться через canon-правило `width: 100%`. В `.is-collapsed`
добавили `flex: 0 0 0` + `width: 0` для primary — chevron теперь
корректно занимает всю ширину.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
По фидбеку:
1. **Chevron не переворачивается.** Canon-правило
`.rail__usercard__collapse svg { transform: rotate(180deg) }` рассчитано
на `<svg>`, а Quasar `<q-icon>` рендерит `<i class="q-icon">`. Прокидываем
transition + rotate через scoped `:deep(.q-icon)` — chevron теперь крутится
при свёртке/развёртке.
2. **«Карточка улетает»** — резкий jump при `display:none` создаёт визуальный
прыжок. Подменяем display:none на `max-height: 0` + `opacity: 0` с
transition'ом. Логика canon-селектора `.is-collapsed` не нарушается,
просто добавлена плавность. Если структурное поведение rail (top
опускается, bottom-chevron остаётся на месте) всё равно неудобно —
перевёрстаем во вторую итерацию.
3. **Клик по балансу → маршрут кошелька.** Новый prop `balanceRoute` —
если задан, блок `.rail__balance` оборачивается в `<router-link :to>`.
Параллельно эмитим `balance-click` — для аналитики или функционального
handler'а. На витрине привязал к `/_dev/ui#wallet` чтобы проверить.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. `RouteMeta.icon` объявлен обязательным в `src/env.d.ts` — добавили
`icon: 'fa-solid fa-flask'` для dev-роута, иначе vue-tsc валится.
2. `RailUserCard` (`shared/ui/domain/RailUserCard`) — доменный
общеплатформенный компонент мини-кошелька в нижней части drawer'а.
Это будущий E11 (Волна 2), но логично вытащить раньше, чтобы основной
layout можно было заменить целиком уже сейчас.
Реализован по canon строго: rail__usercard с usertop+balance+actions,
collapse-chevron справа с поворотом arrow при свёртке (через
`.is-collapsed` модификатор + canon CSS), отдельный `rail__signout`
блок (выходит вне `.rail__usercard` — рендерится сразу после, в том
же `aside.rail` через footer-slot).
Контракт строго dumb по архитектуре: только props/emits/slots.
- props: name, role, avatarSrc, balance, symbol, balanceLabel,
lockedBalance, lockedLabel, primaryActionLabel, collapsed,
showSignout, signoutLabel
- emits: primary-action, update:collapsed (v-model), signout
- slots: usertop-extra, actions
Coнnected-обёртки (с инжектом store/auth) — на уровне страницы,
не здесь.
3. Витрина `/_dev/ui`:
- Секция 12 (AppDrawer): заменили временный огрызок rail-demo__user
на полноценный `<RailUserCard show-signout>` через footer-slot.
- Секция 16 (Каркас приложения): аналогично — теперь у rail полноценный
footer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
E4 «Layout-каркас» Волны 1: 4 структурные обёртки в `src/shared/ui/layout/`.
По архитектуре — только props/emits/slots, без store/router/api, без знания
о данных. Cтраницы передают menu-items, breadcrumbs, табы и активные ключи
сверху.
Состав:
- `AppDrawer` — canon `.rail`: solid surface, активный пункт soft-accent
пилюлей + 2-px рейл слева. Поддерживает плоский список + секции
(RailSection с eyebrow-меткой), opt-in ⌘K-кнопку, slot `footer` для
user-card / logout.
- `AppHeader` — canon `.topbar`: бургер · crumb · `topbar__actions` ·
`topbar__right`. Поддерживает простой title и multi-level breadcrumb.
Глобальные действия через слоты `notifications`/`theme`/`wallet`/`profile`
или общий `right`.
- `PageHead` — canon `.page-head`: eyebrow + title + subtitle + slot
`actions`. Используется когда страница БЕЗ подстраниц.
- `PageTabs` — canon `.tabbar` + `.tab`: вкладки подстраниц с tab__count,
опциональный hairline-разделитель и `tabbar__actions` справа.
Также в составе коммита:
- Убрали AuthCard из base — он композит (cap + body + head + footer),
переедет в E6 Auth flow рядом с SignUp/ResetKey/Invite.
- BaseForm: gap полей 16→10px (тесные формы как в каноне).
- Витрина `/_dev/ui`: 4 новые секции (12-15) на каждый layout-компонент +
секция 16 «Каркас приложения» — AppDrawer + AppHeader + PageTabs в живой
связке без legacy QLayout/QDrawer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Скрипт извлечения из bundled HTML оставил ` <style>...` в начале и
`</style>` в конце обоих файлов. CSS-парсер ловил `<` как ошибочный
селектор и пропускал блок до ближайшей `}` — съедая весь первый блок
правил.
В tokens.css первым блоком был `:root, [data-theme="light"]` со светлой
палитрой — она целиком игнорировалась. `[data-theme="dark"]` ниже
парсился нормально, поэтому тёмная тема работала, а светлая — нет:
кнопки primary/ghost/danger оставались с transparent background и
сливались с canvas.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Заносим эталонную дизайн-систему в проект — только базовые элементы для
дальнейшей компоновки. Source — MONO Design System.html.
Архитектура (по `_bmad-output/planning-artifacts/architecture.md`):
- `src/css/mono-platform/{tokens.css,components.css}` — canon токены и стили
компонентов 1:1 из эталона; импортируются первыми в quasar.config css[].
- `src/shared/ui/base/<Имя>/{Vue,types,index}` — 14 базовых обёрток (Vue 3 +
TS, native HTML над canon BEM, no Quasar-deps кроме Dark API в ThemeToggle).
FSD-граница: только props/emits/slots, никаких store/router/api.
- `src/boot/ui.ts` — глобальная регистрация 14 базовых компонентов.
- `src/boot/theme.ts` — sync `html[data-theme]` ↔ Quasar Dark.
- Inter + JetBrains Mono в index.html.
14 базовых:
BaseButton, BaseInput, BaseSelect, BaseCard, BaseTable, BaseChip, BaseBadge,
BaseDialog, BaseBanner, BaseForm, EmptyState, Avatar, ThemeToggle, AuthCard.
Витрина `/_dev/ui` (dev-only) — каждая обёртка отдельной секцией над чистым
canon-канвасом, без layout-обёрток. Маршрут смонтирован на корневом уровне,
ВНЕ DynamicLayoutWrapper, чтобы legacy default.vue не перебивал стили.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 05:30:28 +00:00
970 changed files with 39381 additions and 32457 deletions
**Базовая ветка mono-ai-1 — `dev`.** Прямые коммиты в `dev` для мелких фиксов разрешены и не требуют feature-ветки/PR (отменено 2026-05-22 пользователем). Для крупных задач — feature-ветка (`feat/...` или `fix/...`) от dev и PR в dev; merge делает пользователь на GitHub. Push в `main` / `mvp` — по-прежнему только через PR.
**Релиз/деплой — FF-промоушн `dev → testnet → main`** (см. `scripts/RELEASE.md`). Версию бампает `lerna` ОДИН раз на dev (`scripts/cut-release.sh`), тот же коммит едет вверх по fast-forward (`scripts/promote.sh testnet|main`). `testnet`/`main` не несут своих коммитов — только FF-указатели, поэтому merge-конфликтов нет. **В `testnet`/`main` прямых коммитов и повторных бампов версии быть не должно** — это ломает FF. Деплой триггерит push в testnet/main с изменением `lerna.json`; окружение определяет ветка (main → prod). Старые `publish-alpha.sh`/`publish-prod.sh` (merge `-X theirs` + per-branch бамп + back-merge) удалены — они и плодили 20-package.json конфликты.
**Не stash'ить `-u` при unstaged WIP пользователя.** Это создаёт окно для потери при drop/конфликте. Если нужно временно убрать unstaged — `git stash push -- <конкретные-paths>` либо коммит-в-feature, и только свои файлы. Кейс 2026-05-18: после `git stash -u`+`drop` при конфликте потерял WIP пользователя (infra.ts/config.ts/quasar.config.cjs и др.).
### Story-by-story для проекта «Стол заказов» (mono-ai-4 на marketplace2)
Канон — `features/Branch/CreateBranch/{api,model}/index.ts`: тип в model `export type IXxxInput = Mutations.X.Y.IInput['data']`; функция в api `function (data: IXxxInput) { ... variables: { data } }`. Не делать `as` cast'ов.
## ⚠️ ДИЗАЙН-КАНОН desktop — ОБЯЗАТЕЛЕН К ИСПОЛЬЗОВАНИЮ
**ПРИ ЛЮБОЙ ВЁРСТКЕ В `components/desktop/` НЕ ВЫДУМЫВАТЬ СТИЛИ.** Всегда сначала свериться с каноном. Не строй гипотез о цветах/радиусах/типографике/паттернах из памяти — иди и читай.
**Полная спецификация канона — в skill `/mono-desktop-canon`** (правила обёрток, цвета/токены, иконки, структура страницы, stop-signals). Здесь — короткая выжимка; при расхождении побеждает skill.
**Источник истины — в самом репозитории** (НЕ внешние HTML/прототипы):
| **Живой эталон** (`/_dev/ui` в dev-сборке) | `components/desktop/src/pages/_dev/ui/index.vue` |
**При сомнении — открыть `tokens.css` и `_dev/ui/index.vue`, смотреть как сделано там.** Внешний `shared/MONO Design System.html` и `auth-prototype/` каноном НЕ являются — это устаревшие прототипы.
**Запреты:**
- Экран собирается из готовых компонентов: `shared/ui/base` (вместо сырых `q-input`/`q-btn`/`q-card`/`q-table`/`q-chip`/`q-dialog`/`q-select`), `shared/ui/domain` (WalletCard, DataRow, DocumentRow, IdentityPanel…), `shared/ui/layout` (PageHead, PageTabs, AppHeader/AppDrawer). Голый Quasar — только где обёртки нет (`q-icon`, `q-toggle`, `q-list`, `q-menu`, `q-tooltip`, `q-tabs`, `q-separator`, `q-spinner`, `q-inner-loading`…). Props обёрток не угадывать — читать `*.types.ts` рядом.
- Цвет — только токены `var(--p-*)` (поверхности `--p-surface*`, текст `--p-ink*`, линии `--p-line*`, акцент `--p-primary`, статусы `--p-pos/neg/warn/info`) либо utility-классы/color-props. Никаких сырых hex/rgb. Темы light/dark переключаются через `[data-theme]` на `<html>` — токены следуют сами.
- Spacing/радиусы/типографика — токены `--p-1..--p-10` (4px…72px), `--p-r-sm/md/lg/xl`, `--p-fs-*`/`--p-lh-*` либо классы `.t-*`. Без хардкод-px.
- Иконки — `q-icon(name='…')` именами Material Icons. **FontAwesome (`fa-*`) запрещён** — заменять на Material-эквивалент попутно.
- Никаких локальных переопределений `.q-btn`/`.q-card`/`.q-notification` в feature-файлах — Quasar-overrides централизованы в `quasar-canon.css`.
**Кейс 2026-05-28:** при сомнении в каноне пошёл искать его во внешнем HTML/auth-prototype вместо репозитория — перевёрстал не туда, переделывал. SoT — репо (`tokens.css` + `_dev/ui`) и skill `/mono-desktop-canon`, не внешние файлы.
## Frontend desktop — English имена
В`components/desktop/` и любом Vue/TS frontend коде **все имена идентификаторов — английские**:
`Staging обновлён: в списке ${stagedPaths.length} путь(ей). Пропущено без изменений относительно индекса: ${skippedUnchanged}; по .blagoignore: ${skippedIgnored}; артефакты только pull (messages/ и meetings/): ${skippedPullOnlyArtifacts}.`,
`Staging обновлён: в списке ${stagedPaths.length} путь(ей). Пропущено без изменений относительно индекса: ${skippedUnchanged}; по .blagoignore: ${skippedIgnored}; артефакты только pull (messages/ и meetings/): ${skippedPullOnlyArtifacts}; не blago-сущности (нет type=project/issue/story во frontmatter): ${skippedNonEntity}.`,
inlineconstexpreosio::namePUT_MINSHARE="o.reg.putmin"_n;///< Внесение минимального паевого при регистрации (Dr 51 / Cr 80, ISSUE MIN_SHARE_FUND). Одношаговый путь adduser (без совета).
// Двухфазный путь через совет (reguser → confirmpay → confirmreg/declinereg):
inlineconstexpreosio::nameRECEIVE_PAYMENT="o.reg.inpay"_n;///< Приём регистрационного взноса кассой в ожидание решения совета (Dr 51 / Cr 76, ISSUE REGISTRATION_PENDING).
inlineconstexpreosio::nameSETTLE_MINSHARE="o.reg.setmin"_n;///< Зачисление минимального паевого по решению совета (Dr 76 / Cr 80, TRANSFER REGISTRATION_PENDING → MIN_SHARE_FUND).
inlineconstexpreosio::nameSETTLE_ENTRANCE="o.reg.setent"_n;///< Зачисление вступительного по решению совета (Dr 76 / Cr 86, TRANSFER REGISTRATION_PENDING → ENTRANCE_FEES).
inlineconstexpreosio::nameREFUND="o.reg.refund"_n;///< Возврат регистрационного взноса при отказе совета (Dr 76 / Cr 51, BURN REGISTRATION_PENDING — деньги уходят из системы банковским переводом кандидату).
inlineconstexpreosio::nameCOMPLETE_WITHDRAW="o.wal.wthcpl"_n;///< Завершение возврата паевого взноса (Dr 80 / Cr 51, BURN_BLOCKED SHARE_FUND_PAY — деньги уходят из системы, без wallet_to).
inlineconstexpreosio::nameREQUEST_WITHDRAW="o.wal.wthreq"_n;///< Запрос на возврат паевого: BLOCK на SHARE_FUND_PAY (без Dr/Cr).
inlineconstexpreosio::nameDECLINE_WITHDRAW="o.wal.wthdec"_n;///< Отклонение запроса на возврат: UNBLOCK на SHARE_FUND_PAY (без Dr/Cr).
inlineconstexpreosio::nameCOMPLETE_WITHDRAW="o.wal.wthcpl"_n;///< Завершение возврата паевого взноса (Dr 80 / Cr 51, BURNс WITHDRAW_PENDING — деньги уходят из системы, без wallet_to).
inlineconstexpreosio::nameREQUEST_WITHDRAW="o.wal.wthreq"_n;///< Запрос на возврат паевого: TRANSFER SHARE_FUND_PAY → WITHDRAW_PENDING (резерв, без Dr/Cr).
inlineconstexpreosio::nameDECLINE_WITHDRAW="o.wal.wthdec"_n;///< Отклонение запроса на возврат: TRANSFER WITHDRAW_PENDING → SHARE_FUND_PAY (снятие резерва, без Dr/Cr).
}
// capital
@@ -129,14 +134,23 @@ namespace operations {
* `(debit_account_id == 0, credit_account_id == 0)` на уровне записи реестра.
* Compile-time правило `(debit==0) ⇔ (credit==0)` ловит смешанные пары.
// баланса упразднена. Резерв средств под заявку на возврат паевого теперь
// выражается переводом на отдельный кошелёк-резерв `w.wal.wpend` (TRANSFER),
// возврат резерва — обратным TRANSFER, завершение — BURN с резерва. Поле
// `blocked` в таблицах wallets2/userwallets оставлено deprecated (всегда 0
// после ledger2::migrate-свёртки) — физическое удаление поля = небезопасная
// смена layout таблицы на живых коопах, выносится в отдельный cleanup-деплой.
//
// Числовые значения ISSUE/TRANSFER/BURN/NONE СОХРАНЕНЫ (не перенумерованы),
// чтобы исторические op_code в blockchain_actions читались бэкендом без сдвига
// смысла (2 и 3 — бывшие BLOCK/UNBLOCK — больше не выдаются и невалидны на входе).
enumclassWalletOp:uint8_t{
ISSUE=0,///< первичный вход средств на кошелёк wallet_to (wallet_from = empty)
TRANSFER=1,///< перемещение wallet_from → wallet_to (с Dr/Cr ИЛИ без — по парам account_id)
BLOCK=2,///< available-=amount, blocked+=amount на wallet_from
UNBLOCK=3,///< blocked-=amount, available+=amount на wallet_from
BURN=4,///< изъятие amount с wallet_from->available, без wallet_to. Покрывает оба кейса: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY; (b) зеркало ISSUE при `ledger2::revert` (различие — через operation_code: `o.adj.rev` для adjustment-mirror).
NONE=5,///< только бухпроводка без перемещения средств (wallet_from = empty, wallet_to = empty, debit ≠ 0, credit ≠ 0). Покрывает кейсы внутрибалансовых проводок типа Dr 04 / Cr 08 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
BURN_BLOCKED=6,///< изъятие amount из wallet_from->blocked, без wallet_to. Используется при завершении возврата паевого (`o.wal.wthcpl`): средства предварительно заблокированы через BLOCK (REQUEST_WITHDRAW), на завершении сжигаются — выходят из системы (получателя на цепи нет). На L3 синхронно: from_uw->blocked -= amount.
};
/**
@@ -145,7 +159,7 @@ enum class WalletOp : uint8_t {
inlineconstexpreosio::nameACCEPT="p.reg.accept"_n;///< Приём пайщика в кооператив (o.reg.payent + o.reg.putmin).
inlineconstexpreosio::nameACCEPT="p.reg.accept"_n;///< Приём пайщика в кооператив (o.reg.payent + o.reg.putmin; для потока через совет — o.reg.inpay + o.reg.setmin + o.reg.setent).
inlineconstexpreosio::nameREFUND="p.reg.refund"_n;///< Возврат регистрационного взноса при отказе совета (o.reg.refund). Отдельный процесс: приём взноса прерывается, начинается возврат.
staticconstexpreosio::nameCK_MEMBER="w.wal.member"_n;///< ЦК — членская часть пайщика (USER_SHARED)
staticconstexpreosio::nameWITHDRAWALS_SINK="w.wal.wthdrw"_n;///< DEPRECATED 2026-05-21: после переключения o.wal.wthcpl с TRANSFER на BURN_BLOCKED кошелёк больше не получает новых средств. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
staticconstexpreosio::nameWITHDRAWALS_SINK="w.wal.wthdrw"_n;///< DEPRECATED 2026-05-21: исторический sink возвратов. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
staticconstexpreosio::nameWITHDRAW_PENDING="w.wal.wpend"_n;///< Резерв паевого под заявку на возврат (COOPERATIVE-пул). o.wal.wthreq переводит сюда с w.wal.share, o.wal.wthdec возвращает обратно, o.wal.wthcpl сжигает отсюда. Заменил механику blocked/BLOCK/UNBLOCK 2026-05-24.
staticconstexpreosio::nameREGISTRATION_PENDING="w.reg.pend"_n;///< Регистрационный взнос в ожидании решения совета (суспенс счёта 76, USER_SHARED, без сверки соглашений — кандидат ещё не член). o.reg.inpay → сюда (Dr 51/Cr 76); o.reg.setmin/setent уносят на 80/86; o.reg.refund сжигает (Dr 76/Cr 51).
* The `eosio.system` smart contract is provided by `block.one` as a sample system contract, and it defines the structures and actions needed for blockchain's core functionality.
*
@@ -616,6 +642,21 @@ namespace eosiosystem {
/**
* @brief Предложить новую политику финализаторов Savanna.
* Проверяет BLS-ключи и proof of possession каждого финализатора и
* передаёт политику в хост-функцию set_finalizers. Вступает в силу,
* когда не вытеснена более поздней политикой. Первый вызов после
* активации протокол-фичи SAVANNA запускает переход цепи на
* instant finality.
*
* @param finalizer_policy - предлагаемая политика финализаторов
*
* @note Авторизация требуется от аккаунта: @p get_self()
'сводка по вступительному (регистрационному) платежу пайщика. Позволяет восстановить шаг регистрации (ожидание решения совета или отклонение платежа) после перезагрузки страницы и в любой вкладке.',
'Удалить аккаунт пайщика из системы учёта провайдера. Доступно только для незавершённых регистрационных статусов (черновик, неоплачен/отклонён). Активный, заблокированный и любой зарегистрированный в блокчейне аккаунт удалить нельзя. Используется для очистки реестра и освобождения e-mail под перерегистрацию.',
})
@UseGuards(GqlJwtAuthGuard,RolesGuard)
@AuthRoles(['chairman'])
asyncdeleteAccount(
@Args('data',{type:()=>DeleteAccountInputDTO})
data: DeleteAccountInputDTO
):Promise<boolean>{
awaitthis.accountService.deleteAccount(data);
// return true;
// }
returntrue;
}
@Mutation(()=>AccountDTO,{
name:'resetRegistration',
description:
'Откатить собственную незавершённую регистрацию к редактированию данных: снимает заморозку профиля и e-mail, сбрасывает подписанное заявление и непринятую попытку вступительного платежа. Доступно только до отправки регистрации в блокчейн; если взнос уже принят — требуется возврат средств.',
@@ -88,16 +88,25 @@ export class BlockchainDecisionDTO implements SovietContract.Tables.Decisions.ID
@Field(()=>String,{nullable: true})
hash!:string;
@Field(()=>Number,{
description:
'Текущее число членов совета (всего, как считает контракт). Нужно фронту для вычисления порога принятия/отклонения: за/против * 100 > council_members_count * 50',
description:'Опубликовать предложенную повестку и проект решения для дальнейшего голосования совета по нему',
nullable: true,
description:
'Опубликовать предложенную повестку и проект решения для голосования совета. Возвращает созданный пункт повестки (или null, если он ещё не проиндексирован) для немедленного отображения на фронте.',
`Найден существующий активный регистрационный платеж для пользователя ${data.username} на сумму ${amount}${symbol}`
`Регистрационный платёж для ${data.username} уже существует (${lastRegistrationPayment.id}, статус ${lastRegistrationPayment.status}) — повторный ордер не создаём`
@@ -504,12 +602,24 @@ export class GatewayInteractor {
}
this.logger.log(
`Создан исходящий платеж ${data.payment_hash} для пользователя ${data.username} на сумму ${data.quantity}${data.symbol} с платежным методом ${data.method_id}`
`Создан исходящий платеж ${paymentData.hash} для пользователя ${paymentData.username} на сумму ${paymentData.quantity}${paymentData.symbol} с платежным методом ${paymentData.payment_method_id}`
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.