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>
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Epic 10 Story 10.2b — минимальный subgraph на extension-sdk доказывает что
Federation v2 driver + JWT module + healthcheck реально работают вместе.
Smoke результат (SUBGRAPH_PORT=33001 JWT_SECRET=test-secret COOPNAME=voskhod):
- GET /_health → {"status":"ok","version":"unknown","uptimeSeconds":N}
- POST /v1/graphql { helloFromExtension(name:"мир") { greeting fromExtension } }
→ {"greeting":"Здравствуй, мир!","fromExtension":"hello-subgraph@0.1.0"}
- POST /v1/graphql { _service { sdl } } → returns federation v2 SDL с
@key/@external/@requires/@shareable директивами
Также в этом коммите:
- extension-sdk переключён с unbuild на обычный tsc (unbuild esbuild не
поддерживает декораторы для target ES2020). main: dist/index.js, CommonJS.
- @types/passport-jwt в devDeps.
Cross-extension entity references (SharedCooperator @key + ResolveField)
живут в chatcoop subgraph'е (story 10.8) — там есть локальная сущность
ChatThread которая ссылается на пайщика. Этот PoC только доказывает
базовую механику.
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Epic 10 Story 10.1 — coopback становится первым federation v2 subgraph'ом.
Локальная core-схема компилируется с federation директивами (`autoSchemaFile.federation: 2`).
Endpoint остаётся на /v1/graphql; non-federated клиенты (desktop) не должны
заметить переключения — Apollo Federation добавляет лишь служебные
`_service { sdl }` и `_entities` query.
Gateway-режим (Apollo Gateway + IntrospectAndCompose) зайдёт отдельной
story 10.1b — поверх этого subgraph'а + registry в Postgres. На MVP пока
один subgraph, gateway не нужен.
Зависимости:
- + @apollo/subgraph ^2 (peer для federation 2 в @nestjs/apollo)
@key директивы на core entity (User/Cooperative/Account/Wallet) добавятся
по мере вытаскивания первых extension'ов — отдельными точечными PR.
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Новый push в PR отменяет ещё бегущий typecheck той же ветки —
прежде 30-минутные прогоны копились очередью.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 13m24s
Typecheck / controller (pull_request) Successful in 13m19s
mono controller:
- AppsCatalogHttpService.fetchInstallScript(scope, name) — fetch text
install.js из ca-admin /v1/public/packages/:scope/:name/install.js;
degraded mode (null) при отсутствии env-config'а apps-catalog'а.
- Новый REST controller AppsCatalogInstallScriptController
`GET /v1/apps-catalog/install/:scope/:name` под HttpJwtAuthGuard:
валидирует scope/name regex'ом, проксирует на ca-admin, отдаёт
text/javascript + Cache-Control: no-store. Admin-API key остаётся
на backend'е.
- AppsCatalogProxyModule: добавлен controller в module.controllers[].
mono desktop:
- remote-loader.ts: реальный fetch + CJS-eval. fetchInstalledRemotePackages
читает Queries.Extensions.AppsCatalogRemotePackages, парсит packageId
на (scope, name). loadRemoteExtension качает install.js по REST через
axios responseType:'text' с JWT desktop'а, оборачивает в
`new Function('module','exports','require', code)`, вызывает
`module.exports.install()` → IWorkspaceConfig[].
- Try/catch per item (NFR15-стиль): падение одного пакета не валит
остальные. require() сразу throw — пакеты должны быть CJS без
внешних зависимостей (sandbox — Story 11.x).
- sha256 пока не проверяем (release-таблица будет в 9.4.c).
Demo path: voskhod publishes @voskhod/demo-app → ca-admin отдаёт
examples/demo-app/install.js → controller-proxy → desktop boot тянет +
eval'ит + получает workspace «Demo» с роутом demo-hello.
Typecheck / desktop (pull_request) Failing after 7m15s
Typecheck / controller (pull_request) Failing after 7m21s
При nullable=true декоратор @Field NestJS GraphQL не умеет вывести тип из
TS-reflection-метаданных (TS пишет в metadata-ключ Object). Без явного
type fn (() => String) — UndefinedTypeError на старте.
Фикс:
- @Field({ nullable: true }) → @Field(() => String, { nullable: true })
для lastActiveVersion: string | null.
- .env.example добавлены APPS_CATALOG_URL и APPS_CATALOG_API_KEY с
готовыми значениями для dev (URL = DNS-имя ca-admin в mono-shared
сети).
Verify: coopback стартует чисто (Nest application successfully started),
DTO виден через introspection (__type AppsCatalogRemotePackageDTO →
поля packageId/publisher/title/description/rubPerMonth/lastActiveVersion/
compatibleSubnets), guard срабатывает (без JWT → 401 Unauthorized).
Typecheck / desktop (pull_request) Failing after 7m39s
Typecheck / controller (pull_request) Failing after 7m23s
Story 9.5.b mono часть: добавляет модуль расширения
`apps-catalog-proxy` с GraphQL Query.appsCatalogRemotePackages, который
проксирует запрос на apps-catalog/ca-admin /v1/public/packages.
Архитектурно — выбран альтернатива C (см. apps-catalog
docs/epics/E9-demo-app-pilot.md, V2 follow-ups): браузер ходит в каталог
через mono controller, а не напрямую в ca-admin. Поэтому:
- admin-secret apps-catalog'а живёт в env controller'а, не в SPA;
- нет CORS — desktop запрос на свой же origin;
- единый канон mono GraphQL (никаких raw HTTP в desktop).
Компоненты:
- AppsCatalogRemotePackageDTO (@ObjectType): packageId/publisher/
compatibleSubnets/lastActiveVersion + UI-поля title/description/
rubPerMonth (в V1 description и rubPerMonth — заглушки; в V2 будут из
package manifest и apps-catalog pricing).
- AppsCatalogHttpService: axios-клиент к ca-admin с Authorization: Bearer
admin-key из APPS_CATALOG_API_KEY. Degraded mode (пустой список без
ошибки), если env пустой — чтобы dev/CI без apps-catalog не валили
desktop boot.
- AppsCatalogProxyResolver: @Query, @UseGuards(GqlJwtAuthGuard) — каталог
видят только авторизованные пайщики.
- Регистрация в ExtensionsModule.
Config:
- Новые env APPS_CATALOG_URL и APPS_CATALOG_API_KEY (оба optional).
- Зеркальные поля в config.apps_catalog.{url, api_key}.
TODO (требует ручного запуска на остановленном контейнере, см. CLAUDE.md:
'pnpm generate-schema / generate-client — не запускать локально'):
- pnpm -F controller generate-schema → пересоздать schema.gql.
- pnpm -F controller generate-client → graphql-zeus в SDK.
- pnpm -F sdk build → unbuild dist.
Без codegen фронт ещё не сможет дёрнуть резолвер через Zeus.
Typecheck / desktop (pull_request) Failing after 7m18s
Typecheck / controller (pull_request) Failing after 7m14s
Секция «Удалённые пакеты» с карточкой @voskhod/demoapp (publisher,
price 1000 ₽/мес) + dialog «Подписаться» (V1 — заглушка).
Why: Анна видит реальный второй раздел, отделённый от bundle-расширений,
и проходит UX-сценарий клика «Подписаться» — без backend-резолверов.
Реальный список remoteExtensions через Query.appsCatalogRemotePackages
+ Mutation.appsCatalogSubscribe — отложено в 9.5.b (см. mono#64
follow-ups).
Стили — токены MONO Design System (--p-*), никаких magic hex.
Typecheck / desktop (pull_request) Failing after 7m22s
Typecheck / controller (pull_request) Failing after 7m15s
V1 минимум для UI ExtensionStore:
- Информационный banner сверху grid'а карточек, объясняющий что bundle-
extensions ниже бесплатные, а remote-пакеты с подпиской появятся
на следующем шаге пилота.
- Реальная карточка remote-пакета с кнопкой «Подписаться» / «Уже
подписан до DD.MM» — story 9.5.b (требует расширения extStore
на kind/subscribed/pricingRubPerMonth + backend-резолверы coopback'а).
Refs: docs/integration/native-to-catalog-research.md §9 story 9.5.
Typecheck / desktop (pull_request) Failing after 7m22s
Typecheck / controller (pull_request) Failing after 7m21s
9.3 — components/desktop/extensions/developer/:
- install.ts: workspace `developer`, defaultRoute=developer-my-packages,
4 страницы (packages, publish, subscribers, pricing), все under
meta.roles=['chairman']. Регистрация в extensions-registry.ts.
- pages/: V1 Vue-заглушки с описанием TODO (без реальных мутаций).
Mutation flow (POST /v1/admin/package, /releases, /pricing с подписью
chairman'а) — story 9.3.b.
9.4 — processes/init-installed-extensions/remote-loader.ts:
- Контракт fetch/load для remote-пакетов с активной подпиской: тип
RemoteExtensionDescriptor, функции fetchInstalledRemotePackages,
loadRemoteExtension, installRemoteExtensions.
- V1 — заглушка возвращающая []. Не падает, логирует пропуск.
- index.ts: после bundle-pass — remote pass через installRemoteExtensions;
try/catch чтобы падение remote-flow не валило bundle extensions.
- TODO 9.4.b: реальный fetch к coopback + tar-x + dynamic require
+ sha256-check + (V2 sandbox).
Видимость стола: пока через meta.roles в install.ts. Полноценная
авторизация через DesktopWorkspace.grants с флагом is_operator_coop —
9.3.b/9.5.
Refs: /home/admin/mono-ai-5/docs/integration/native-to-catalog-research.md §9.
Typecheck / desktop (pull_request) Successful in 13m20s
Typecheck / controller (pull_request) Successful in 12m58s
Финальные решения по V1 пилоту (без миграции native):
- demo-app = пустой Hello-стол, @voskhod/demo-app
- Pricing 1000 RUB/мес (60 сек на dev), trial 15 мин
- partner1 как покупатель через pnpm boot:extra (на той же цепи)
- Self-subscription bypass: воскод как оператор подписан бесплатно
- Без кэша tarball'ов — каждый desktop boot качает заново
- Стол Разработчика = новый bundle-extension developer, виден chairman оператора
- partner1 desktop+coopback в отдельном docker overlay из apps-catalog
Epic 9 раскладка на 8 stories с зависимостями. Реализация — следующими
коммитами в зонтичной ветке apps-catalog mono + feat/E9-demo-app-pilot
в apps-catalog репо.
Typecheck / desktop (pull_request) Successful in 13m17s
Typecheck / controller (pull_request) Successful in 12m53s
html2pdf.js на верхнем уровне модуля трогает `self` → при SSR-бандле Quasar
падает [Quasar] boot error: ReferenceError: self is not defined. Экспорт PDF
нужен только на клиенте по действию пользователя — переносим импорт в
dynamic import() внутри exportFormToPdf, сервер модуль не вычисляет.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Были перекрещены относительно реестра документов: 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>
Typecheck / desktop (pull_request) Successful in 13m31s
Typecheck / controller (pull_request) Successful in 13m10s
Документ остаётся в скроллящемся body BaseDialog, кнопка вынесена в слот
footer (.base-dialog__foot, вне overflow) — устраняет залипание, когда до
кнопки нельзя домотать (стек maximized-диалогов RequireAgreements).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 13m36s
Typecheck / controller (pull_request) Successful in 13m33s
Дефолт реестра совета был newresolved → status=Resolved → виден только
утверждённый документооборот, submitted-документы (в т.ч. свежеподанные
заявления) пропадали. Переключатель вкладок на странице скрыт
(showFilter=false), сменить вид было нельзя. Выравниваем с реестром пайщика:
newsubmitted → status не ограничивается → совет видит ВСЕ документы кооператива.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 14m24s
Typecheck / controller (pull_request) Successful in 13m28s
freedecision эмитит newresolved и newdecision по одному package в одной
транзакции — оба @OnEvent-хендлера срабатывают одновременно. safeReassemble
(newdecision/newact/newlink) пере-upsert'ил все строки пакета ПОЛНЫМ набором
колонок со сталым row.status, прочитанным до коммита newresolved → затирал
resolved обратно в submitted. Решение совета застревало в submitted и пропадало
из реестра совета (дефолт «Только утверждённые» = status=Resolved).
Пересборка теперь обновляет ТОЛЬКО агрегат-производные колонки
(document_aggregate/full_title/content_text/signers_text) точечным UPDATE.
status/block_num/source_action_data принадлежат статус-событиям и пересборкой
не трогаются → конкурентный newresolved не откатывается. Новый метод
SignedDocumentRepository.updateAggregate + SignedDocumentAggregateUpdate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Предыдущий фикс снимал лоадер, но 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>
Typecheck / desktop (pull_request) Successful in 13m54s
Typecheck / controller (pull_request) Successful in 13m7s
Кнопка «Утвердить» совета слала authorize+exec напрямую из браузера ключом
председателя — при HTTP 504 от ноды ошибка терялась в браузере, бэкенд её не
видел и не логировал. Переводим действие на проведение через контроллер ключом
кооператива, как остальные soviet-мутации (sendAgreement) — теперь ошибки связи
с блокчейном попадают в логи бэкенда.
Контракт soviet: authorize/exec require_auth(chairman/executer) → require_auth(coopname).
Согласие председателя не теряется — закреплено его личной подписью на document2
(verify_document_or_fail) + проверкой is_valid_chairman.
- contracts: authorize.cpp/exec.cpp — require_auth(coopname)
- controller: новая мутация authorizeDecision (DecisionAuthorizeModule) + порт/адаптер,
пушит authorize+exec одной транзакцией ключом кооператива из vault
- cooptypes: authorizations actor chairman/username → coopname
- sdk: Mutations.Decisions.AuthorizeDecision + регенерация schema.gql/zeus
- desktop: фича AuthorizeAndExecDecision и process-decisions зовут бэкенд-мутацию
вместо useGlobalStore().transact; подпись документа остаётся клиентской
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Typecheck / desktop (pull_request) Successful in 13m52s
Typecheck / controller (pull_request) Successful in 13m12s
Страницы deep-link (карточка документа, name=document-details/
user-document-details) помечены meta.hidden=true и в навигацию не
попадают — иконка им не нужна. Drawer рендерит q-icon под v-if='item.icon',
undefined безопасен. Убирает TS2741 'Property icon is missing'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Failing after 8m20s
Typecheck / controller (pull_request) Successful in 12m31s
Карточка реестра вела на страницу документа, передавая rawDocument.hash — а это
doc_hash (хеш самого документа, без подписи). Бэкенд же ищет документ по колонке
hash (подписанный документ = содержимое + подпись): UPPER(d.hash)=UPPER(:hash).
Значения разные → getDocuments возвращал 0 → «Документ не найден».
Из поиска работало, потому что searchDocuments отдаёт именно hash (колонку), а не
doc_hash. Карточка теперь берёт тот же document.hash (= колонка hash в реестре).
Проверено по БД: фильтр по doc_hash → 0 строк, по document.hash → 1; у всех
записей document.hash == колонка hash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Failing after 8m31s
Typecheck / controller (pull_request) Successful in 12m49s
Реестр документов переведён с таблицы на канон плоские строки (DocumentRow):
строка кликабельна и ведёт на отдельную страницу документа. Таблица с
инлайн-раскрытием оставлена только для Union-страницы (expand=true).
- DocumentCardsList: список DocumentRow (заголовок, дата, ФИО подписантов,
скачивание в actions), скелетоны при загрузке, «Загрузить ещё». Статус-чип
не показываем — в реестре всё подписано (канон stop-signal).
- ListOfDocumentsWidget: проп expand — карточки по умолчанию (совет/пайщик),
таблица для Union (передаёт expand=true) — поведение Union не меняется.
- useDocumentNavigation перенесён из фичи поиска в entity Document
(переиспользуют и поиск, и карточный список — чистые слои).
- DocumentDetailsPage: кнопка «назад» теперь в липком баре (sticky,
top=--p-topbar-h), видна всегда при прокрутке документа.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Failing after 8m32s
Typecheck / controller (pull_request) Successful in 12m33s
activeWorkspaceName инициализируется асинхронно — при холодном переходе по
прямой ссылке на документ совета скоуп мог упасть в скоуп пайщика и документ
«не находился». Имя роута (document-details / user-document-details) доступно
сразу и детерминированно.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Failing after 8m24s
Typecheck / controller (pull_request) Successful in 12m42s
Прошлый подход (фильтр реестра по ?document=<hash> + баннер) отклонён —
нужна полноценная отдельная страница документа с переходом назад и deep-link.
- DocumentDetailsPage: canon-страница по эталону карточки собрания (back-link
под шапкой, имя документа в заголовок через setPageTitleOverride, скелетон,
EmptyState; контент — ComplexDocument). Грузит документ сама по hash из роута,
поэтому открывается и прямым переходом по ссылке.
- Роуты documents/:hash в обоих расширениях: document-details (совет) и
user-document-details (пайщик), hidden — по аналогии с meet-details.
- DocumentModel.loadDocument(username, hash): точечная загрузка одного документа
(статус-агностичный фильтр newsubmitted), не трогает состояние списка.
- Клик в поиске ведёт на страницу документа (useDocumentNavigation выбирает роут
по рабочему столу), а не фильтрует реестр.
- Откат фильтр-подхода: удалены SearchedDocumentBanner и useDocumentRouteFilter,
страницы реестра/виджет/таблица возвращены к baseline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 12m55s
Typecheck / controller (pull_request) Successful in 12m31s
Клик по результату поиска вёл в никуда (openDocument был пуст) — найденный
документ нельзя было посмотреть. Теперь клик кладёт hash в ?document=<hash>
текущего роута; страница реестра читает его и сводит список к одному документу,
авто-раскрывая его. Переиспользуем готовый backend-фильтр filter.document.hash
(тот же путь, что у Union-страницы) — без изменений API и codegen.
- useDocumentRouteFilter/useDocumentNavigation: чтение и установка ?document.
- SearchedDocumentBanner: баннер «показан один документ» с возвратом к списку.
- Обе страницы реестра (совет + пайщик) фильтруют виджет по выбранному документу.
- Виджет реагирует на смену filter и форсит статус-агностичный newsubmitted при
hash-фильтре (иначе на вкладке «только утверждённые» неутверждённый документ
из поиска не нашёлся бы).
- DocumentsTable авто-раскрывает строку по expandHash (регистронезависимо).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 12m54s
Typecheck / controller (pull_request) Successful in 12m32s
В экране ожидания регистрации (WaitingRegistration) «до 24 часов» — неактуально:
реальный регламентный срок рассмотрения советом — до 30 дней (обычно решение
принимается за день-два). Поправили текст под фактический срок.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 13m3s
Typecheck / controller (pull_request) Successful in 12m36s
Большая пустая полоса между строкой поиска и первым результатом мешала —
снят верхний padding секции результатов (q-pt-none), список начинается ближе.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 12m57s
Typecheck / controller (pull_request) Successful in 12m36s
Поиск показывал username («ant») и сырой full_title с хвостом ` - - дата.pdf`.
Приводим к виду реестра на главной:
- SearchResult.signer — ФИО подписанта-субъекта («Иванов Иван Иванович»), берётся
из signer_certificate готового агрегата (подпись с signer===username, иначе первая;
организация → short_name). Тот же источник, что у чипов подписей в реестре.
- full_title в выдаче поиска теперь = meta.title (чистое наименование), как в реестре;
fallback — прежний full_title.
- Фронт DocumentSearchDialog: заголовок + чип ФИО (BaseBadge) + дата вместо строки
«username · дата»; убран пустой highlights-блок.
Схема + zeus-клиент перегенерированы (generate-schema/generate-client + sdk build).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 13m13s
Typecheck / controller (pull_request) Successful in 12m41s
Документы хранят дату в локализованном формате `DD.MM.YYYY HH:mm` + отдельное
meta.timezone (так пишет фабрика @coopenomics/factory через moment.tz). Прежний
`new Date(meta.created_at)` этот формат не парсил → Invalid Date → колонка
timestamptz падала с "invalid input syntax ... 0NaN-NaN-NaN", и throw в одной
записи ронял весь прогон backfill. Парсим тем же moment.tz, что и фабрика
(timezone из меты, иначе config.timezone), невалидное → null. Плюс per-record
try/catch в backfill: одна битая запись логируется и пропускается, а не валит прогон.
Проверено вживую на voskhod: backfill завершён scanned=8 created=4 updated=4
skipped=0, document_created_at = 2026-05-28 07:53:00+00 (валидные даты, не null).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 12m56s
Typecheck / controller (pull_request) Successful in 12m36s
TypeORM @Index(['...']) принимает имена СВОЙСТВ сущности, а не db-колонок.
Колонка объявлена @Column({ name: 'package' }) packageHash, поэтому индекс
['coopname','package'] падал на старте: "Index contains column that is missing
in the entity (SignedDocumentEntity): package". Индекс по db-колонке `package`
строится корректно и через имя свойства packageHash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ошибки цепи возвращают суммы с сырой 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+&.
Typecheck / desktop (pull_request) Successful in 14m11s
Typecheck / controller (pull_request) Successful in 13m38s
Ошибки цепи возвращают суммы с сырой precision=4 ("100.0000 RUB"). Добавлен formatAssetsInText() рядом с formatAsset2Digits и применён в FailAlert — единая точка для всех тостов ошибок. Суммы вида <целое>.<>=3 знаков> <ТИКЕР> приводятся к 2 знакам с группировкой; уже отформатированные не трогаются (идемпотентно).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Typecheck / desktop (pull_request) Successful in 13m31s
Typecheck / controller (pull_request) Successful in 13m7s
Устаревшая формулировка в docstring backfill-сервиса: после перехода на ключ
doc_hash идемпотентность/уникальность реестра — по (coopname, doc_hash), а НЕ по
(coopname, package). package сознательно не уникален (один процесс = несколько
разных документов с разными подписантами), поэтому ключом записи он быть не может.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 13m36s
Typecheck / controller (pull_request) Successful in 13m5s
Ось упорядочивания версий подписи одного doc_hash должна мерить одну величину
на обеих сторонах guard'ов ingestAction. Раньше block_num хранился через
resolveBlockNum(meta.block_num, action.block_num) с приоритетом meta.block_num
(блок ГЕНЕРАЦИИ документа фабрикой), а incomingBlock в guard'ах — action.block_num
(блок ДЕЙСТВИЯ в цепи). Так как документ всегда генерируется до подачи в цепь
(meta.block_num <= action.block_num), защита от отката на старую версию подписи
(incomingBlock < existing.blockNum) и идемпотентность по блоку фактически не
срабатывали. Теперь приоритет — action.block_num, meta.block_num лишь fallback.
Заодно выправлены устаревшие комментарии интерфейса репозитория (ключ — doc_hash,
а не hash).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 13m57s
Typecheck / controller (pull_request) Successful in 13m34s
Два уточнения по замечаниям:
1. Ключ реестра переведён с hash на doc_hash. doc_hash = идентичность документа; hash включает
наложенные подписи, поэтому у ОДНОГО документа бывает несколько hash (версии по мере накопления
подписей — напр. акт приёма-передачи: 1-я подпись → один hash, 2-я → другой, doc_hash тот же).
При ключе по hash документ задваивался в списке (одна подпись / две подписи). Теперь:
- уникальный индекс (coopname, doc_hash); hash и package — неуникальные индексы;
- ingestAction ключуется по data.document.doc_hash и держит КРАЙНЮЮ версию: guard'ы «не понижать
статус» и «не откатывать на меньший block_num» (getState отдаёт status+blockNum); крайняя версия
перетирает предыдущую (тот же документ, больше подписей);
- hash хранится как версия последней подписи (для фильтра Union по document.hash и отображения).
package по-прежнему группирует разные документы процесса (обмен = разные doc_hash = разные строки).
2. searchDocuments вернул доступ пайщику, но со скоупом: член совета (chairman/member) ищет по всему
кооперативу, обычный пайщик — ТОЛЬКО по своим документам (search scope username=user.username).
Снял role-гард, добавил @CurrentUser; SearchResolver решает скоуп по роли. Заодно закрывает прежнюю
утечку (старый поиск был coop-wide для любого авторизованного).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 13m53s
Typecheck / controller (pull_request) Successful in 14m23s
Самоаудит по замечанию: package — это идентификатор ПРОЦЕССА, в который может входить несколько
заявлений с разными подписантами. Доказано в контракте: marketplace/change.cpp шлёт ДВА newsubmitted
с одним package, но разными document и username (обмен: contribute+return). Прежний уникальный индекс
(coopname, package) + upsert-по-package затирал второе заявление первым → ПОТЕРЯ ДОКУМЕНТА и склейка
username. Старый explorer-путь (список по newsubmitted) показывал оба.
- Сущность: уникальный индекс теперь (coopname, hash) — одна строка = один документ-заявление;
(coopname, package) стал НЕуникальным группирующим индексом.
- ingestAction ключуется по data.document.hash; смена статуса (newsubmitted/newresolved/newdeclined,
все несут то же заявление — подтверждено createagenda/declinedoc/make_complete_document) матчит
строку по hash.
- newdecision/newact/newlink несут package (не заявление) → пересобирают агрегаты ВСЕХ заявлений
пакета (repository.findByPackage), сохраняя статус каждого.
- Репозиторий: upsert/getStatus по hash; findByPackage вместо getSourceActionData; убраны
неиспользуемые setStatus/exists.
Также: searchDocuments закрыт гардом RolesGuard + @AuthRoles(['chairman','member']) — поиск по
документообороту кооператива только для совета (SearchDocumentsInput не содержит username, поэтому
правило «свои ресурсы» не открывает доступ обычному пайщику). Поиск пайщиком по своим документам
пока не предусмотрен.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 14m40s
Typecheck / controller (pull_request) Successful in 13m16s
Самоаудит видимости документов выявил две регрессии относительно прежнего explorer-пути:
1. Вкладка «Все входящие» (type=newsubmitted) теряла решённые документы. Прежний фильтр был по
ИМЕНИ действия: у каждого пакета есть newsubmitted-действие → «Все входящие» = все документы
(решённые показывались и тут, и во «Только утверждённые»). Партиция по статусу (newsubmitted→
status=Submitted) скрывала решённые из дефолтной вкладки User/DocumentsPage. Фикс: newsubmitted →
без фильтра статуса (все), newresolved → status=Resolved.
2. Backfill сканировал explorer с query={} — без скоупа. Explorer (cooparser) хранит трейсы по
получателям; без receiver вернулись бы все трейсы каждого действия (soviet/пайщик) и, возможно,
чужие кооперативы. Фикс: query={receiver: coopname} — ровно как прежний chairman-путь getDocuments
(один трейс на документ, только наш кооператив).
Утечки не было и нет: RolesGuard разрешает обычному пайщику только data.username==его_имя, а фильтр
findAggregates ограничивает username; coopname-скоуп (весь кооператив) доступен лишь совету/председателю.
findAggregates всегда фильтрует по config.coopname.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 14m26s
Typecheck / controller (pull_request) Successful in 13m48s
Завершение перехода одним PR: searchDocuments и getDocuments уже читают PG-реестр подписанных
документов, OpenSearch стал мёртвым кодом — убираем его полностью.
- Удалены infrastructure/search/* (opensearch.service, search-registry.service,
search-infrastructure.module) и application/search/services/search-event.service
(старый индексатор по newsubmitted — заменён SignedDocumentIngestionService + backfill).
- app.module: снят SearchInfrastructureModule. search.module: только SearchResolver (читает
SIGNED_DOCUMENT_REPOSITORY из глобального TypeOrmModule).
- package.json: убрана зависимость @opensearch-project/opensearch; pnpm-lock.yaml пересчитан.
- docker-compose: удалён сервис opensearch + volume opensearch_data.
- .env-example: убраны OPENSEARCH_* (поиск больше не конфигурируется через env).
- system.interactor: features.search = true (поиск на PG доступен всегда, не зависит от OPENSEARCH_ENABLED).
Файлы удалены только из текущего состояния (git rm) — в истории остаются.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 13m35s
Typecheck / controller (pull_request) Successful in 13m6s
Старый getDocuments через DocumentService подмешивал в explorer-фильтр `receiver: username`
(on-chain require_recipient): receiver=coopname отдавал все документы кооператива, receiver=<пайщик>
— его документы; Union-страница добавляла filter.document.hash. PG read-path эти ключи игнорировал
(искал несуществующий data.username) — личная страница пайщика показала бы все документы кооператива.
- interactor: extractReceiverScope (receiver===coopname → весь кооператив; иначе фильтр username=receiver,
т.к. колонка username = пайщик-субъект заявления) + extractHashFilter (filter.document.hash).
- repository: новый параметр hash в SignedDocumentListParams; фильтр UPPER(d.hash)=UPPER(:hash)
(фронт шлёт хэш в upper-case).
Набор документов getDocuments теперь совпадает со старым explorer-путём для всех трёх потребителей
(Cooperative/ListOfDocuments, User/DocumentsPage, Union/ListOfCooperatives).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 13m28s
Typecheck / controller (pull_request) Successful in 13m12s
Поиск по документам — в основном по фамилии подписанта, а не username. content_text (html заявления)
этого не покрывает: соподписанты (председатель/совет) подписывают решение/акты, а не тело заявления,
плюс зависимость от текста шаблона. ФИО уже присутствуют в агрегате — в signer_certificate каждой подписи
(DocumentAggregator резолвит их из учётных данных подписанта).
- Новая колонка signed_documents.signers_text — ФИО/наименования всех подписантов пакета
(last/first/middle_name физлиц-ИП и short_name организаций из signer_certificate всех частей
агрегата: заявление/решение/акты/связанные) + их username.
- Заполняется на ingestion и backfill из готового агрегата (collectSignersText).
- searchDocuments ILIKE теперь покрывает signers_text (приоритетно), full_title, content_text, username.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 14m18s
Typecheck / controller (pull_request) Successful in 13m30s
Фундамент C28-21: PG-проекция подписанных документов (таблица signed_documents),
наполняемая ловлей blockchain-событий soviet с внутренней шины и разовым backfill'ом.
Аддитивно — OpenSearch и резолверы (searchDocuments/getDocuments) пока не трогаются.
- SignedDocumentEntity (signed_documents): package/hash/coopname/username/status,
full_title/content_text/html/pdf(bytea), document_aggregate(jsonb), meta, block_num.
- SignedDocumentRepository (+SIGNED_DOCUMENT_REPOSITORY) + TypeORM-реализация (upsert/setStatus/search ILIKE).
- SignedDocumentIngestionService: @OnEvent(action::soviet::{newsubmitted,newresolved,newdeclined})
-> идемпотентный upsert с защитой от понижения статуса; контент из factory-Mongo.
- SignedDocumentBackfillService: разовый идемпотентный скан действий (env-gate SIGNED_DOCS_BACKFILL_ON_BOOT).
- Регистрация в TypeOrmModule + app.module.
newdeclined пока не экспортирован как registry-action в cooptypes — подписка по литералу на будущее.
Следующая итерация: перевод searchDocuments/getDocuments на PG + снос OpenSearch + тесты.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Баг 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>
Typecheck / desktop (pull_request) Successful in 13m36s
Typecheck / controller (pull_request) Successful in 13m14s
Глобальный Omit<IPayment,'id'>&{id:string} ломал store.ts/mergePayments
и features/Payment/SetStatus/api (туда приходит сырой Zeus-вывод с
id:unknown). Откатываю override в entities/Payment/model/types.ts,
сужаю id до string локально в ListOfPaymentsWidget через IPaymentRow —
точка сужения только в UI, контракт стора/API не трогается.
vue-tsc desktop локально: PASS.
Typecheck / desktop (pull_request) Failing after 8m13s
Typecheck / controller (pull_request) Successful in 13m20s
QuestionsTable принимал Cooperative.Document.IComplexAgenda[] (cooptypes,
table:IDecision без certificates, id:IUint64=number|string), а сверху
ListOfAgendaQuestions передаёт IAgenda[] из desktop/entities/Agenda
(GraphQL-вывод getAgenda, table:BlockchainDecision с certificates,
id:number). Перевожу QuestionsTable на IAgenda — это реальная форма
данных из стора; уходит расхождение IComplexAgenda↔IAgenda для
:agenda='row' в QuestionCard (он тоже IAgenda).
IPayment.id был ModelTypes['ID']=unknown (Zeus не имеет scalar-resolver
для ID), из-за чего шаблон ListOfPaymentsWidget падал на expanded.get/.set
и :id-биндингах. Переопределяю id как string через Omit+&.
Typecheck / desktop (pull_request) Failing after 8m42s
Typecheck / controller (pull_request) Has been cancelled
Снижает минимальный порог состава совета кооператива на этапе
Install Coop с 5 до 3 человек. Единственный ограничитель —
контракт soviet::createboard (см. consts.hpp + createboard.cpp:58).
Backend (install.interactor) и desktop (SetSovietForm) собственных
числовых проверок не имеют — фронт требует только «хотя бы одного»,
бэкэнд передаёт массив как есть в blockchainPort.createBoard.
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>
Typecheck / desktop (pull_request) Failing after 8m16s
Typecheck / controller (pull_request) Successful in 13m28s
В CLAUDE.md новый раздел «ДИЗАЙН-КАНОН desktop — ОБЯЗАТЕЛЕН»:
1) HTML-канон ~/blago/production/shared/MONO Design System.html;
2) Живая реализация components/desktop/src/pages/_dev/ui/index.vue.
После компакта забыл, что shared/MONO Design System.html — основной SoT,
и нашёл «канон» в auth-prototype, который НЕ канон — пришлось переверстать
тосты. Фиксирую правило, чтобы не повторять.
Typecheck / desktop (pull_request) Failing after 8m19s
Typecheck / controller (pull_request) Successful in 13m6s
Завершаю аудит — добавил capital по запросу:
- RouteMenuButton получил опциональный icon-проп; на мобильном
превращается в round-иконку + tooltip, на десктопе — flat + label.
- В ProjectPage и ComponentPage прописал material-иконки для каждого
раздела (Описание/Артефакты/Компоненты/План/Участники/История/
Задачи/Голосование/Результаты).
- ImportContributorButton, ImportContributorsButton, FilterDialogWithButton
переведены в canon-micro (isMobile → flat+dense+sm+accent + tooltip).
Заодно вынес q-dialog из q-btn-вложенности.
RouteMenuButton по сути — раздельные таб-кнопки навигации; полноценное
UX-решение для capital (вынести в SecondLevelTabs под шапкой) — отдельная
задача, тут только мобильная читаемость шапки.
Typecheck / desktop (pull_request) Failing after 8m14s
Typecheck / controller (pull_request) Successful in 12m59s
CSS-хак `.topbar__actions .base-btn__label { display: none }` не покрывал
эти две кнопки: они рисуют q-btn напрямую через registerAction (не через
BaseButton под .topbar__actions). В итоге на мобильном «Добавить члена»
и «добавить участок» оставались с лейблами и распирали шапку.
Перевёл обе в canon-паттерн как CreateMeetButton/DepositButton/WithdrawButton:
isMobile → flat+dense+sm+accent, иконка + q-tooltip; без обёртки
.header-action, чтобы кнопка не растягивалась на всю высоту шапки.
Typecheck / desktop (pull_request) Failing after 8m14s
Typecheck / controller (pull_request) Successful in 13m2s
- quasar-canon.css: переписан override .q-notification под реальный
канон (shared/MONO Design System.html → .toast): тёмный фон
(var(--p-ink) для нейтрального; #052e16/#4c0a0a/#3d2400/#0c1e3f
для positive/negative/warning/info), светлый текст, ЦВЕТНАЯ иконка
по типу (--p-pos/--p-neg/--p-warn/--p-info). Раньше ошибочно был
сделан нейтральный фон + left-accent-полоска — «откуда попало»,
не из канона.
- alerts.ts: убран color: 'grey-7'/'positive' у actions — они
наследуют светлый тон от тёмного фона через override.
- _dev/ui секция 39 «Тосты»: пять кнопок для триггера всех типов
тостов (успех, успех с CTA, ошибка, in-app push, push с avatar).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Failing after 8m14s
Typecheck / controller (pull_request) Successful in 13m1s
- SuccessAlert/FailAlert/NotifyAlert (shared/api/alerts.ts): единый
визуал через override .q-notification в quasar-canon.css. Нейтральная
поверхность + 3px accent-полоска по типу (positive/negative/warning
/info), без «цветной заливки карточки» (stop-signal §19.1).
- Позиция тостов bottom-right — было top-right, что противоречило
канону mono-design-system v2 (toast-host: fixed; bottom: 24px;
right: 24px). На мобильнике — full-width минус 16px полей.
- SuccessAlert: type:'positive' вместо color:'primary' — раньше успех
красился брендовым teal, а не позитивным зелёным.
- NotifyAlert: дефолтная иконка notifications, если нет avatar — чтобы
in-app push от Novu визуально совпадал с Success/Fail.
- Topbar @media (max-width:600px): .topbar__actions .base-btn__label
скрыт, остаются только иконки CTA-кнопок страницы. Бренд-название
с ellipsis (max-width: 50vw). Раньше кнопки переносились на вторую
строку на узких экранах.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Typecheck / desktop (pull_request) Failing after 8m22s
Typecheck / controller (pull_request) Successful in 13m12s
CI vue-tsc --noEmit падал на четырёх файлах:
- QuestionsTable.vue: props.decisions типизирован через
PropType<Cooperative.Document.IComplexAgenda[]>. Раньше тип был
плоский Array → row в v-for становился unknown, и template
ругался на row.table.id, isProcessing(row.table.id) и т.п.
- ListOfPaymentsWidget.vue: явный computed items: IPayment[] через
cast (as unknown as IPayment[]) — zeus резолвил items слабее, чем
нужно шаблону, и поля row.id / username / quantity / etc. оставались
unknown.
- ExtensionPage.vue и ExtensionCard.vue: для AutoAvatar :username
добавлен fallback || '' — extension.name / extension.title могут
быть string | null | undefined, AutoAvatar требует string.
ESLint по четырём файлам — EXIT=0; tsc-прогон делегирован CI
(точечный vue-tsc по факту тащит весь проект и вешает машину, см.
memory feedback_no_full_vue_tsc_desktop).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Без 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>
Typecheck / desktop (pull_request) Successful in 13m48s
Typecheck / controller (pull_request) Successful in 13m9s
matrixRoomId — это одновременно идентичность и capability: зная его, можно
попытаться войти в комнату. Раздавая его на фронт (страница «Комнаты секретаря»
и список транскрипций отдавали его в каждой строке), мы по сути публиковали
handle для входа в приватные комнаты любому, кто видит раздел. Приватность тогда
теряет смысл.
Комнаты секретаря:
- ChatcoopSecretaryRoom и RemoveSecretaryRoomInput переведены с matrixRoomId на
внутренний opaque id реестра (DB-ключ, по нему войти в Matrix-комнату нельзя);
- удаление резолвится по id → matrixRoomId на бэкенде; мутация больше не
возвращает matrixRoomId;
- фронт: row-key/удаление по id, убран показ matrixRoomId в таблице;
- попутно фикс латентного бага из PR #34: parseKind не знал про 'secretary' и
мапил такие комнаты в capital_project — из-за чего editable=false и удаление
падало с «удалять можно только комнаты секретаря».
Транскрипции:
- CallTranscription больше не отдаёт matrixRoomId (на фронте он не отображался,
но уходил в payload по всем комнатам, включая приватные). Остаётся roomId
(LiveKit room name — внутренний, не Matrix-handle);
- input-фильтр GetTranscriptionsInput.matrixRoomId сохранён: это вход, не утечка,
и им пользуется blago-cli;
- blago-cli: рендер транскрипций больше не зависит от tr.matrixRoomId (строка
«Matrix room» убрана, иначе pull и restore рассогласовались бы — restore не
имеет контекста комнаты). Заодно поправлен fallback курсоров.
Вне scope намеренно: chatcoopListNonProjectCommunicationRooms и per-room запросы
оставлены с matrixRoomId — их зовёт только blago-cli (доверенный инструмент
председателя), на фронт они не уходят, и matrixRoomId там — операционный ключ
синхронизации.
logger.ts: при CONTROLLER_SCHEMA_GEN не подключаем файловые транспорты winston
(codegen рядом с работающим контейнером падал на EACCES в root-овый logs/).
schema.gql + zeus (sdk и controller) перегенерированы и закоммичены — иначе CI
typecheck собирает sdk против старого zeus и падает.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Прошлый вариант разъезжался: «Скопировать» висел сиротой справа, провалы
между блоками, узкая потерянная кнопка, карточка 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>
Typecheck / desktop (pull_request) Successful in 13m19s
Typecheck / controller (pull_request) Successful in 12m49s
generate-client пишет копию zeus и в components/controller/zeus — обновляю вместе
со schema.gql, чтобы не оставлять дрифт сгенерированных артефактов.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 13m29s
Typecheck / controller (pull_request) Successful in 13m0s
generate-schema + generate-client после добавления резолверов SecretaryRooms и
chatcoopListNonProjectCommunicationRooms. Без этого @coopenomics/sdk:build падал
в CI на отсутствующих в zeus типах (ChatcoopSecretaryRoom / CreateSecretaryRoomInput
/ RemoveSecretaryRoomInput / ChatcoopNonProjectCommunicationRoom) — общий build-шаг
обеих джоб typecheck. Локально sdk build (prebuild tsc --noEmit) зелёный.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Typecheck / desktop (pull_request) Failing after 7m26s
Typecheck / controller (pull_request) Failing after 6m28s
- entity SecretaryRoom (api/model/store) поверх SDK chatcoopListSecretaryRooms /
chatcoopCreateSecretaryRoom / chatcoopRemoveSecretaryRoom
- SecretaryRoomsPage: список комнат реестра (тип, наличие секретаря, шифрование),
создание комнаты (публичная/приватная) и удаление комнат секретаря
- маршрут chatcoop-secretary-rooms, доступ роли chairman+member
- системные/проектные комнаты — read-only (удаление только у kind secretary)
Затипизируется после generate-schema/generate-client (новые SDK-операции).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Задача 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>
Build bootstrap container / build (push) Successful in 3m14s
Все три reboot-скрипта теперь идентичны, кроме строки pnpm run boot[:extra|:clean]:
- wipe blockchain-data контейнером (docker run alpine rm) вместо sudo rm —
работает без sudo на любой ноде независимо от владельца данных (на Pi нет
passwordless sudo; на проде nodeos пишет данные под root). Единый способ с reboot.sh.
- monoredis в down -v и up -d — без него coopback падает на старте
(getaddrinfo EAI_AGAIN monoredis → nodemon crash), и провайдер не получает
org-данные partner1.
- coopback через up -d --force-recreate (перечитывает env), как в reboot.sh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Build bootstrap container / build (push) Successful in 3m10s
- booter.ts: installExtraData (регистрация partner1 с auto-approve → триггер аренды
провайдером) выполняется только под EXTRA_RENT=1. boot:extra используется и для
других задач (пересев совета/чейна), где аренда VM не нужна.
- infra.ts: partner1 подписывает wallet-соглашение (wallet::signagree, program_id=1)
при регистрации. Без членства в ЦПП Кошелька provider.performInitialTransfers
(150 AXON на partner1 перед ACTIVE) падает ассертом eosio.token::is_can_transfer
«Получатель не является участником целевой потребительской программы кошелька»,
и инстанс навсегда висит в INSTALL.
- blockchain/index.ts: activateFeature не роняет boot при protocol_feature_exception
(фича уже активна на не-обнулённом чейне) — ловим и продолжаем.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Typecheck / desktop (pull_request) Successful in 13m28s
Typecheck / controller (pull_request) Successful in 13m9s
Штатный синк вместо точечной правки: schema.gql приведён в соответствие с
актуальными DTO контроллера (был протухший — накопленный дрейф полей,
не только blocked). zeus перегенерирован из новой схемы; оба
controller/zeus и sdk/src/zeus идентичны.
Результирующие изменения относительно L3-blocked-ветки:
- удалён blocked из Ledger2Wallet/ProgramWallet (цель PR);
- синхронизированы ранее не закоммиченные дрейфы: is_server_init в SystemInit,
enum-значение AWAITING_AUTHORIZATION, актуализация описаний/ролей @Field.
- blocked сохранён в ChartOfAccountsItem (legacy chart-of-accounts).
Проверено локально: sdk typecheck и controller typecheck зелёные.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Кошельки (по фидбэку — переносы выглядят плохо):
- .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>
Typecheck / desktop (pull_request) Successful in 13m29s
Typecheck / controller (pull_request) Successful in 13m8s
CI Typecheck падал на @coopenomics/sdk:build (tsc): селекторы
programWalletSelector/ledger2WalletSelector и зависимые capital-селекторы
больше не содержат blocked, но zeus-типы (MakeAllFieldsRequired) всё ещё
требовали его как обязательное поле.
- schema.gql: убран `blocked: String!` из типов Ledger2Wallet и ProgramWallet
(точечно, без полной регенерации — она тянет несвязанный дрейф схемы).
- zeus (controller/zeus + sdk/src/zeus): убран blocked из Ledger2Wallet и
ProgramWallet во всех 4 секциях (ValueTypes/ResolverInputTypes/ModelTypes/
GraphQLTypes). blocked в ChartOfAccountsItem (legacy chart-of-accounts) и
статус-комментарии (accepted|blocked) сохранены.
- CoopCard.vue: удалён мёртвый закомментированный блок с wallet.blocked.
Проверено локально: sdk typecheck и controller typecheck зелёные.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Typecheck / desktop (pull_request) Failing after 6m36s
Typecheck / controller (pull_request) Failing after 6m30s
Упраздняем субсчёт "заблокировано" на кошельках. Резерв средств под заявку
на возврат паевого теперь выражается переводом на отдельный COOPERATIVE-кошелёк
w.wal.wpend, а не блокировкой внутри w.wal.share. Все движения — только переводы
и сжигание, без BLOCK/UNBLOCK.
Контракты (ledger2/wallet/capital):
- wallets.hpp: новый кошелёк WITHDRAW_PENDING (w.wal.wpend, COOPERATIVE).
- operations.hpp: из WalletOp удалены BLOCK/UNBLOCK/BURN_BLOCKED (числовые
значения ISSUE/TRANSFER/BURN/NONE сохранены для совместимости истории).
Переключены 3 операции возврата: o.wal.wthreq -> TRANSFER share->wpend;
o.wal.wthdec -> TRANSFER wpend->share; o.wal.wthcpl -> BURN wpend.
- walletop.cpp/revert.cpp: удалены кейсы и валидации блокировки.
- migrate(): свёртка blocked->available по ВСЕМ коопам (выполняется автоматически
при деплое; сигнатура без аргументов). Поле blocked в структурах таблиц
оставлено deprecated — физическое удаление = смена layout таблицы на живых
коопах, отдельный cleanup-деплой.
- capital balances/importcontr: больше не читают blocked.
- p.wal.wthdrw.standard.yaml: стандарт приведён к модели "резерв на кошельке".
cooptypes/SDK:
- operations.ts: WalletOp без BLOCK/UNBLOCK/BURN_BLOCKED; 3 операции возврата
переключены; wallets.generated.ts регенерён (добавлен w.wal.wpend).
- selectors: убран blocked из programWalletSelector/ledger2WalletSelector.
Backend (controller): убрано поле blocked из GraphQL DTO ProgramWallet и
Ledger2Wallet. TypeORM-колонка/внутренние интерфейсы оставлены deprecated
(без DB-миграции).
Frontend (desktop): убраны все поля "Заблокировано" — WalletProgramWidget,
WalletWidget, ParticipantWalletsPage, CoopWalletsPage, CapitalWalletsCardsWidget,
ContributorsListWidget, optimistic blocked_delta.
Границы (не в этом PR): legacy soviet::progwallets blockbal/unblockbal и
donor-marketplace; legacy ledger v1 chart-of-accounts "Заблокированные средства";
полное удаление поля blocked из C++-структур таблиц.
Требует CI-шага: regen GraphQL schema + zeus client (generate-schema/-client)
для согласования schema.gql и zeus-типов с удалённым полем blocked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Корневая причина: при pull конфликт объявлялся по условию
`dirty && remoteUpdatedAt !== prev.remote_updated_at`, где `dirty`
определяется сравнением raw-байтового sha файла с content_etag_local.
Серверные метки времени (updated_at/created_at) входят и во frontmatter
файла, и в etag. Когда сервер бьёт _updated_at родителя при дочерней
мутации (создание/удаление issue/story), etag в индексе расходится с
файлом ровно на строку updated_at — файл считается «грязным», а при
изменившемся remote_updated_at pull пишет маркеры слияния на весь файл,
хотя содержательно текст идентичен. Подтверждено: подстановка
remote_updated_at в файл воспроизводит etag байт-в-байт (58 записей на
проде voskhod).
Фикс: в syncEntityFile перед записью маркеров сравниваем локальный и
серверный тексты в каноне без updated_at/created_at. Если совпадают —
это не конфликт: принимаем серверную версию и лечим etag, без маркеров.
Реальная правка тела/заголовка по-прежнему даёт маркеры.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why: на странице без логина логотип был <img :src='logo.svg'> — img рендерит
SVG в изоляции, не наследует currentColor, поэтому показывался чёрно-белым
и не реагировал на смену темы. В личном кабинете (WorkspaceSwitcher)
тот же logo.svg рендерится inline через v-html в зелёном квадрате
и наследует color (logo.svg на fill:currentColor) — «зелёненький
на зелёном фоне», одинаковый в обеих темах.
What: CommonHeader brand-slot переведён на тот же приём —
`logo.svg?raw` + v-html внутри .app-q-header__logo
(background var(--p-primary-soft), color var(--p-primary), 28px квадрат,
16px svg). Переключение темы больше не требуется — зелёный константен.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Trial run #148 (PR #30) упал в job controller на 37 TS2307 «Cannot find
module» для @coopenomics/{sdk,inter,notifications}. Локально маскируется
тем, что controller стартует через ts-node, который резолвит .ts напрямую
через tsconfig-paths/pnpm-симлинки и не зависит от dist/. tsc же ищет
types из package.json целевого пакета — а оно показывает на dist/index.d.ts
из unbuild.
Замена локального scoped-build на полный `pnpm lerna run build` (как в
корневом Dockerfile builder-стадии) собирает граф целиком и устойчиво
к добавлению новых workspace-пакетов. Добавляет ~2-3 мин ко времени job'а,
end-to-end оценочно ~21 мин (был 18 на сломанной версии).
build образов в release.yaml не ловит TS-ошибки: quasar build идёт через
esbuild с выключенным vueTsc в vite-plugin-checker (см. quasar.config.cjs),
а у controller'а build-скрипта вообще нет — `lerna run build` его молча
пропускает, в проде ts-node стартует и валится на типах только в рантайме.
Поэтому битый тэг мог уехать в DockerHub (кейс PR #392 / rename 1080→1020
в cooptypes — TS2551 в distribution-management проскочил именно так).
Новый reusable workflow .github/workflows/typecheck.yaml:
- desktop: build cooptypes/factory/sdk → quasar prepare → vue-tsc --noEmit --skipLibCheck
- controller: build cooptypes/factory → pnpm typecheck (tsc --noEmit)
- триггер pull_request на dev + workflow_call
В release.yaml добавлен job typecheck (uses: ./.github/workflows/typecheck.yaml),
release-job получил needs: typecheck. Downstream publish-packages /
publish-docs / trigger-coopenomics-docs через release автоматически в цепочке.
Push в dev/testnet/main НЕ триггерит — PR-гейта достаточно, тэги покрыты
через workflow_call. Если vue-tsc упрётся в OOM/таймаут на ubuntu-latest,
fallback на `pnpm --filter @coopenomics/desktop run typecheck` (без SFC).
PR #27 включил transpileOnly через "ts-node" блок в tsconfig.json
для ускорения cold-start dev. Это сломало TypeORM на старте:
DataTypeNotSupportedError: Data type "Object" in "TokenEntity.type"
is not supported by "postgres" database.
Корень: transpileOnly режим ts-node использует ts.transpileModule,
один файл за раз без TypeChecker. Cross-file type aliases — типа
`import type { TokenType } from '~/types/token.types'` где
`TokenType = (typeof tokenTypes)[keyof typeof tokenTypes]` — без
type-checker'а **не резолвятся**. design:type metadata
для `@Column() type!: TokenType` записывается как `Object` вместо
`String`. TypeORM пытается создать колонку Object → unsupported.
Проблема структурная: множество TypeORM Entity в controller'е
используют `import type {SomeAlias}` + `@Column() field: SomeAlias`,
полагаясь на полный type-resolve в metadata-emit. Без явного `type:`
в каждом @Column переход на transpileOnly / SWC невозможен.
Возвращаемся к полному ts-node (cold-start 60+ сек, как было до
PR #27). @swc/core / @swc-node/register остаются в devDeps —
безвредны, не используются. Smoke-suite tests/unit/_swc-readiness/
остаётся как regression-net для будущих попыток (тестирует
emitDecoratorMetadata инвариант).
Будущий путь к ускорению (отдельная задача):
1. Пройтись по всем TypeORM Entity, добавить explicit type: в
каждый @Column — снимет зависимость от cross-file metadata.
2. ИЛИ перевести TokenType-подобные type-aliases в enum (value-
import) — runtime binding позволит metadata эмититься корректно.
3. Тогда transpileOnly / SWC заработают без regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Локально гоняем mongo как standalone. Раньше был --replSet rs0 +
rs.initiate в entrypoint, но проект не использует ни transactions
(нет startSession), ни change streams (нет .watch()), ни
readConcern:'majority' — replica-set серверно избыточен. Клиенты
(mongoose в controller, MongoClient в parser, notifications) уже
подключаются по URL без replicaSet=/directConnection= параметров,
для них переход прозрачен.
Бонусы:
- pnpm run reboot больше не висит на «MongoDB еще не готов
(нет PRIMARY)»: на arm64 sleep 5 в entrypoint не успевал поднять
mongod до того, как rs.initiate пытался выполниться, и rs.status()
внутри try/catch ловил неправильную ошибку — replica config не
применялся, oplog.rs не создавался, PRIMARY никогда не наступал
(инцидент 2026-05-23).
- Старт mongo на 5-10 сек быстрее.
- Меньше состояния в volume — нет oplog/replica-config.
reboot.sh: ждать db.adminCommand({ping:1}) вместо db.hello().isWritablePrimary.
Прод-конфигурация (k8s/swarm) этого файла не использует — там
своя replica-схема для HA, не затронута.
Если в будущем потребуются transactions — вернуть --replSet rs0
+ rs.initiate в entrypoint обратно. Триггер: появление в коде
session = await mongoose.startSession() или .watch().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why: предыдущий фикс `overflow-wrap: anywhere; word-break: break-word`
ломал слова в любом месте даже когда колонка достаточно широкая —
«Минимальный неснижаемый остаток» рендерился по одному слову на строку,
несмотря на ~480px доступной ширины. Реально нужен только wrap по пробелам;
agressive break-word оправдан только для URL-подобных нерасчленяемых строк.
What: убраны `overflow-wrap` и `word-break` из .wallet__title/__sub —
браузер делает естественный wrap по пробелам, длинные заголовки переносятся
только когда не помещаются. `.wallet--row` reset для compact-варианта
оставлен — там по-прежнему single-line+ellipsis.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: canon-стиль .wallet__title и .wallet__sub был с
`white-space: nowrap; overflow: hidden; text-overflow: ellipsis;` — на full-
варианте это резало длинные подписи («Минимальны…», «Возвращается п…»)
даже на просторных экранах, потому что grid-колонка `.wallet__main` сжимается
ради `.wallet__amount` справа. Эта обрезка будет всплывать на любой
длинной строке (метки программ, статусы пайщика, длинные subtitle).
Compact-вариант `.wallet--row` (слот шапки) должен остаться одной строкой.
What:
- .wallet__title/__sub: убран nowrap/ellipsis; добавлено
`overflow-wrap: anywhere; word-break: break-word; hyphens: auto` (для title)
и `overflow-wrap: anywhere; word-break: break-word` (для sub) — длинные
заголовки переносятся на 2+ строки.
- .wallet--row: явно возвращает nowrap+ellipsis (с reset word-break/
overflow-wrap), чтобы compact-ряды в шапке оставались строго одной строкой.
- WalletProgramWidget: карточка минимального неснижаемого остатка
перемещена в начало grid'а (была после программ) — это базовая защита
средств пайщика, логичнее видеть её первой.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Слияние в release.yaml через jobs+needs (последовательно):
- release (контракты+контейнеры+webhook, как раньше)
- publish-packages (если не -alpha) — был publish-packages.yaml
- publish-docs (не -alpha + main) — был publish-docs.yaml
- trigger-coopenomics-docs (не -alpha + main) — был build-contracts-docs.yaml
Удалено:
- build-contracts.yaml (ручной workflow_dispatch, сознательно вынесен из
релиз-пути после инцидента 2026-05-13, пользователем подтверждено удаление)
- publish-packages.yaml, publish-docs.yaml, build-contracts-docs.yaml
(содержимое перенесено в release.yaml как зависимые jobs)
Гейты унифицированы на !contains(github.ref, '-alpha') во всех publish-*
и trigger-* (раньше publish-docs/build-contracts-docs резали ещё
-beta/-rc/-test). IS_PROD в release-job остаётся на (alpha|beta|rc|test)
намеренно — webhook продакшна и тэг :latest должны быть строже.
Telegram-уведомления удалены из release.yaml и build-bootstrap.yaml.
Полагаемся на дефолтные email-уведомления Gitea-инстанса (mailer ENABLED=true).
Секреты TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID после merge можно удалить из репо.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why:
- Заголовок «Кошелёк» для program='wallet' путал — в столе пайщика этот
кошелёк семантически главный, и в любых других местах canon (MicroWallet,
WalletCardMini, _dev/ui) он тоже должен называться полно — «Главный
кошелёк». Лучше один canon-default, чем локальный override в каждом
потребителе. Согласовано — переименование canon DEFAULT_TITLES.
- Минимальный неснижаемый остаток — НЕ баланс кошелька, а самостоятельная
сущность пайщика (паевой взнос, возвращается при выходе). Пристегивать
его DataRow-строкой под grid'ом — нелогично, как было и раньше в legacy.
Правильнее — отдельной карточкой в той же сетке кошельков.
What:
- WalletCard.vue: DEFAULT_TITLES.wallet 'Кошелёк' → 'Главный кошелёк'.
Глобально для всех потребителей canon-компонента.
- WalletProgramWidget.vue: убран TITLE_OVERRIDE и DataRow-блок минимального
остатка. Карточка остатка теперь рендерится в общем .wallet-programs
grid'е canon-разметкой `.wallet` с иконкой `savings`, заголовком
«Минимальный неснижаемый остаток», подзаголовком «Возвращается при выходе
из кооператива» и зарезервированной суммой. Нейтральная подсветка иконки
через `.wallet--minimum { --prog-bg, --prog-fg }`, чтобы визуально
отличалась от программ, но встала в общую сетку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Полный SWC через ts-node наткнулся на TDZ-ловушку при загрузке
src/extensions/1ccoop/oneccoop-extension.module.ts:
ReferenceError: Cannot access 'OneCoopPlugin' before initialization
at Object.get OneCoopPlugin
at oneccoop-secret-key.guard.ts:83 (line in transpiled SWC output)
Корень: circular import между oneccoop-extension.module.ts и
guard.ts (guard импортирует OneCoopPlugin для @Inject(forwardRef(...))
+ type annotation; module импортирует guard для providers). SWC
эмитит `Reflect.metadata("design:paramtypes", [OneCoopPlugin])`
при class-declaration `@Injectable()` — это runtime-обращение к
OneCoopPlugin до того, как oneccoop-extension.module.ts закончил
инициализацию. tsc/CommonJS-loader прощает (hoisted exports +
Object.defineProperty(get) для late-binding), SWC по строгой
ES-семантике — нет.
Масштаб системный, не локальный: 32 файла в src/ используют
forwardRef(() => Class), много из них в pattern @Inject + type
annotation в конструкторе. Под SWC каждый такой файл —
потенциальный TDZ. Чинить по одному (import type + lazy require
в forwardRef) — десятки правок с риском уронить тип-safety.
Решение: остаёмся на ts-node --transpileOnly. Тот же 25x cold-start
(~1 сек на пробе), tsc-семантика прощает циклы, никаких code-changes.
@swc/core и @swc-node/register оставляем в devDependencies —
готовы к будущему полному SWC, когда отдельной задачей разорвём
forwardRef-циклы (заменить class-on-class @Inject через string/Symbol
токены, либо вынести типы в отдельные файлы без cycle).
SWC-options через .swcrc как module.lazy:true проверены — не лечат,
проблема в decoratorMetadata, не в импортах.
Smoke-suite tests/unit/_swc-readiness/ — 4/4 PASS на transpileOnly,
emitDecoratorMetadata через barrel-import сохраняется.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why: на старом WalletWidget пайщик видел свой минимальный неснижаемый остаток
(паевой взнос, возвращается при выходе из кооператива) — самостоятельная
сущность пайщика, не баланс кошелька. При переходе на canon я её упустил.
Канон-default WalletCard 'wallet' = «Кошелёк» — для стола пайщика этот
кошелёк семантически является главным (свободный остаток ЦК), поэтому
локально перекрываем заголовок на «Главный кошелёк».
What:
- TITLE_OVERRIDE['wallet'] = 'Главный кошелёк' — локальное перекрытие
заголовка только в этом widget'е, canon DEFAULT_TITLES не трогаем
(другие потребители WalletCard могут использовать общий «Кошелёк»).
- DataRow «Минимальный неснижаемый остаток» под grid'ом программ
(только когда session.participantAccount.minimum_amount > 0)
с hint «Возвращается пайщику при выходе из кооператива».
- Источник остатка — session.participantAccount?.minimum_amount,
как и в legacy WalletWidget.
Note: locked-line уже рендерится самим canon WalletCard, когда
locked-balance > 0 (logic: hasBlocked ? blocked.amount : undefined
в WalletProgramWidget). Отдельная разметка под «Заблокировано» не нужна.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: type predicate `e is CanonProgramEntry` не сходился — optional `locked?: string`
в interface vs required `locked: string | undefined` в литерале map'а. TS считает
эти типы несовместимыми для predicate, хотя они эквивалентны при присваивании.
What: переход на `flatMap<CanonProgramEntry>` с возвратом `[]` для исключаемых
программ. Predicate не нужен, generic flatMap даёт точный тип результата.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Финал миграции: вместо ts-node --transpileOnly теперь полный SWC
(Rust-транспайлер) через ts-node "swc" mode. Установлены
@swc-node/register@^1.11.1 и @swc/core@^1.15.33 в controller.
tsconfig "ts-node" → { "swc": true, "files": true } (вместо
transpileOnly). Все 14 ts-node-вызовов очищены от --transpileOnly
(избыточно при swc).
Замер:
- ts-node + tsc (baseline): 26+ сек cold-start на простом скрипте
- ts-node --transpileOnly (Node 20): ~1.0 сек
- ts-node + swc (Node 22): ~1.0 сек (та же скорость, но
без потери типов — SWC всё ещё транспайлит, просто на Rust)
На полном controller-проекте dev-cold-start раньше был 60+ сек,
теперь — secunda-уровень (точно замерить можно только перезапустив
coopback в контейнере).
Node 22:
- nvm alias default 22 (Node 22.22.3 — требование Quasar 2.5.2,
заодно убрал quasar-prepare warning при pnpm install).
- libxmljs2 native binary теперь совместим с runtime (на Node 20
падал tests/unit/reports на NODE_MODULE_VERSION mismatch).
.npmrc в корне (`store-dir=./.pnpm-store`) — фиксирует store в
монорепе. Корень проблемы был: docker-coopback запущен от root и
писал в `/app/.pnpm-store` (= host's /home/ant/mono/.pnpm-store),
host-pnpm дефолтно искал в ~/.local/share/pnpm/store — отсюда
ERR_PNPM_UNEXPECTED_STORE. Relative-path в .npmrc устраняет
mismatch навсегда: и хост, и контейнер видят store через тот же
relative-путь от repo root.
Smoke-suite tests/unit/_swc-readiness/ — 4/4 PASS на SWC, что
формально подтверждает: emitDecoratorMetadata через barrel-import
сохраняется (Nest DI, class-validator, @ValidateNested + @Type все
получают правильные design:type/paramtypes из SWC).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why: первая страница стола пайщика (default route 'wallet') использовала legacy
обвязку — CardStyles import, scoped SCSS с .body--dark и hardcoded rgba цветами,
ColorCard с произвольным цветом по индексу. Канон уже знает программы платформы
(blagorost/wallet/generator) через WalletCard + токены --prog-*; на нём и строим.
What:
- WalletPage.vue: убран import 'src/shared/ui/CardStyles', scoped SCSS на
токенах --p-6/--p-4; вырезана легаси-карточка «Минимальный остаток»
(она не отображалась — не было разметки в template). useHeaderActions
для Deposit/Withdraw оставлен — канон поддерживает actions через #actions slot.
- WalletProgramWidget.vue: переписан на canon WalletCard + EmptyState.
Фильтр на канон-набор программ через ZEUS_TO_CANON
(MAIN→wallet, BLAGOROST→blagorost, GENERATOR→generator);
MARKETPLACE и прочие исключены — это не из основной тройки платформы
и должны рендериться отдельным виджетом.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: на главную без логина нужно показывать бренд кооператива (логотип + имя)
рядом с глобальными действиями шапки; раньше выводилось только текстовое
название через title-проп без логотипа.
What:
- AppHeader.vue: опциональный slot #brand перед .topbar__crumb;
hasBrand computed по slots.brand. Когда slot заполнен — крошка не рендерится.
- components.css: стили .topbar__brand (desktop + .topbar--mobile вариант)
с canon-токенами (--p-fs-body, --p-ink, --p-fs-meta).
- CommonHeader.vue: на !loggedIn заполняет #brand src/assets/logo.svg
+ <b>{coopTitle}</b>; передача title-пропа убрана.
- default.vue: .fixed-top-right { top: 51px } → top: var(--p-topbar-h)
(canon 56px) — выравнивание FAB под точную высоту шапки.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cooptypes/test/wallets-registry.snapshot — `human_name` для
w.wal.wthdrw в src/ledger2/wallets.generated.ts получил суффикс
"(deprecated, не используется в новых операциях)" (видимо, при
свежей регенерации из C++), но snapshot не обновили. Updating.
controller/tests/unit/process-registry — apply-anchor data поле
переименовано action_code → operation_code в самом сервисе
(src/domain/process-registry/services/process-registry.service.ts),
вместе с константой ACTION_CODE_TO_PROCESS_TYPE → OPERATION_CODE_TO_PROCESS_TYPE.
В тесте оставались старые имена → 1 из 6 кейсов падал на mismatch
regex'а сообщения ошибки. Переименовываю action_code → operation_code
во всём файле и обновляю regex (f2).
Что НЕ починено в этом коммите (pre-existing, не моё):
- 5 case'ов (a..e) в том же файле всё ещё падают: они используют
operation_codes 'sov.axncnv' / 'cap.act2shr' / 'cap.act2ln' /
'wall.depcpl' / 'reg.entrfee' / 'reg.minshare' / 'mig.opncash',
которых нет в Ledger2.LEDGER2_OPERATION_REGISTRY (cooptypes).
Test fixtures устарели относительно текущей канонической
ledger2-онтологии (canonical имена — o.cap.crtnma, o.cap.dbtwrf,
o.cap.lend, o.cap.repay, o.reg.payent, o.reg.putmin и т.п.).
Этот тест-suite надо переписать под актуальный operation registry
отдельным PR (требует знания canonical naming kanon).
Падает и на dev (без моих правок) — это pre-existing tech debt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cold-start controller dev (`pnpm dev`) сейчас 60+ секунд из-за полной
TypeScript-компиляции в ts-node на каждый рестарт nodemon. Решение —
добавить --transpileOnly во все 14 ts-node-скриптов и зафиксировать
это в tsconfig "ts-node" блоке. Транспайл-онли пропускает type-check,
оставляет только TS→JS transform (с сохранением emitDecoratorMetadata).
Замер на standalone-пробе с barrel-импортом + Reflect.getMetadata
(tests/unit/_swc-readiness/transpile-only-probe.ts):
- baseline (полный ts-node): 26+ сек (показал тест ts-jest)
- ts-node --transpileOnly: ~1 сек
Ускорение ~25x на cold-start. dev / start / migration:* / init:* /
analyze:modules / generate-schema — все на --transpileOnly.
Типы продолжаем проверять отдельно: `pnpm typecheck` (tsc --noEmit
по этому же tsconfig.json) — это и так стоит делать перед коммитом
по правилам проекта.
Safety-net — smoke-suite tests/unit/_swc-readiness/swc-readiness.test.ts:
ловит главный риск SWC/transpile-only регрессий — потерю metadata на
barrel-импортах (поведение тех же кодпутей, что и в реальных Nest-
сервисах с injection через @Inject и class-validator DTO).
- ServiceB DI через barrel: design:paramtypes = [ServiceA] (не Object)
- @ValidateNested + @Type через barrel: nested design:type = NestedPayload
- Nest Test.createTestingModule резолвит сервисы через barrel-import
- class-validator/class-transformer не теряют type info
Если позже понадобится **полный SWC** (Rust-транспайлер, ещё в разы
быстрее) — добавить @swc/core + @swc-node/register в devDependencies
и переключить ts-node на swc-режим:
1. `pnpm add -D @swc/core @swc-node/register -F @coopenomics/controller`
2. В tsconfig.json "ts-node" → добавить `"swc": true` либо
использовать `node --import @swc-node/register/esm-register` в скриптах.
Этот шаг не делается сейчас, так как install требует sudo на shared
pnpm-store (root-owned /home/ant/mono/.pnpm-store/v10) — пользователь
утром может сделать руками.
tsc --noEmit зелёный за 32 сек, smoke-suite + 1 case onboarding-ttl + 1 case
onboarding-steps-registry — все зелёные на baseline и после изменения
tsconfig.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
В dev-режиме vue-tsc через vite-plugin-checker удерживал постоянную
100% загрузку CPU и 2–4 GB RAM на больших Vue 3 + Quasar проектах
(Milkdown / BPMN-js / VueFlow / Mermaid / OpenLayers). Каждое
сохранение запускало полный re-typecheck в фоне, что в долгих
сессиях выглядело как утечка памяти и вешало машину.
Типы продолжаем гонять отдельно: `pnpm typecheck` (tsc --noEmit
--skipLibCheck) и через Volar в IDE. eslint в checker'е остаётся —
он лёгкий и полезен для overlay'я.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Существующая модель cmdk в проекте (entities/CmdkMenu/model/store.ts) —
иерархия рабочих столов и их страниц. Активный стол sticky сверху с
бейджем «Активный», без запроса показывается иерархия (стол + indented
страницы), с запросом — плоский список со столом-префиксом у каждой
страницы; стол отдельной строкой появляется только если запрос явно
начинается с его имени или содержит «стол»/«workspace».
Переписал canon CommandPalette под эту модель:
- Props: `workspaces: CommandPaletteWorkspace[]` вместо
`commands: CommandItem[]`. Каждый workspace = `{ name, title, icon,
isActive?, pages: CommandPalettePage[] }`. Page = `{ name, title,
icon?, shortcut? }`.
- Emits: `select-workspace(name)`, `select-page(workspaceName, pageName)`
— props-only, навигация и filtering по ролям/conditions остаются
заботой connected-обёртки (миграция legacy CmdkMenu — отдельная story).
- Sticky-баннер активного стола, plus accent-soft фон + outline-обводка
на selected, ↑↓ работает плоско поверх иерархии (стол → страницы → стол
→ страницы).
Mock-data в /_dev/ui/index.vue обновлён: три стола (Председатель/Пайщик/
Отчётность) со своими страницами вместо плоского списка команд.
Старый widgets/Desktop/CmdkMenu пока живёт параллельно — переключим в
ходе миграции Wave 3.
Реализованы три props-only доменных компонента из E11:
- NotificationCenter — panel-content для popover в шапке: группировка
notifications по category (system/financial/voting/message), unread-bullet
через BaseBadge, кнопка «Прочитать все», EmptyState и «Показать все».
Relative-date форматирование с русским склонением.
- CommandPalette — ⌘K/Ctrl+K с fuzzy-поиском, секции recent/pages/actions,
↑↓ навигация, Enter/Esc обработка. localStorage недавних — в connected
обёртке, компонент props-only.
- DetailsDrawer — side-sheet справа 480px (override через :width), slots
default/actions/footer, Esc и backdrop close, на xs — fullscreen.
Все три зарегистрированы в boot/ui.ts и локально импортированы в /_dev/ui
с mock-data в секциях 36-38.
E11.4 RailUserCard отложен из-за конфликта имён — существующий компонент
имеет другую роль; нужно согласовать канон-нейминг.
Заменил `.full-width.text-center` + flex без выравнивания на flex-column
со `min-height: 360px` — лоадер больше не прижат к левому верху карточки.
Заодно поправил опечатку «подговка» → «Формируем документ».
Корень: я положил символ валюты в #append slot своим <span>, в обход
встроенного prop suffix. Quasar имеет правила позиционирования именно
для родного .q-field__suffix (см. revert d0a8dc0080 от 2026-05-19,
где было решено оставить Quasar дефолт — «нас устраивает»). Мой span
в append-slot не подчинялся этим правилам и сидел в произвольной
позиции относительно цифр.
Фикс:
- :suffix='symbol' — символ валюты идёт нативным механизмом Quasar.
- Удалил .amount-input__symbol класс и template #append вообще.
- Удалил override align-items: center на q-field__control/__append/
__after (тоже мешал, как было показано в d0a8dc0080).
- font-weight 500 на цифрах оставил — нормальный вес поля.
Совет на будущее: использовать встроенные q-input props (suffix,
prefix), а не #append/#prepend slots, если можно — Quasar для них
держит готовое выравнивание, проверенное пользователем.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Корень: font-weight: 600 + tabular-nums + Quasar dense нативный input
имеют чуть смещённую baseline относительно append-слота, где сидит
RUB. Визуально цифры лежали ниже суффикса.
Фикс:
- font-weight 600 → 500 (нормальный вес поля ввода, без bold-акцента
на цифрах).
- Явный font-size + line-height на нативном input.
- align-items: center на q-field__control / __append / __after, чтобы
суффикс и any after-слот (кнопка «макс») центрировались по высоте
входной полосы.
- align-self: center на самом __symbol — на случай если q-field__append
кто-то переопределит как stretch.
TODO на следующий подход: BaseDocument loader («Формируем документ…»)
выравнивать по центру рамки документа (сейчас стоит сверху).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
getStatusLabel() требует Record<PaymentStatusEnum, string>, AWAITING_AUTHORIZATION
был добавлен в enum, но забыт в локальном маппинге → ts-node краш в dev.
Build bootstrap container / build (push) Failing after 7m23s
`init/cooperative.ts` и `init/participant.ts` импортировали
`signProgramAgreement` из `tests/wallet/`, который на верхнем уровне делал
`import { expect } from 'vitest'`. При запуске `esno src/index.ts boot`
vitest падал с «Vitest failed to access its internal state», потому что
исполнялся вне vitest-воркера.
- Перенёс реализацию в `init/sign-program-agreement.ts`,
`expect(...).toBeDefined()` заменил на обычные `throw new Error(...)`.
- `getCoopProgramWallet` инлайнил (через `walletUtils` тащился второй
module-level `import expect from 'vitest'`).
- `tests/wallet/signProgramAgreement.ts` теперь реэкспортирует из init/,
чтобы существующие тесты продолжали работать.
- Заодно убрал дохлый `import { describe, expect, it } from 'vitest'` из
`init/participant.ts` (символы в файле не использовались) + неиспользуемые
axios/Registry/sendPost.../sleep/GOVERN_SYMBOL/SYMBOL.
[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>
- WrappedEditor (q-field+Editor) на странице транскрипции выглядел сломанным: floating-label «Заметка о звонке» падал поверх первой строки memo, кнопка «Сохранить» торчала сбоку без выравнивания, рамки редактора не было.
- Перешёл на голый Editor (Milkdown через src/shared/ui/Editor) в собственной карточке: border-radius:10px, рамка по тону --tr-border, padded=true. Кнопка «Сохранить» — в нижней панели карточки (border-top, flex-end). Hint «доступно председателю и членам совета» — отдельной строкой под карточкой.
- TranscriptionDetailPage: добавлен h2 «Заметка о звонке» как у секции «Текст», memo больше не висит без подписи.
Список транскрипций был зажат max-width: 720px в центре страницы и собран из самописных rows. Заменил на тот же паттерн, что использует CalendarPage / CalendarEventsTable — q-page(padding) + q-table с колонками: Звонок (название + превью memo), Начало, Длительность, Участники, Статус. Клик по строке открывает детальную страницу транскрипции.
Удалены типы 'txt' и 'image' из DocumentPreviewType + соответствующие
ветки рендера (pre.document-preview__txt и img.document-preview__image)
и стили. В реальных потоках платформы документ кооператива всегда
HTML (рендерится в ShadowHtml внутри BaseDocument) или PDF — plain-
text фрагмент в моноширинном pre не используется и выглядит как
техническая ерунда. Аналогично image.
_dev: удалён previewTxtDemo (мусорный EOSIO chain id из debugging
notes → перешитый в фрагмент протокола → теперь полностью убран
по требованию пользователя). Секция 30 показывает только HTML +
loading + error состояния.
- TranscriptionMemoEditor: q-input type=textarea заменён на WrappedEditor (Milkdown через src/shared/ui/WrappedEditor) — теперь memo отображается с markdown-рендерингом (заголовки/списки/жирный/ссылки), а не как plain текст. Кнопка «Сохранить» вынесена в отдельную панель под редактором (фокус-кольцо и поле растут естественно). Минимальная высота 180px, без скролла — растёт по содержимому.
- TranscriptionDetailPage: max-width 720 → 1040px, padding 16/20 → 16/32 — страница перестала быть сильно зауженной, гармонично для desktop.
- скилл manage-transcription-memo: добавлен раздел «Формат содержимого .memo.md» — первая строка строго одно предложение ≤150 символов без markdown (о чём был звонок), затем пустая строка и сжатая суть; этикет/повторы/разогрев убираются.
vue-tsc:
- _dev: убрал txId/explorerUrl из signatureSignedDemo — оба поля
удалены из Signature ранее (общего эксплорера нет).
- BaseDocument: canonSignatures.map — нормализую is_valid через
'?? undefined' (бэкенд может вернуть null, canon-компонент ждёт
boolean | undefined).
DocumentPreview txt demo: вместо мусорного 'EOSIO chain id …
dirty window' (мой случайный кусок из debugging notes) — фрагмент
протокола собрания пайщиков ПК «Восход».
ComplexDocument: .col-md-7 → .col-md-10. Это контейнер, в котором
лежит BaseDocument в реальных страницах документов. 7/12 = 58%
ширины было визуально 'приплюснуто'.
Курсор transcriptionLastEndedExclusiveByProject фильтровал ВСЕ артефакты транскрипции — поэтому
для уже скачанных meeting.md sibling-файлы .memo.md не появлялись. Разделил циклы:
1. meeting.md — только endedAt > lowerBoundExclusive (тяжёлый GetTranscription с сегментами).
2. .memo.md — для всех COMPLETED транскрипций каждый pull (поле memo приходит уже в лёгком
GetTranscriptions, повторных запросов не делаем).
Проверено: blago pull → 4/4 sibling .memo.md появились пустыми в проекте 33-platforma-otchetov-dlya-fnsfss; затем blago transcription memo опубликовал 476 символов в крайней транскрипции 7116fb31-3b8c-4a63-9c0c-11da26aba075, повторный pull сохранил содержимое без конфликта.
- pull-communication: для каждой COMPLETED-транскрипции вызывает syncTranscriptionMemoFile, который ВСЕГДА обеспечивает файл meetings/<stem>.memo.md (пустой, если на сервере memo пуст). Файл сразу индексируется (entity_type=call_transcription_memo) — редактирование→blago transcription memo идёт без шагов «создать файл».
- Конфликты при отсутствии prev-индекса разруливаются явно: server пуст → принять локальный draft как baseline; оба непустые и разные → git-style merge-markers; совпало → проиндексировать как есть.
- update-transcription-memo: текст ошибки про отсутствующий sibling указывает на blago pull (создаст sibling сам).
- скилл manage-transcription-memo: процедура переписана под «sibling уже есть, просто открой и редактируй».
Реальный формат подписей кооперативного документа — это не «pending/
signed/rejected» из абстрактного SignatureCard, а IDocumentAggregate с
полями doc_hash + signatures[] (signer_certificate, public_key,
signature, is_valid). Каждая подпись разворачивается в детали.
Что сделано:
- Новый canon-компонент DocumentSignatures (story 9.5) в shared/ui/
domain — props-only, принимает уже резолвнутые signerName и hash-
совпадение, эмитит download/verify. Поверх Quasar — собственный
expand на ref<Set<number>>, чтобы стиль был полностью canon.
- BaseDocument теперь рендерит DocumentSignatures вместо своего
q-card.verify-card + q-list + q-expansion-item на teal/red badges.
Адаптер canonSignatures маппит signer_certificate → ФИО через
getNameFromCertificate, чтобы canon-компонент не знал про сертификаты.
- SignatureCard: удалены поля txId/explorerUrl и ссылка «Открыть в
explorer» — общего эксплорера в платформе нет.
Demo: секция 29 → DocumentSignatures (валидный + с битой подписью),
секция 30 → DocumentPreview (HTML заголовок сокращён, чтобы не
обрывался при узкой колонке).
- новый entity_type call_transcription_memo в index-store (тип файла .memo.md, parsing/sync через стандартный syncEntityFile)
- pull-communication: для каждой COMPLETED-транскрипции с непустым tr.memo пишет sibling-файл meetings/<stem>.memo.md (hash = "<uuid>:memo"); при наличии локального draft, не индексированного в .blago/index.json, серверный memo не записывается — выводится warning
- update-transcription-memo: после успешной мутации сохраняет sibling и заносит запись в индекс (etag локального файла), чтобы следующий pull шёл штатно через syncEntityFile вместо warning'а
- скилл manage-transcription-memo: добавлены замечания про pull-синк, git-style маркеры конфликта и поведение неиндексированных черновиков
- `blago transcription memo <pathOrId> [--file <path>] [--text <inline>]` — публикация краткого содержания транскрипции через chatcoopUpdateTranscriptionMemo
- pathOrId: путь к meetings/<stem>.md (id из .blago/index.json, entity_type=call_transcription) или UUID
- default-источник memo — sibling-файл meetings/<stem>.memo.md рядом с meeting (pull-only, в push не уходит)
- скилл ai/commands/manage-transcription-memo.md описывает workflow «прочитать meeting → собрать .memo.md → согласовать → опубликовать»
- backend: убран @MaxLength(4000) с UpdateCallTranscriptionMemoInputDTO.memo и описание лимита из @Field
- backend: chatcoopUpdateTranscriptionMemo больше не доступен роли user, только chairman/member (read-методы — без изменений)
- desktop: TranscriptionMemoEditor больше не задаёт maxlength и counter в q-input
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>
@livekit/rtc-node (Rust + rustls + rustls-native-certs) reads root CAs
only from the system trust store. node:22-slim ships without
ca-certificates, so without /etc/ssl/certs/ca-certificates.crt every TLS
handshake from native bindings fails with
"invalid peer certificate: UnknownIssuer" — even for a valid LE chain.
Node.js itself is unaffected (own embedded CA bundle).
Incident 2026-05-21: secretary in dicoop/coopback:v2026.5.21-2 could not
connect to wss://chatcooprtc.coopenomics.world (controller logs full of
SecretaryAgentService UnknownIssuer; nginx on api-prod never saw the
handshake — broken before HTTP upgrade). Hot-fix on
voskhod-coopback-blue (docker cp ca-certificates.crt + restart)
restored secretary connectivity; this commit makes the fix permanent
in the base image so every consumer of dicoop/mono-base
(coopback / desktop / cooparser / notifications / boot) inherits it
on the next release tag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-mature payment row: при wallet.interactor.ts::createWithdraw платёж
сразу появлялся в gateway PG со status=PENDING — кассир видел заявку
как готовую к выплате ДО того как совет её утвердил.
Минимальный фикс без переписывания create/sync-цепочки:
- PaymentStatusEnum += AWAITING_AUTHORIZATION (с лейблом «Ожидает
решения совета»). Initial status в gateway.interactor.createWithdraw
переключён с PENDING на AWAITING_AUTHORIZATION.
- Новый WithdrawAuthorizationListener в gateway.module:
- on-chain `wallet::authwthd` (совет авторизовал)
→ AWAITING_AUTHORIZATION → PENDING (кассир увидит и сможет подтвердить).
- on-chain `wallet::declinewthd` (совет/Gateway отказал)
→ AWAITING_AUTHORIZATION → CANCELLED.
UI кассира (desktop) должен в отдельном PR фильтровать AWAITING_AUTHORIZATION
из списка платежей (текущие компоненты, скорее всего, и так не показывают
неизвестный статус). PAYMENT_STATUS_LABELS подхватится автоматически.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Завершение возврата паевого (o.wal.wthcpl) переключено с TRANSFER
(SHARE_FUND_PAY → WITHDRAWALS_SINK) на новый WalletOp::BURN_BLOCKED:
заблокированная при createwthd сумма сжигается на кошельке пайщика,
получателя на цепи нет — деньги уходят из системы банковским переводом.
Чинит assertion 'walletop TRANSFER: недостаточно L3-средств' на
completewthd: после REQUEST_WITHDRAW (BLOCK) сумма лежит в L3.blocked,
а TRANSFER проверял L3.available.
Изменения:
- ledger2: WalletOp::BURN_BLOCKED=6 + case в walletop.cpp (списание из
L2/L3 blocked). OPERATION_REGISTRY: o.wal.wthcpl с BURN_BLOCKED, без
wallet_to. burn_pattern_correct обобщён на оба BURN-варианта.
- cooptypes: WalletOp += 'BURN_BLOCKED'; o.wal.wthcpl wallet_to=null.
- wallets.hpp: WITHDRAWALS_SINK (w.wal.wthdrw) помечен DEPRECATED —
остаётся в реестре для исторических L2-балансов.
- standard p.wal.wthdrw: приведён к коду — одностадийный flow
(pending → authorized → completed), без approvewthd.
- wallet::approvewthd удалён как dead code: action не в whitelist
callback'ов, из createwthd создаётся повестка сразу с callback=authwthd.
Удалены: .cpp, declaration в wallet.hpp, include в wallet.cpp, struct
и action в abi_json/wallet.json.
- sov.authpkg.standard.yaml: упоминание approvewthd → authwthd.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
После Эпика 2 / компонента 48 `soviet::sndagreement` отказывается на
`program_id > 0`, программные соглашения переехали в контракт wallet
(`wallet::signagree`, ADR-008). Соответственно boot test helper'ы
(`signWalletAgreement`/`signCapitalAgreement`/`signGenerationContract`)
и две точки `init/*` (`participant.addUser`, `cooperative.createCooperative`),
которые ещё дёргали `soviet::sndagreement` через `signAgreement`/
`blockchain.sendAgreement` с `agreement_type ∈ {wallet, blagorost, generator}`,
падали в beforeAll тестов на ассерте контракта.
Делаем:
- Новый общий helper `tests/wallet/signProgramAgreement.ts` —
обёртка над `wallet::signagree` с auth=coopname@active. Проверяет, что
в `wallet::users[username].programs[]` появилась запись с нужным
`program_id`. Возвращает `{wallet, program, txId}` — той же формы,
что прежние helper'ы (downstream-тесты остаются без изменений).
- Три исторических helper'а становятся тонкими обёртками над ним:
`signWalletAgreement` → (program_id=1, draft_id=1),
`signCapitalAgreement` → (program_id=4, draft_id=1000),
`signGenerationContract` → (program_id=3, draft_id=0).
- `tests/capital/consts.ts` — добавлены `walletDraftId`/`sourceDraftId`/
`capitalDraftId`, соответствующие мапе `program_map` в
`lib/core/programs.hpp` (источник правды).
- `init/participant.ts` (`addUser`) и `init/cooperative.ts`
(`createCooperative`) — `blockchain.sendAgreement(...agreement_type='wallet')`
заменён на `signProgramAgreement(...program_id=1, draft_id=1)`.
`tests/soviet/signAgreement.ts` оставлен как есть — он по-прежнему нужен
для не-программных типов соглашений (`signature`/`user`/`privacy`,
program_id=0), которые остались за `soviet::sndagreement`.
Не входит:
- Замена `getUserProgramWallet` (читает legacy `soviet::progwallets`)
на чтение `wallet::users` в downstream-тестах — отдельный технический
долг, не блокирует beforeAll.
На voskhod walletop `convertsegm` падал «недостаточно L3-средств у
пайщика» в проектах, где CRPS-перераспределение увеличивало долю
contributor'а. Причина: `w.cap.gen` был USER_SHARED, а CRPS в
`approvecmmt` не делал per-user компенсирующих TRANSFER между сегментами:
инвариант `Σ COMMIT_RID == Σ ACCEPT_RID` соблюдался только на проекте,
не на сегменте, и L3-проверка walletop ломала convertsegm у пайщиков,
чья доля выросла через CRPS.
Фикс — переключение `w.cap.gen` с USER_SHARED на COOPERATIVE (ADR-002):
генератор становится единым кооперативным пулом без L3, walletop
проверяет только L2-баланс пула. `convertsegm` любого пайщика берёт из
общего котла ровно `segment.available_for_program` без проверки
персонального остатка.
Изменения:
- wallets.hpp: GENERATOR_FUND kind USER_SHARED → COOPERATIVE.
- signact2.cpp: коммент-инвариант — закрытие 08 на программе, не на
сегменте (L3-разрез по пайщику снят).
- ledger2::migrate(): переписан на cleanup осиротевших userwallets
[voskhod, w.cap.gen, *] прямым erase. Старая legacy → ledger2
миграция убрана (meta.migrated=true в проде), сигнатура сжата до
пустых скобок. migrate теперь — универсальная точка расширения
для разовых исправлений состояния ledger2, по аналогии с
capital::migrate.
migrate3 и capital::migrate сознательно не трогаются: migrate3 —
низкоуровневая per-record миграция L3 со своим назначением (не для
очистки чужой таблицы); capital::migrate остаётся пустой точкой
расширения капитала.
L2 `wallets2[w.cap.gen]` = Σ старых userwallets — синхронен на момент
перехода (проверено по фактическим данным voskhod).
Не входит в этот PR: балансовая корректировка для уже застрявших
пайщиков на voskhod — отдельной миграцией-скриптом после деплоя.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Build bootstrap container / build (push) Failing after 3m12s
* fix(controller): IS_UNIONED zod-парсер принимает string из .env
z.boolean().default(true) валится для переменной из .env, потому что
process.env всегда отдаёт строку: zod не приводит "true"/"false" к
boolean, в результате validateEnv падает с «IS_UNIONED: параметр не
установлен» и coopback не стартует, если в .env стоит IS_UNIONED=false
(стандартный dev-обход messenger-гейта, см. flow подключения партнёра).
Заменено на string().default('true').transform(v => v === 'true') —
сохранение прежнего default=true и поддержка string-форм из env.
* feat(epic-0): partner onboarding harness — signin → sign agreements → connect
Что добавлено:
- desktop/quasar.config.cjs: vite server.allowedHosts для voskhod-dev/
partner-dev/api-dev (Vite 5.4+ блокирует cross-origin Host без явного
списка — иначе SSR/HMR-сервер возвращает 403 «Blocked request»).
- docs-harness/lib/harness.mjs: helper signOnboardingAgreements —
реальная подпись каскада SignAgreementDialog (wallet/signature/
privacy/user), каждый клик отправляет sendAgreement → wallet::signagree
on-chain. В отличие от dismissOnboardingDialogs делает on-chain эффект
(см. inc 2026-05-18: stale vault SERVER_SECRET → bad decrypt).
- scenarios/onboarding/01..07: новый partner-flow Эпика 0 — partner1
заходит в Восход, подписывает 4 типовых соглашения, идёт на «Подключение»,
ant одобряет, наблюдаем установку partner-dev.
- scenarios/registration/01..04: архив старого registration-doc как
отдельная история (см. mem feedback_provider_mono_pr_flow).
- scripts/debug-*: вспомогательные скрипты для портал-структуры и
chairman onboarding'а.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(docs-harness/08): chairman install wizard на partner-dev
Сценарий 08 проходит установочный wizard /:coopname/install целиком:
шаг 1 (RequestKeyForm: WIF partner1 из state/cooperatives/partner1.json),
шаг 2 (SetInitForm: readonly orgdata из is_server_init=true, «Далее»),
шаг 3 (SetSovietForm: один председатель — Иванов И.И.),
шаг 4 (SetVariablesForm: ОПФ+ во всех падежах, устав, конф.email).
Финальный submit «Завершить установку» сейчас падает on-chain в
soviet::create — `assertion: Один из аккаунтов не найден в реестре
пайщиков`. Причина: в install.interactor.ts adduser и createBoard
шлются двумя отдельными tx; partner1 nodeos не producer, между tx есть
лаг p2p-репликации, createBoard приходит до того как participants[N]
обновился. Шот 06-error-state снимается при таймауте; шот
06-completed появится после фикса race в install.interactor (отдельный
коммит).
Сценарий запускается:
BASE_URL=https://partner-dev.coopenomics.world COOPNAME=partner1 \
node run.mjs onboarding/08-chairman-install-on-partner-dev
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(controller/install): bundle adduser+createBoard в одну tx
Cause. install.interactor.ts шлёт adduser×N и createBoard двумя отдельными
tx через BlockchainService. На production-нодах с producer-схемой это
работает (lag реплицирования минимальный), но на dev-loop'е partner-
coopback соединён со своим nodeos, который p2p-репликой подтягивает
блоки от producer'а — после accept'а adduser в local state ещё нет
soviet::participants[username] к моменту push'а createBoard. Контракт
soviet::createboard падает «Один из аккаунтов не найден в реестре
пайщиков».
Fix. Объединил все adduser-action'ы и createBoard-action в одну tx
через новый метод BlockchainPort.installSoviet(). Обе action'ы теперь
атомарны в одном блоке — soviet::addpartcpnt (inline action от
adduser) обновляет participants и createBoard видит запись сразу же
в том же блоке.
Поток в install.interactor.ts перестроен в два шага:
1. Цикл по soviet: createUser в БД + setupNotificationSubscriber +
сбор addUserActions[] и members[] (без on-chain активности).
2. installSoviet(addUserActions, createBoardData) — одна tx.
Catch при ошибке on-chain (как и раньше) откатывает users из БД.
Зачем. Закрывает блокер сценария 08-chairman-install-on-partner-dev:
без этого финальный экран wizard'а «Установка завершена» недостижим
на dev-loop'е (Эпик 0 не закрывается).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(docs-harness/09): invite-token → WIF → signin chairman
Закрывает Эпик 0: после сценария 08 (chairman install wizard) на
partner1 в Postgres появляется invite-токен председателя, но Novu не
доставит его на @example.com адрес. Сценарий 09 идёт за токеном
напрямую в БД через `ssh partner1 → docker exec postgres → psql` и
прогоняет до финального signin под новым ключом.
Шаги:
1. fetchLatestInviteToken — SELECT token FROM tokens WHERE
type='invite' AND blacklisted=false AND expires > NOW() LIMIT 1.
2. Открываем `${BASE_URL}/${COOPNAME}/auth/invite?token=<token>` —
widget Invite.vue клиентски генерирует новый WIF (generateAccount).
3. Извлекаем WIF из q-input, сохраняем в
state/cooperatives/partner1-chairman.json для последующих сценариев.
4. Чекбокс «Я сохранил ключ» → «Установить ключ» → resetKey шлёт
on-chain ChangeKey, фронт редиректит на /auth/signin.
5. Финальный signin под chairman.partner1@example.com + новый WIF;
ждём перехода на /chairman или /participant.
Запускается:
BASE_URL=https://partner-dev.coopenomics.world COOPNAME=partner1 \
node run.mjs onboarding/09-chairman-key-and-login
Требует SSH-доступа к partner1 (PARTNER_SSH=user1@91.218.246.46 по
умолчанию) и предварительно прогнанный сценарий 08 (после merge
fix(controller/install) — иначе токен не появится).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(controller/install): откатить installSoviet bundle на sleep 2s
Bundle adduser×N + createBoard в одну tx работает, но требует расширения
BlockchainPort и больше read'а; для не-producer-нод (dev-loop) достаточно
короткой паузы между adduser и createBoard, чтобы p2p-реплика подтянула
блок с participants. 2с гарантированно перекрывают и prod (~50ms), и
dev-loop (1-3с).
Отменён 3f2ecc42cf (installSoviet в blockchain.port + blockchain.service +
install.interactor), добавлен `await new Promise(setTimeout, 2000)` между
циклом adduser и createBoard.
Это однострочный фикс race-condition; bundle вернётся когда понадобится
многошаговая атомарность.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(desktop+harness): SPA mode dev + q-checkbox селектор
- desktop dev по умолчанию в SPA — SSR грузит сервер на 70%+ CPU и ломает
reverse-proxy HTTP/2 multiplex'ом параллельных модулей /src/*. Старый
режим доступен как `dev:ssr`. docker-compose зовёт `dev` — без правок.
- harness 09: Quasar q-checkbox рендерит label в div, не <label>; кликаем
по `.q-checkbox:has-text(...)`.
* feat(boot:extra): засев partner1 как coop-пайщика в воскход + announce=domain
В installExtraData (reboot:extra) теперь регистрируется partner1 как
готовый coop-пайщик воскхода со status=active, чтобы dev-стенд после
reboot:extra сразу позволял запустить полный E2E прогон Эпика 0
(provider order_instance → Hostkey rent → playbook → coopback healthy
→ provider initSystem → chairman wizard).
Flow засева:
newaccount → reguser(type=organization) → regcoop → stcoopstatus(active)
+ Mongo: organization + paymentMethod (bank_account)
+ Postgres: users.partner1{type:organization, role:user, status:joined}
В CooperativeClass — `announce` теперь хранит доменное имя
('voskhod-dev.coopenomics.world'), не текст. Provider читает announce
как domain для DomainDelegationPollingService (regex→is_valid,
health-check L7-Proxy 8880→is_delegated). См. memory
reference_provider_announce_domain.
Без этих правок:
- provider не подхватывает partner1 в свою DB после reboot:extra
- harness 08 после initSystem видит «Unsupported account type:
undefined» — partner1 organization_data отсутствуют в воскход'е
- announce='Тестовый кооператив' не парсится как domain → is_valid=false
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(boot:extra): partner1 reuse WIF из docs-harness фикстуры
В installExtraData boot:extra на каждом запуске генерировал случайный
WIF для partner1 через generateKeypair(undefined). harness 08
(chairman wizard /partner1/install) брал WIF из docs-harness/state/
cooperatives/partner1.json (засеян однократно в репозиторий) — после
reboot:extra on-chain active key уже другой, startInstall mutation на
partner-coopback'е не принимает ключ, wizard зависает на шаге 1 без
видимой ошибки. ACTIVE+initialized провайдером проходит, но
installSystem (создание совета + рассылка инвайт-токенов) не
наступает → harness 09 не может получить WIF chairman'а.
Фикс: если фикстура есть, читаем wif (опц. publicKey, иначе деривим
ecc.privateToPublic) и передаём как seededKeys в generateKeypair —
получается детерминированный partner1 active key, синхронный с
harness'ом. Иначе fallback на старое поведение (рандомный ключ).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: coopops <coopops@coopenomics.world>
После переезда контрактов в cpp/ (коммит 2b6b56a, март 2025) паттерны
в components/contracts/.gitignore ссылались на старые пути
(soviet/soviet.wasm и т.п.) и не матчились. В итоге собранные
.wasm/.abi уходили в репозиторий.
— переписаны паттерны на cpp/**/*.{wasm,abi}
— тестовые фикстуры в cpp/tests/test_contracts/** оставлены трекаться
— git rm --cached для soviet.{wasm,abi} и starter.{wasm,abi}
WalletCardMini подключает WalletCard к Pinia (useWalletStore.program_wallets) +
System store (info.symbols.root_govern_symbol) + useRouter (клик →
name: 'wallet'). Программный mapping: wallet→MAIN, blagorost→BLAGOROST,
generator→GENERATOR (Zeus.ProgramType). Loading определяется автоматически
по наличию данных в store (или пробрасывается через prop).
Подключено в слот wallet AppHeader на dev-витрине (секция 16 — каркас) +
выделенная секция 18 с тремя программами. На dev-странице покажет
skeleton-loading т.к. loadUserWallet не вызывался; на реальных страницах
после init процесса — настоящий баланс.
Со-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Перенёс из общей auto-memory: stack, worktree policy (mono-ai-1 vs mono-ai-4), PR-flow, локальные тесты, GraphQL/desktop каноны, платформенные паттерны (роли, agreements, трёхуровневый онбординг), SDK login, vault & SERVER_SECRET, Capital фиксы, Стол Заказов MVP. Часть реорганизации памяти агента по проектам; в mono-ai-2..5 файл подцепляется локальными симлинками без отдельного коммита (попадёт в feature-ветки при следующем merge от dev).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Благорост — капитализация РИД (не «накопления на жильё»), Генератор —
генерация РИД (не «доходы от программы»). Кошелёк остаётся «свободный
остаток». Черновые ярлыки риском попадают в production через копипасту.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Канонический .wallet с компактным (--row) и full-вариантами, программные
акценты через --prog-blagorost / --prog-wallet / --prog-generator (UX-DR20).
Витрина на /_dev/ui секция 17: три программы × оба варианта + loading +
empty + locked-line.
Compact (.wallet--row): 36px icon, 18px sum, 14px padding — для слота шапки.
Full (.wallet): 44px icon, 22px sum, 18px padding — для дашборда.
Connected-обёртка widgets/wallet-card-mini (Story 5.2) делается отдельным
коммитом, требует интеграции с реальным wallet-store и MARKETPLACE_ASSET_CONFIG.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Анимация max-height + opacity при свёртке давала видимый прыжок крупной
суммы баланса (`font-size: 22px` + `display: flex; align-items: baseline`):
во время промежуточных кадров baseline переcчитывался относительно
схлопывающегося контейнера → текст «прыгал в середину» и обратно.
Возвращаемся к canon `display: none`. Resize моментальный, без skipping.
Поведение rail (top опускается, bottom-chevron остаётся через flex-spacer)
работает и без анимации — пользователь это и подтвердил в фидбеке.
Chevron rotation через :deep(.q-icon) и balance-route не трогаем —
они независимы.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. **Палитра системы.** Новая секция «00» в шапке dev-страницы со всеми
основными --p-* токенами:
- Акцент (accent / hover / press)
- Поверхности (canvas / canvas-2 / surface / 2 / 3)
- Текст (ink / ink-on-accent)
- Линии (line / 1 / 2) — текстовый input для rgba
- Статусы (pos / neg / warn / info)
- Программные тинты (blagorost / wallet / generator)
Color-picker для hex-токенов, текстовый ввод для rgba. Live-override
через `document.documentElement.style.setProperty()` — inline-style на
:root перебивает CSS-правила, перекрашивание мгновенное. Подсветка
переопределённых токенов + счётчик в шапке + кнопка «Сбросить» (через
removeProperty). Watch на Quasar Dark — при смене темы пикеры
обновляются на новые resolved-значения.
2. **Chevron в свёрнутом виде — full-width.** Из-за того что для плавной
анимации primary остаётся в DOM (max-height:0 вместо display:none),
у него сохранялся `flex: 1` и он съедал ширину карточки → chevron не
мог растянуться через canon-правило `width: 100%`. В `.is-collapsed`
добавили `flex: 0 0 0` + `width: 0` для primary — chevron теперь
корректно занимает всю ширину.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
По фидбеку:
1. **Chevron не переворачивается.** Canon-правило
`.rail__usercard__collapse svg { transform: rotate(180deg) }` рассчитано
на `<svg>`, а Quasar `<q-icon>` рендерит `<i class="q-icon">`. Прокидываем
transition + rotate через scoped `:deep(.q-icon)` — chevron теперь крутится
при свёртке/развёртке.
2. **«Карточка улетает»** — резкий jump при `display:none` создаёт визуальный
прыжок. Подменяем display:none на `max-height: 0` + `opacity: 0` с
transition'ом. Логика canon-селектора `.is-collapsed` не нарушается,
просто добавлена плавность. Если структурное поведение rail (top
опускается, bottom-chevron остаётся на месте) всё равно неудобно —
перевёрстаем во вторую итерацию.
3. **Клик по балансу → маршрут кошелька.** Новый prop `balanceRoute` —
если задан, блок `.rail__balance` оборачивается в `<router-link :to>`.
Параллельно эмитим `balance-click` — для аналитики или функционального
handler'а. На витрине привязал к `/_dev/ui#wallet` чтобы проверить.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. `RouteMeta.icon` объявлен обязательным в `src/env.d.ts` — добавили
`icon: 'fa-solid fa-flask'` для dev-роута, иначе vue-tsc валится.
2. `RailUserCard` (`shared/ui/domain/RailUserCard`) — доменный
общеплатформенный компонент мини-кошелька в нижней части drawer'а.
Это будущий E11 (Волна 2), но логично вытащить раньше, чтобы основной
layout можно было заменить целиком уже сейчас.
Реализован по canon строго: rail__usercard с usertop+balance+actions,
collapse-chevron справа с поворотом arrow при свёртке (через
`.is-collapsed` модификатор + canon CSS), отдельный `rail__signout`
блок (выходит вне `.rail__usercard` — рендерится сразу после, в том
же `aside.rail` через footer-slot).
Контракт строго dumb по архитектуре: только props/emits/slots.
- props: name, role, avatarSrc, balance, symbol, balanceLabel,
lockedBalance, lockedLabel, primaryActionLabel, collapsed,
showSignout, signoutLabel
- emits: primary-action, update:collapsed (v-model), signout
- slots: usertop-extra, actions
Coнnected-обёртки (с инжектом store/auth) — на уровне страницы,
не здесь.
3. Витрина `/_dev/ui`:
- Секция 12 (AppDrawer): заменили временный огрызок rail-demo__user
на полноценный `<RailUserCard show-signout>` через footer-slot.
- Секция 16 (Каркас приложения): аналогично — теперь у rail полноценный
footer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR #403 использовал Soviet::make_complete_document — это вытащило заявление о трансляции (1080)
на верхний уровень реестра как самостоятельный документ. В разделе «Документы» 1080 оказался
отдельной строкой, под ним болтались протокол+акт из соседнего пакета (агрегатор подтягивал их
по тому же result_hash без главного документа).
Правильно — линковать 1080 к существующему пакету процесса p.cap.rid, где ведущим документом
является заявление о внесении РИД (1040, statement из pushrslt). Канон уже есть в signact1/signact2:
Action::send<newlink_interface>(_soviet, "newlink"_n, _capital, coopname, username,
Names::Capital::SIGN_ACT*_RESULT, result_hash, act);
Здесь по тому же паттерну, action = Names::Capital::CONVERT_SEGMENT (константа уже в dev из #403,
её не трогаем), package = result_hash. Off-chain controller добавит 1080 в группу пакета 1040,
а не на верхний уровень.
verify_document_or_fail оставлен — это безопасность, не связана с проблемой реестра.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Любой авторизованный пользователь раньше мог запросить capitalStories /
capitalStory и получить требования всех проектов кооператива. Закрываем эту
лазейку на бекенде и показываем аккуратную заглушку на фронте.
Backend (controller):
- GenerationService.getStories: фильтрует список разрешённых project_hash
через PermissionsService. Допуск к корневому проекту каскадно открывает
все его компоненты; допуск к одному компоненту — только этот компонент.
Запрос без project_hash/issue_hash доступен только chairman/member.
- GenerationService.getStoryByHash: проверяет допуск через project_hash
(или issue.project_hash для story с issue_hash) и возвращает null без
доступа.
- getCapitalStories / getCapitalStory резолверы теперь получают currentUser.
- ProjectPermissionsOutputDTO: добавлены has_parent_clearance и
can_view_artifacts — для отображения заглушек на фронте.
Frontend (desktop):
- ProjectRequirementsPage / ComponentRequirementsPage: показывают
ArtifactsAccessPlaceholder когда can_view_artifacts=false, с кнопкой
получения допуска (или PendingClearanceButton при поданном запросе).
- Новый shared компонент ArtifactsAccessPlaceholder в стиле остальных
заглушек расширения capital.
SDK:
- projectSelector обновлён под новые поля; schema.gql / zeus
регенерированы.
Co-authored-by: coopops <coopos@coopenomics.world>
E4 «Layout-каркас» Волны 1: 4 структурные обёртки в `src/shared/ui/layout/`.
По архитектуре — только props/emits/slots, без store/router/api, без знания
о данных. Cтраницы передают menu-items, breadcrumbs, табы и активные ключи
сверху.
Состав:
- `AppDrawer` — canon `.rail`: solid surface, активный пункт soft-accent
пилюлей + 2-px рейл слева. Поддерживает плоский список + секции
(RailSection с eyebrow-меткой), opt-in ⌘K-кнопку, slot `footer` для
user-card / logout.
- `AppHeader` — canon `.topbar`: бургер · crumb · `topbar__actions` ·
`topbar__right`. Поддерживает простой title и multi-level breadcrumb.
Глобальные действия через слоты `notifications`/`theme`/`wallet`/`profile`
или общий `right`.
- `PageHead` — canon `.page-head`: eyebrow + title + subtitle + slot
`actions`. Используется когда страница БЕЗ подстраниц.
- `PageTabs` — canon `.tabbar` + `.tab`: вкладки подстраниц с tab__count,
опциональный hairline-разделитель и `tabbar__actions` справа.
Также в составе коммита:
- Убрали AuthCard из base — он композит (cap + body + head + footer),
переедет в E6 Auth flow рядом с SignUp/ResetKey/Invite.
- BaseForm: gap полей 16→10px (тесные формы как в каноне).
- Витрина `/_dev/ui`: 4 новые секции (12-15) на каждый layout-компонент +
секция 16 «Каркас приложения» — AppDrawer + AppHeader + PageTabs в живой
связке без legacy QLayout/QDrawer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Скрипт извлечения из bundled HTML оставил ` <style>...` в начале и
`</style>` в конце обоих файлов. CSS-парсер ловил `<` как ошибочный
селектор и пропускал блок до ближайшей `}` — съедая весь первый блок
правил.
В tokens.css первым блоком был `:root, [data-theme="light"]` со светлой
палитрой — она целиком игнорировалась. `[data-theme="dark"]` ниже
парсился нормально, поэтому тёмная тема работала, а светлая — нет:
кнопки primary/ghost/danger оставались с transparent background и
сливались с canvas.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Заносим эталонную дизайн-систему в проект — только базовые элементы для
дальнейшей компоновки. Source — MONO Design System.html.
Архитектура (по `_bmad-output/planning-artifacts/architecture.md`):
- `src/css/mono-platform/{tokens.css,components.css}` — canon токены и стили
компонентов 1:1 из эталона; импортируются первыми в quasar.config css[].
- `src/shared/ui/base/<Имя>/{Vue,types,index}` — 14 базовых обёрток (Vue 3 +
TS, native HTML над canon BEM, no Quasar-deps кроме Dark API в ThemeToggle).
FSD-граница: только props/emits/slots, никаких store/router/api.
- `src/boot/ui.ts` — глобальная регистрация 14 базовых компонентов.
- `src/boot/theme.ts` — sync `html[data-theme]` ↔ Quasar Dark.
- Inter + JetBrains Mono в index.html.
14 базовых:
BaseButton, BaseInput, BaseSelect, BaseCard, BaseTable, BaseChip, BaseBadge,
BaseDialog, BaseBanner, BaseForm, EmptyState, Avatar, ThemeToggle, AuthCard.
Витрина `/_dev/ui` (dev-only) — каждая обёртка отдельной секцией над чистым
canon-канвасом, без layout-обёрток. Маршрут смонтирован на корневом уровне,
ВНЕ DynamicLayoutWrapper, чтобы legacy default.vue не перебивал стили.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
UI показывал баланс 99999.9999 RUB как 100 000,00 — пользователь видел
больше, чем реально на кошельке, и попытка инвестировать «весь баланс»
падала в walletop TRANSFER с insufficient funds (precision=4 на цепи
vs precision=2 в UI).
Принцип: «никогда не показывать сумму больше реальной». Truncate
выполняется на уровне строки, без преобразования в Number, чтобы
избежать FP-погрешности (0.29 * 100 = 28.999... в IEEE 754).
Изменения:
- desktop: новая утилита floorDecimalString — единый источник истины.
- desktop: formatAsset2Digits / formatToAsset / addAssets используют floor.
- desktop: убран double-formatting amount.toFixed(4) перед formatAsset2Digits
в ParticipantWalletsPage.
- factory: Factory.formatAsset / Factory.formatShare через приватный
truncateDecimal — тот же принцип в документах (заявления, акты).
Не затронуто (отдельный фолоу-ап): 10 файлов с inline .toFixed(4)
перед отправкой в контракт — рекомендую заменить на formatToAsset().
Co-authored-by: coopops <coopos@coopenomics.world>
* fix(controller): IS_UNIONED zod-парсер принимает string из .env
z.boolean().default(true) валится для переменной из .env, потому что
process.env всегда отдаёт строку: zod не приводит "true"/"false" к
boolean, в результате validateEnv падает с «IS_UNIONED: параметр не
установлен» и coopback не стартует, если в .env стоит IS_UNIONED=false
(стандартный dev-обход messenger-гейта, см. flow подключения партнёра).
Заменено на string().default('true').transform(v => v === 'true') —
сохранение прежнего default=true и поддержка string-форм из env.
* feat(epic-0): partner onboarding harness — signin → sign agreements → connect
Что добавлено:
- desktop/quasar.config.cjs: vite server.allowedHosts для voskhod-dev/
partner-dev/api-dev (Vite 5.4+ блокирует cross-origin Host без явного
списка — иначе SSR/HMR-сервер возвращает 403 «Blocked request»).
- docs-harness/lib/harness.mjs: helper signOnboardingAgreements —
реальная подпись каскада SignAgreementDialog (wallet/signature/
privacy/user), каждый клик отправляет sendAgreement → wallet::signagree
on-chain. В отличие от dismissOnboardingDialogs делает on-chain эффект
(см. inc 2026-05-18: stale vault SERVER_SECRET → bad decrypt).
- scenarios/onboarding/01..07: новый partner-flow Эпика 0 — partner1
заходит в Восход, подписывает 4 типовых соглашения, идёт на «Подключение»,
ant одобряет, наблюдаем установку partner-dev.
- scenarios/registration/01..04: архив старого registration-doc как
отдельная история (см. mem feedback_provider_mono_pr_flow).
- scripts/debug-*: вспомогательные скрипты для портал-структуры и
chairman onboarding'а.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(docs-harness/08): chairman install wizard на partner-dev
Сценарий 08 проходит установочный wizard /:coopname/install целиком:
шаг 1 (RequestKeyForm: WIF partner1 из state/cooperatives/partner1.json),
шаг 2 (SetInitForm: readonly orgdata из is_server_init=true, «Далее»),
шаг 3 (SetSovietForm: один председатель — Иванов И.И.),
шаг 4 (SetVariablesForm: ОПФ+ во всех падежах, устав, конф.email).
Финальный submit «Завершить установку» сейчас падает on-chain в
soviet::create — `assertion: Один из аккаунтов не найден в реестре
пайщиков`. Причина: в install.interactor.ts adduser и createBoard
шлются двумя отдельными tx; partner1 nodeos не producer, между tx есть
лаг p2p-репликации, createBoard приходит до того как participants[N]
обновился. Шот 06-error-state снимается при таймауте; шот
06-completed появится после фикса race в install.interactor (отдельный
коммит).
Сценарий запускается:
BASE_URL=https://partner-dev.coopenomics.world COOPNAME=partner1 \
node run.mjs onboarding/08-chairman-install-on-partner-dev
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(controller/install): bundle adduser+createBoard в одну tx
Cause. install.interactor.ts шлёт adduser×N и createBoard двумя отдельными
tx через BlockchainService. На production-нодах с producer-схемой это
работает (lag реплицирования минимальный), но на dev-loop'е partner-
coopback соединён со своим nodeos, который p2p-репликой подтягивает
блоки от producer'а — после accept'а adduser в local state ещё нет
soviet::participants[username] к моменту push'а createBoard. Контракт
soviet::createboard падает «Один из аккаунтов не найден в реестре
пайщиков».
Fix. Объединил все adduser-action'ы и createBoard-action в одну tx
через новый метод BlockchainPort.installSoviet(). Обе action'ы теперь
атомарны в одном блоке — soviet::addpartcpnt (inline action от
adduser) обновляет participants и createBoard видит запись сразу же
в том же блоке.
Поток в install.interactor.ts перестроен в два шага:
1. Цикл по soviet: createUser в БД + setupNotificationSubscriber +
сбор addUserActions[] и members[] (без on-chain активности).
2. installSoviet(addUserActions, createBoardData) — одна tx.
Catch при ошибке on-chain (как и раньше) откатывает users из БД.
Зачем. Закрывает блокер сценария 08-chairman-install-on-partner-dev:
без этого финальный экран wizard'а «Установка завершена» недостижим
на dev-loop'е (Эпик 0 не закрывается).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(docs-harness/09): invite-token → WIF → signin chairman
Закрывает Эпик 0: после сценария 08 (chairman install wizard) на
partner1 в Postgres появляется invite-токен председателя, но Novu не
доставит его на @example.com адрес. Сценарий 09 идёт за токеном
напрямую в БД через `ssh partner1 → docker exec postgres → psql` и
прогоняет до финального signin под новым ключом.
Шаги:
1. fetchLatestInviteToken — SELECT token FROM tokens WHERE
type='invite' AND blacklisted=false AND expires > NOW() LIMIT 1.
2. Открываем `${BASE_URL}/${COOPNAME}/auth/invite?token=<token>` —
widget Invite.vue клиентски генерирует новый WIF (generateAccount).
3. Извлекаем WIF из q-input, сохраняем в
state/cooperatives/partner1-chairman.json для последующих сценариев.
4. Чекбокс «Я сохранил ключ» → «Установить ключ» → resetKey шлёт
on-chain ChangeKey, фронт редиректит на /auth/signin.
5. Финальный signin под chairman.partner1@example.com + новый WIF;
ждём перехода на /chairman или /participant.
Запускается:
BASE_URL=https://partner-dev.coopenomics.world COOPNAME=partner1 \
node run.mjs onboarding/09-chairman-key-and-login
Требует SSH-доступа к partner1 (PARTNER_SSH=user1@91.218.246.46 по
умолчанию) и предварительно прогнанный сценарий 08 (после merge
fix(controller/install) — иначе токен не появится).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(controller/install): откатить installSoviet bundle на sleep 2s
Bundle adduser×N + createBoard в одну tx работает, но требует расширения
BlockchainPort и больше read'а; для не-producer-нод (dev-loop) достаточно
короткой паузы между adduser и createBoard, чтобы p2p-реплика подтянула
блок с participants. 2с гарантированно перекрывают и prod (~50ms), и
dev-loop (1-3с).
Отменён 3f2ecc42cf (installSoviet в blockchain.port + blockchain.service +
install.interactor), добавлен `await new Promise(setTimeout, 2000)` между
циклом adduser и createBoard.
Это однострочный фикс race-condition; bundle вернётся когда понадобится
многошаговая атомарность.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: coopops <coopops@coopenomics.world>
Build bootstrap container / build (push) Failing after 2m54s
Финальная фаза процесса p.cap.rid (convertsegm) принимала document2 convert_statement (шаблон 1080)
параметром, но:
1. **Не проверяла подпись** — `verify_document_or_fail` отсутствовала, поэтому on-chain
принял бы любую сконструированную document2 без валидной user-подписи.
У signact1/signact2 (соседние фазы того же процесса) verify есть — здесь забыли.
2. **Не регистрировала документ в реестре** — `newlink`/`newsubmitted`/`newresolved`
не вызывался, заявление о трансляции 1080 «терялось»: off-chain controller (process_instance)
не видел финальный документ привязанным к result_hash, процесс p.cap.rid не помечался completed.
У pushrslt (create_approval) и signact2 (newlink с SIGN_ACT2_RESULT) линковка есть.
Канон есть в soviet/src/system/converttoaxn.cpp:54 и soviet/src/agreement/sndagreement.cpp:104:
паттерн `Soviet::make_complete_document(calling_contract, coopname, username, action, package_hash, document)`
шлёт newsubmitted + newresolved одной парой, package = анкер процесса (здесь result_hash).
Изменено:
- names.hpp: новая константа Names::Capital::CONVERT_SEGMENT = "convertsegm"_n (12 символов)
- convertsegm.cpp:
- verify_document_or_fail(convert_statement, {username}) сразу после require_auth
- Soviet::make_complete_document(...) ДО delete_result (чтобы линковка прошла, пока result_hash ещё анкер)
Контракт capital собирается без ошибок (cdt-cpp testnet mode), warnings — старые ricardian.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
PR #394 «починил» TS-ошибки coopback заменой типа `Cooperative.Registry.GenerationConvertStatement`
на `GenerationMoneyInvestStatement` в 3 файлах controller'а. Это семантически неверно:
- 1080 GenerationConvertStatement — заявление о трансляции паевого взноса
(поля: project_hash, main_wallet_amount, blagorost_wallet_amount, to_wallet, to_blagorost, appendix_hash)
- 1020 GenerationMoneyInvestStatement — заявление о денежном паевом взносе по программе Генерация
(совсем другой набор полей)
DTO BaseGenerationConvertStatementMetaDocumentInputDTO декларирует поля 1080, но `implements ExcludeCommonProps<action>`
где action = тип 1020 → TS2352 на as-cast в interactor, потому что Action'ы не пересекаются по полям.
Реальная причина исходных ошибок coopback после #392 — несвежий dist `@coopenomics/cooptypes`
на dev-узле (старое имя символа). Лечится пересборкой пакета, не переименованием ссылок.
Изменено (откат #394):
- generation-convert-statement-document.dto.ts:12 — `action = ...GenerationConvertStatement.Action`
- distribution-management.service.ts:56 — `registry_id: ...GenerationConvertStatement.registry_id`
- distribution-management.interactor.ts:48,66 — `Promise<...GenerationConvertStatement.Action>`
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
z.boolean().default(true) валится для переменной из .env, потому что
process.env всегда отдаёт строку: zod не приводит "true"/"false" к
boolean, в результате validateEnv падает с «IS_UNIONED: параметр не
установлен» и coopback не стартует, если в .env стоит IS_UNIONED=false
(стандартный dev-обход messenger-гейта, см. flow подключения партнёра).
Заменено на string().default('true').transform(v => v === 'true') —
сохранение прежнего default=true и поддержка string-форм из env.
Co-authored-by: coopops <coopos@coopenomics.world>
PR #392 переименовал Cooperative.Registry.GenerationConvertStatement в
GenerationMoneyInvestStatement в @coopenomics/document, но в controller
осталось 3 несинхронизированные ссылки:
- distribution-management.service.ts:56 — registry_id метода generation
- distribution-management.interactor.ts:48,66 — тип Action в return и as-cast
- generation-convert-statement-document.dto.ts:12 — type action в DTO
Coopback падал на ts-node compile (TS2551/2724), что блокировало старт
всего dev-stack после reboot dev-chain 2026-05-18.
Co-authored-by: coopops <coopos@coopenomics.world>
* refactor(capital/convert): унифицировать 1080 как универсальное заявление о конвертации, убрать 1081/1082
Шаблон 1080 (GenerationConvertStatement) уже технически универсален — содержит
обе суммы (main_wallet_amount/blagorost_wallet_amount) и условные блоки в
context. Шаблоны 1081 (GenerationToProjectConvertStatement) и 1082
(GenerationToCapitalizationConvertStatement) были недоделанными заглушками
без полей и нигде не подключены в UI.
Изменения:
- cooptypes: 1080 переименован GenerationToMainWalletConvertStatement →
GenerationConvertStatement; title/description нейтральные. 1081/1082 удалены.
- factory: Template + Action 1080 переименованы; в Action добавлено
super.formatAsset(...) для обеих сумм (как в Action 1020). 1081/1082 удалены.
- controller: DTO переименован; appendix_hash убран из generate-input и
перенесён в signed-meta-input; добавлен enrich appendix_hash через
AppendixRepository.findConfirmedByUsernameAndProjectHash в
DistributionManagementInteractor.prepareGenerationConvertStatementData
(по образцу InvestsManagementInteractor для 1020). Резолвер мутации
переименован в capitalGenerateGenerationConvertStatement; убраны два
резолвера 1081/1082. mutation-log-mapper обновлён.
- sdk: мутация переименована, две удалены, zeus regenerated.
- desktop: Distribution-фичи 1081/1082 удалены, 1080-фича переименована.
ConvertSegment теперь шлёт project_hash + обе суммы (formatToEosioAsset) +
to_wallet/to_blagorost; appendix_hash подтягивается на бекенде.
- controller schema.gql regenerated.
registry_id=1080 не меняется — on-chain контракт convertsegm не сверяет
registry_id, миграций БД/контракта не требуется.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(capital/convert): применить ревью — title «трансляция паевого взноса из программы Генерация»
По комментарию ревью в PR #392 (строка 37): принять доменный термин
«трансляция паевого взноса» (перенос между программами) вместо
«конвертация»; description согласован в том же стиле.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(docs-harness): onboarding 01..06 — визуальная цепочка регистрация → активация
Шесть сценариев visual docs-harness, покрывающих полный путь подключения
нового кооператива через провайдера Восход:
01-register-coop — регистрация кооператива-клиента
02-sign-and-submit — подпись заявления о вступлении + PayInitial
03-operator-approve — chairman принимает заявку в реестре одобрений
04-sign-connection-agreement — Партнёр-1 видит ConnectionAgreementStepper
(на текущем стенде получаем заглушку
/signup, пока пайщик не принят)
05-activate-from-registry — оператор открывает карточку инстанса
в provider-frontend, выбирает preset
06-wait-instance-active — pending → ACTIVE (overrideInstance
для имитации финального статуса в шоте)
Обвязка:
• lib/registrator-signup.mjs — переиспользуемый helper подписания.
• lib/harness.mjs — расширенный dismissOnboardingDialogs (Положение ЦПП
и связанные модалки chairman'а).
• state/cooperatives/{,.gitkeep} — папка для фикстур; partner1.json
(с приватным wif) игнорируется (.gitignore обновлён).
ОГРАНИЧЕНИЕ: в 06 финальный ACTIVE — это playwright-override JSON, а не
реальный POST /instances/activate с боевым Ansible до testnet300.coopenomics.world.
Реальный E2E (аренда VM на Hostkey + поднятие кооператива на домене)
будет следующим шагом Эпика 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(controller): is_server_init flag в initSystem для разблокировки provider-overwrite
Поле data.is_server_init: boolean (optional) в InitDTO + домейн-интерфейсе.
Если true (вызов от провайдера через server-secret) — coopback ставит
init_by_server=true безусловно, даже если до этого пользователь успел
заполнить визард первым (user-init). Это разблокирует ситуацию, когда
провайдер не успел вызвать initSystem до того как chairman открыл
/install — следующий callInitSystemMutation от провайдера перезапишет
organization_data и пометит её readonly для визарда.
Без флага сохраняется прежняя логика: первая инициализация — серверная,
повторная наследует флаг.
Инцидент 2026-05-18 на partner1: при первой установке provider вообще
не успел/не сходил в callInitSystemMutation, визард пользователя
проинициализировал систему как user-init (init_by_server=false), визард
2-го захода не предзаполнил форму. С этим фиксом следующий вызов
provider'а поднимет флаг и фикстура встанет на место.
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(release): publish
* chore(release): publish
* ci: атомарный release.yaml, убрать workflow_run-связку (#366)
build-contracts + build-containers через workflow_run упёрлись в:
(а) default-branch caveat (новая логика не активна, пока не в main),
(б) `${{ github.event.workflow_run.head_sha }}` иногда пуст в YAML-
выражениях — описание см. в шаге Resolve tag, инцидент v2026.5.14
не дёрнул PRODUCTION_WEBHOOK_URL.
Замена — один `release.yaml` на push тэга `v*`: резолвит ветку через
`git branch --contains`, собирает контракты → пушит
`dicoop/contracts:<branch>`, собирает базу + сервисные образы →
пушит `dicoop/<svc>:<tag>`, шлёт webhook. Гонок нет by construction.
`build-contracts.yaml` остаётся только на push веток для CI-
обновления `dicoop/contracts:dev|testnet|main` без релизного тэга.
Триггер `tags: ['*']` и логика резолва ветки через --contains
оттуда удалены — это теперь забота release.yaml.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* chore(release): publish
* chore(release): publish
* ci: tag-only триггеры для build-contracts и docs (#367)
build-contracts.yaml — только workflow_dispatch (ручная пересборка
`dicoop/contracts:<branch>` для отладки на dev-ноде). Тэги обрабатывает
release.yaml атомарно (контракты + контейнеры + webhook), отдельная
сборка по push'у в ветку только давала вторую параллельную сборку.
publish-docs.yaml и build-contracts-docs.yaml — на push:tags v* с
gate-job'ом, пропускающим только продакшн-тэги (без -alpha/-beta/-rc/
-test) на main. Раньше docs пересобирались на каждый push в
main/testnet/dev/reports/marketplace2 — впустую.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(capital/time-tracking): личные доли estimate, partial-split, revert при decline
Три бага в распределении билетов времени, вскрытые на прод-инциденте voskhod
(проект CC7-1 «Концепция», estimate=15 ч, 3 creators):
БАГ #1 — recalcDoneEstimatesForContributorProject раздавал «общий остаток пула»
(estimate − committed_total) / N всем creators поровну. Закоммитивший свою долю
получал её ещё раз, остальные — урезанную (15/3=5 → после committed 5 у одного
становилось 10/3=3.33 у каждого, включая того кто уже закоммитил).
Фикс: личная доля = estimate/N − собственный committed estimate. Введён общий
helper redistributeIssueEstimateEntries, который используют и applyExplicit-
EstimateToTimeEntries (force=true), и recalcDoneEstimates (force=false с
no-op оптимизацией если раскладка уже совпадает с планом).
БАГ #2 — commitTime при partial split (entry.hours > requested) создавал новую
committed-запись без entry_type и estimate_snapshot. По default'у БД сохраняла
её как entry_type='hourly', что ломало последующий recalc (он фильтрует только
entry_type='estimate'). Фикс: явно копировать entry_type и estimate_snapshot
из оригинального entry.
БАГ #3 — declineCommit / handleDeclineCommit меняли только commit.status в БД,
но не возвращали time-entries в is_committed=false. После отказа мастера часы
оставались в total_committed_hours и не возвращались в available_hours. Фикс:
новый метод revertEntriesForDeclinedCommit в TimeTrackingInteractor + методы
findCommittedByCommitHash / revertCommittedEntriesByCommitHash в TimeEntry-
Repository. После revert делается force=true redistribute для затронутых DONE
задач, чтобы доли вернулись в норму.
Покрытие тестами: 17 unit-тестов в time-tracking.interactor.spec.ts с
регрессионными сценариями под каждый из трёх багов плюс integration-сценарий
полного lifecycle CC7-1 (estimate → коммит → decline → revert).
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Failing after 11s
Publish Docs / build-and-publish-docs (push) Failing after 18m20s
Раньше MINIO_ENDPOINT имел default http://minio:9000, и на проде без
minio-контейнера контроллер падал на bootstrap в HeadBucket с
getaddrinfo ENOTFOUND minio — Nest application не поднимался вообще.
Теперь MINIO_ENDPOINT optional без default; адаптер хранит enabled-флаг
по наличию endpoint и при отсутствии — onApplicationBootstrap логирует
warning и возвращается без сетевых вызовов. Любая попытка getBucket /
fetchObjectForReadProxy кидает InterFileStorageBackendUnavailableError
с понятным сообщением.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Если syncCommit не дождался delta из блокчейна, interactor возвращает DB-only
entity, где description/meta остаются undefined — non-nullable GraphQL field
ломал ответ мутации capitalCreateCommit. Сделал оба поля nullable.
UI CreateCommitButton.vue: satisfaction_stars=0 по умолчанию, label "не указано"
пока пользователь не выбрал; блок contribution_feedback в payload только если
stars >= 1 или review_text непустой.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Failing after 7s
Publish Docs / build-and-publish-docs (push) Failing after 17m11s
* [E59-2][@ant] feat(inter): InterFileStorage порт — типы, токен INTER_FILE_STORAGE и типизированные ошибки для универсального файлового хранилища контура кооператива
* [E59-3][@ant] feat(controller): MinIO-адаптер InterFileStoragePort, реестр бакетов, @UseBucket/@InjectBucket декораторы и FileStorageInfrastructureModule с forRoot/forFeature; 36 unit-тестов на адаптер, реестр, декоратор и HMAC-подписание
* [E59-4][@ant] feat(controller): HTTP-ручка GET /api/storage/:bucket/:key с HMAC-валидацией подписи и стримом из MinIO; fetchObjectForReadProxy на адаптере, controller registered в FileStorageInfrastructureModule.forRoot; 9 e2e-тестов через supertest на 200/403/404/502
* [E59-5][@ant] test(controller): integration suite против реального MinIO — 8 сценариев на полный цикл put/head/getReadUrl/GET/delete + ошибки лимитов/MIME/metadata + HMAC-роут 200/403/404; docker-compose рядом с тестами, npm run test:integration:file-storage с автодетектом доступности MinIO
* [E59-6][@ant] feat(controller,compose): MinIO в dev docker-compose, env-валидация и FileStorageInfrastructureModule.forRoot в app.module — контроллер на старте идемпотентно создаёт бакет coop-<coopname>; integration-тесты проходят против MinIO из dev compose
* [E59-6][@ant] docs(file-storage): краткий README для разработчиков расширений — пример @UseBucket/@InjectBucket/forFeature, операции, ошибки, env, как запускать тесты
---------
Co-authored-by: coopops <coopos@coopenomics.world>
Build bootstrap container / build (push) Failing after 2m56s
build-contracts.yaml — только workflow_dispatch (ручная пересборка
`dicoop/contracts:<branch>` для отладки на dev-ноде). Тэги обрабатывает
release.yaml атомарно (контракты + контейнеры + webhook), отдельная
сборка по push'у в ветку только давала вторую параллельную сборку.
publish-docs.yaml и build-contracts-docs.yaml — на push:tags v* с
gate-job'ом, пропускающим только продакшн-тэги (без -alpha/-beta/-rc/
-test) на main. Раньше docs пересобирались на каждый push в
main/testnet/dev/reports/marketplace2 — впустую.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
build-contracts + build-containers через workflow_run упёрлись в:
(а) default-branch caveat (новая логика не активна, пока не в main),
(б) `${{ github.event.workflow_run.head_sha }}` иногда пуст в YAML-
выражениях — описание см. в шаге Resolve tag, инцидент v2026.5.14
не дёрнул PRODUCTION_WEBHOOK_URL.
Замена — один `release.yaml` на push тэга `v*`: резолвит ветку через
`git branch --contains`, собирает контракты → пушит
`dicoop/contracts:<branch>`, собирает базу + сервисные образы →
пушит `dicoop/<svc>:<tag>`, шлёт webhook. Гонок нет by construction.
`build-contracts.yaml` остаётся только на push веток для CI-
обновления `dicoop/contracts:dev|testnet|main` без релизного тэга.
Триггер `tags: ['*']` и логика резолва ветки через --contains
оттуда удалены — это теперь забота release.yaml.
Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
После переключения build-containers с `push: tags` на `workflow_run`
обнаружился разрыв в миграционный период: на default-ветке (main) лежит
старый build-containers (с `push: tags`), на dev/testnet — уже новый
(с `workflow_run`). Push релизного тэга на коммит из dev/testnet НЕ
триггерит:
- старый build-containers в main: GitHub читает workflow definition
ИЗ КОММИТА тэга (где уже новая версия без `push: tags`);
- новый build-containers через workflow_run: триггер берётся из
default-ветки, где ещё старая версия без `workflow_run`.
В итоге для тэгов v2026.5.13-alpha-2/-3 build-containers не запустился
вовсе.
Фикс: workflow_dispatch с input.tag — позволяет руками запустить
сборку+деплой для любого выпущенного тэга. Логика resolve_tag
поддерживает оба источника. Это снимает блокер до мержа в main.
Использование:
gh workflow run "Build Docker Images" -f tag=v2026.5.13-alpha-3
После коммита 5735e1fc36 workflow стал триггериться на push тэгов,
но `Determine build mode and docker tag` использовал `github.ref_name`
напрямую, который для тэга = `v2026.5.13-alpha-2` → не матчит ни одну
из ветвей в `case` → workflow падает «Unsupported branch».
Фикс: для триггера по тэгу резолвим ветку через `git branch -r --contains
$SHA` (порядок: main → testnet → dev). Полная история нужна для
`--contains`, поэтому checkout с `fetch-depth: 0`. Семантику
build-режима несёт ветка, не имя тэга.
Раньше при релизе (`chore(release): publish` + git tag) запускались
параллельно `build-contracts.yaml` (push веток) и `build-containers.yaml`
(push тэгов). Поскольку контейнеры собираются ~8 минут, а контракты ~11,
build-containers финишил первым и слал webhook на тестнет за 2-3 минуты
до того, как build-contracts успевал запушить новый `dicoop/contracts:dev`
в DockerHub. Ансибл `setup-contracts.yaml` подтягивал ПРЕДЫДУЩИЙ образ
и через `cleos set contract` перетирал чейн старым wasm.
Инцидент 2026-05-13: walletop-фикс ledger2 (коммит 2e3410b830),
вручную задеплоенный 12-05, был откачен ансиблом ровно по этой
причине — ансибл подтянул контракты сборки 12-05 04:54 (sha de0f6c79,
до моего фикса).
Изменения:
- `build-contracts.yaml` дополнительно триггерится на push любого тэга
(без path-фильтра): нужен unconditional запуск, чтобы у workflow_run
всегда был upstream-завершение даже когда коммит ничего не правит
в `components/contracts/cpp/**`.
- `build-containers.yaml` переключён с `push: tags` на `workflow_run:
Build contracts container completed`. Внутри: резолв тэга через
`git describe --tags --exact-match $head_sha` — если на коммите тэга
нет (обычный push в ветку без релиза), no-op. Если есть — собирает
контейнеры и шлёт webhook ровно как раньше, но с гарантией что
`dicoop/contracts:<branch>` уже свежий.
Важно: workflow_run-триггер берёт definition из default-ветки (main),
поэтому новый build-containers.yaml начнёт работать только после мержа
этого коммита в main. До тех пор сохраняется старое поведение dev-ветки
(workflow_run от build-contracts на dev запустит build-containers из
main; если там старая версия — всё ещё через push: tags).
Build bootstrap container / build (push) Failing after 2m52s
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Failing after 11s
Build contracts container / build (push) Failing after 11m30s
Publish Docs / build-and-publish-docs (push) Failing after 19m16s
В коммите 8847a5c093 production-код registerCapitalInAgreementRegistry
изменил applicable_account_types у blagorost_offer на [] — оферта
тянется через программу CAPITALIZATION, а не как дефолт для individual.
Тест capital-plugin-register.test.ts остался на ассерте
[AccountType.individual] и с тех пор красный.
Выравниваю ассерт под актуальное поведение, удаляю ставший лишним
импорт AccountType.
В `soviet::coagreements[voskhod]` оферта Благорост (program_id=4)
зарегистрирована под `type='capital'` — и на тестнете, и на проде.
Расширение capital после Эпика 1.3 отвечает за on-chain тип оферты,
но в `capital-agreement-ids.ts` значение `'blagorost'` унаследовано
из старого ядерного `AgreementType.CAPITAL`, который, в свою очередь,
был неверно изменён в commit 283af35f3b («looking for access
violation bug»).
Эффект на тестнете: при регистрации любого individual-аккаунта
контроллер шлёт `soviet::sndagreement` с agreement_type='blagorost',
`get_coagreement_or_fail` падает с «Соглашение указанного типа не
найдено», регистрация не завершается.
Возвращаю значение к on-chain имени; обновляю тест и три комментария,
которые декларировали старое значение как канон.
migrate_voskhod_facts:
- accounts2[51]: 176 800 → 145 000 (минус 31 800 минП, теперь только деньги)
- accounts2[08]: 543 400 → 575 200 (плюс 31 800 минП — инвестировано в активы)
- accounts2[04/80/86] без изменений; Σ Dr = Σ Cr = 63 073 511 ✓
Новый COOPERATIVE-кошелёк w.sov.mnused для аналитики "использованные
минимальные паевые взносы" (Cr 80 source, перешедшие в 08). Для voskhod
31 800 размещается там вместо w.reg.minshr — обязательство перед пайщиками
на счёте 80 сохранено, но wallet-аналитика отражает что средства уже
ушли в долгосрочные активы.
migrator-048 (Phase 1 L3 → w.reg.minshr): skip voskhod явным if в TS +
жёсткий guard в migrate3 (eosio::check отвергает запись L3(voskhod,
w.reg.minshr) в prod). В testnet-сборке guard выключен — voskhod проходит
стандартный арифметический путь для видимости invariant-фейлов.
Реестр LEDGER2_WALLET_REGISTRY 13 → 14; namespace w.sov.* расширён до
"совет-level фондов" (целевое финансирование + использованные паевые).
TS-зеркало (wallets.generated.ts) регенерировано через pnpm gen:from-cpp;
snapshot-тест cooptypes обновлён.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Промежуточные статусы пайщика (created/joined/payed/registered) больше
не показываются как «уже зарегистрирован». Пайщик с WIF в localStorage,
но без принятия советом, видит публичную главную и кнопки login/register
— как незарегистрированный. Это позволяет ему серфить сайт между шагами
регистрации, не получая преждевременно подпись оферты цифрового кошелька.
Изменения:
- SessionStore.isFullyActive — новый computed, true при user_account.status === 'active'.
- navigation-guard-setup: ветка index выбирает дашборд только для isFullyActive;
requiresAuth-маршруты при isAuth && !isFullyActive (вне /auth/*) шлёт на index.
- init-wallet: не дёргать wallet.loadUserWallet пока !isFullyActive (account.getAccount
оставляем — нужен для определения статуса).
- init-app: selectDefaultWorkspace только при isFullyActive (иначе non_authorized).
- Desktop store: ветки в selectDefaultWorkspace / getDefaultPageRoute идут от
isFullyActive, не isAuth.
Источник истины статуса — миграция V2.2.0 + ParticipantStatusSyncService:
все accepted-пайщики on-chain переводятся в users.status='active' в моно.
Замер на восходе (api.coopenomics.world soviet::participants[scope=voskhod]):
34 accepted + 1 blocked = 35, все «принятые» → ровно active.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
При выборе программы GENERATION generateRegistrationDocuments падал с
«Данные соглашения благороста не найдены в Udata»: blagorost_offer с
applicable_account_types: [individual] тянулась как дефолтная оферта,
но generateDocumentParameters под GENERATION зовёт только
generateGeneratorOfferParameters → Factory не находит udata для blagorost
→ Promise.all rejected → фронт получает 0 документов → пайщик ничего
не подписывает → backend бракует "Отсутствуют blagorost_offer, generator_offer".
Фикс: applicable_account_types: [] на blagorost_offer. Оферта подтягивается
исключительно через agreement_ids программы CAPITALIZATION (как и generator_offer
через GENERATION).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Селектор `extensionOnboardingStateSelector` использовал `as any` на nested-объекте, из-за чего `InputType<...>` терял форму `steps` (резолвился в `unknown`). Убрал `as any` + `as const`, добавил `MakeAllFieldsRequired` валидацию — паттерн как в `commitSelector`.
Composable `useExtensionCooperativeOnboarding` объявлял интерфейс через `ReturnType<typeof computed<T>>`, что резолвится в `WritableComputedRef` (последний overload Vue). Заменил на явные `ComputedRef<T>` / `Ref<T>` — фактическая форма не writable.
Со-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SDK:
- Регенерирован zeus-клиент под новые типы ExtensionOnboardingState и
CompleteExtensionOnboardingStepInput из платформенного резолвера.
- Добавлен namespace `Queries.Onboarding.GetExtensionOnboardingState`,
`Mutations.Onboarding.CompleteExtensionOnboardingStep` и селектор
`extensionOnboardingStateSelector`.
Desktop features/CooperativeOnboarding (FSD):
- `useExtensionCooperativeOnboarding(getExtensionName)` — реактивный
controller со state/steps/allDone/expiresAt + load/completeStep.
- `<CooperativeOnboardingGate extension="...">` — slot-based wrapper:
пока !all_done показывает `slot[onboarding]` (по дефолту —
CooperativeOnboardingSteps), после ратификации всех шагов —
основной слот.
- `<CooperativeOnboardingSteps>` — список шагов с кнопкой "Создать
предложение совету"; событие `propose` поднимается parent'у для
открытия формы — UX-форма остаётся за конкретным расширением.
Использование Стол заказов (и любым новым расширением): обернуть
рабочий экран в `<CooperativeOnboardingGate extension="stol_zakazov">`,
шаги показываются автоматически из платформенного реестра.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Системный паттерн «онбординг кооператива на расширение»:
- OnboardingStepsRegistry — in-memory реестр шагов, регистрация
декларативно в initialize() расширения.
- ExtensionOnboardingService — generic getState/completeStep, работает
с любым extension, шаги задаются спецификацией IExtensionOnboardingStepSpec
с двумя generator'ами: free_decision (создаёт project + опубликовывает +
регистрирует tracking-rule SOVIET_DECISION) и meet (registerTrackingRule
MEET_DECISION с externally-provided proposal_hash).
- ExtensionOnboardingEventsService — generic слушатель DecisionTrackedEvent
для расширений без legacy events-сервиса (chairman/capital — пропускает).
- GraphQL endpoint getExtensionOnboardingState / completeExtensionOnboardingStep
с ролевой защитой (query open для chairman/member/user, mutation chairman).
A2: chairman и capital декларативно регистрируют свои существующие шаги
через ONBOARDING_STEP_REGISTRATION_PORT — step_key совпадает с
config-полями onboarding_<step_key>_done/hash, поэтому legacy resolver'ы
и платформенный сходятся на единой истине в config'е.
Capital: step_key='blagorost_provision' выбран под существующее поле
onboarding_blagorost_provision_done (legacy enum использует 'blagorost_program').
Юнит-тесты OnboardingStepsRegistry: регистрация/дубликаты/order/unregister.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Платформенный composable + 2 widget'а для генерации и подписи пачки
документов пайщика. Локальный state (без Pinia) — можно инстанциировать
независимо в любом месте: и в Registrator, и на странице расширения,
онбордящего пайщика по своему flow.
— useDocumentSigning(getOpts) — load() / setAccepted() / signAll() / linkHashes
— <DocumentsChecklist :documents @update:accepted> — чек-лист с ReadAgreementDialog
— <DocumentsSignCanvas @signed> — canvas-подпись, эмитит сигнатуру
Backend генерации (generateRegistrationDocuments mutation +
AgreementRegistry) уже был generic — этот эпик закрывает фронтенд-сторону.
Registrator не переключаем — у него legacy-привязки к
useRegistrationStore + полям store.walletAgreement/etc. в state. B2
переключения Registrator на новые widget'ы — отдельной story, когда
потребуется почистить legacy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SDK (Zeus regen из обновлённой schema.gql после Эпика 2.1):
- registrationAgreementSelector — все 10 полей RegistrationAgreement
- Queries.Agreements.GetRegistrationAgreements — query с
filter coopname/account_type/program_key
- Zeus index.ts / const.ts регенерированы (controller + sdk)
Desktop (FSD feature в components/desktop/src/features/OfferGate):
- useOfferGate composable — реактивно проверяет on-chain agreements
пайщика через Queries.Agreements.Agreements; signed ↔ существует
запись (coopname, username, type) со status !== DECLINED
- <OfferGate> Vue компонент c props {coopname, username,
agreementType, offerTitle, signupUrl?} — рендерит slot если оферта
подписана, иначе q-banner с кнопкой «Перейти к подписанию»
- model/types.ts — типизация props
Симметричен бэкендовому AgreementSignaturePort (Эпик 3.1): один
вердикт без расхождения между UI и сервером.
pnpm typecheck desktop — exit 0. Контракт-тесты controller/sdk —
тоже зелёные (53). План C28-10 раздел 2.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Новый модуль domain/onboarding/constants/onboarding-ttl.ts —
единый источник правды для ONBOARDING_EXPIRY_DAYS (30),
ONBOARDING_EXPIRY_MS и helper computeOnboardingExpiresAt(startedAt)
- chairman-extension.module и capital onboarding.service используют
helper вместо дублированного хардкода `30 * 24 * 60 * 60 * 1000`
- Юнит-тест (4 кейса) фиксирует константы и эквивалентность helper'а
старому inline-вычислению
Runtime-смок: coopback hot-reload зелёный.
53 unit-тестов зелёные. tsc --noEmit exit 0. План C28-10 раздел 4.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Новый платформенный порт AgreementSignaturePort + AGREEMENT_SIGNATURE_PORT
в domain/agreement/ports — расширения проверяют подпись соответствующей
оферты перед допуском к своим операциям
- AgreementService.hasSigned реализация порта через AGREEMENT_REPOSITORY
(Postgres backfill on-chain agreements3); подписано ↔ найдена запись
(coopname, username, type) в любом статусе кроме DECLINED
- @Global() на AgreementModule + useExisting биндинг AgreementService
на порт; расширения могут инжектить порт без явного импорта модуля
- L3-гейт в InvestsManagementService:
• createProjectInvest → проверка подписи 'generator' иначе
ForbiddenException с понятным сообщением для UI;
• createProgramInvest → проверка подписи 'blagorost' аналогично;
Используются константы из extensions/capital/constants/capital-agreement-ids.ts
- Юнит-тесты L3-гейта (5 кейсов): отсутствие подписи блокирует обе
программы, наличие подписи передаёт управление интерактору, подпись
другого пайщика не открывает доступ (cross-user isolation)
Runtime-смок: coopback hot-reload зелёный (13:53:58), DI порта
зарегистрировано без UnknownDependencies.
49 unit-тестов зелёные. tsc --noEmit exit 0. План C28-10 раздел 3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Новый платформенный порт чтения соглашений и программ
(AgreementQueryPort, AGREEMENT_QUERY_PORT) в
domain/registration/ports — резолверы и расширения инжектят его
вместо AgreementConfigurationService напрямую, чтобы граница чтения
оставалась стабильной при будущей перестройке реализации
- useExisting AgreementConfigurationService → AGREEMENT_QUERY_PORT
биндинг в registration-domain.module
- RegistrationAgreementDTO для GraphQL — спецификация оферты
(не подписанное Agreement), сливает платформенные + extension-
зарегистрированные в едином формате
- Новый GraphQL Query getRegistrationAgreements(coopname, account_type,
program_key?): [RegistrationAgreement!]! — в RegistrationResolver
- RegistrationService.getRegistrationAgreements делегирует порту
- Контрактный тест AGREEMENT_QUERY_PORT (5 кейсов): structural
implementation check, 4 платформенные оферты для individual,
пустые программы при пустом реестре, getAgreementById для
существующей и несуществующей оферты
- Auto-regenerated components/controller/schema.gql
Runtime-смок: GraphQL introspect RegistrationAgreement даёт 10 полей,
живой query getRegistrationAgreements(voskhod, individual) возвращает
4 платформенные оферты с корректным order/applicable_account_types.
44 unit-тестов зелёные. tsc --noEmit exit 0. План C28-10 раздел 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Эпик 1.1 — платформенная инфраструктура реестра:
- AgreementRegistrationSpec / ProgramRegistrationSpec DTO
- AgreementRegistrationPort интерфейс + AGREEMENT_REGISTRATION_PORT токен
- AgreementRegistryService с in-memory state, идемпотентностью по
(id, extension_name), ConflictException на конфликт владельца,
tear-down через подписку на EXTENSION_APP_TERMINATE_EVENT
- ONBOARDING_COMPLETED_EVENT + подписка ExtensionLifecycleDomainService
на восстановление расширения после завершения L1-онбординга
Эпик 1.2 — Capital и Chairman регистрируются через port:
- CapitalPlugin.initialize() → registerCapitalInAgreementRegistry
при завершённом L1 (5 _done флагов) регистрирует 2 оферты
(generator/blagorost) и 2 программы (generation/capitalization)
- CapitalOnboardingEventsService.handleDecisionTracked после
blockchain newresolved эмитит ONBOARDING_COMPLETED_EVENT при
переходе последнего L1 _done false→true (idempotency через
wasAlreadyDone guard)
- ChairmanOnboardingEventsService — аналогичный эмит при
завершении 7 L1 шагов председателя
Эпик 1.3 — чистка ядра controller от capital-специфики:
- AgreementId/AgreementType enum'ы сокращены до 4 платформенных
(signature/wallet/user/privacy); BLAGOROST_OFFER/GENERATOR_OFFER
и CAPITAL/GENERATOR удалены
- registration-programs.config.ts удалён (voskhod-hardcode уехал
в registry capital); registration-agreements.config.ts сокращён
- CooperativeConfigService.getExcludedFromBaseAgreements удалён
- IAgreementConfigItem/IRegistrationProgram типы id/agreement_type/
key/agreement_ids ослаблены до string — ядро не знает значений
расширений
- AgreementConfigurationService переписан: inject AgreementRegistryService,
слияние платформенных оферт с extension-зарегистрированными;
программы читаются только из registry
- system.service.getRegistrationConfig: requires_selection
вычисляется как programs.length > 1
- participant.interactor.mapAgreementIdToDocumentType: case'ы
capital удалены, identity-fallback по Object.values(DocumentType)
- Capital: новый файл constants/capital-agreement-ids.ts —
локальный source-of-truth для строковых значений оферт/типов/
программ; используется в register-capital-in-agreement-registry
Тесты: 39 unit-тестов в 5 suites (agreement-registry, capital
plugin-register, capital onboarding-events, chairman onboarding-events,
existing access-policy-union) — все зелёные.
Остаётся как техдолг (не входит в 1.x): DocumentType.BLAGOROST_OFFER/
GENERATOR_OFFER (физические колонки таблицы candidates), ProgramKey
enum в ядре (используется в blockchain payload и switch case
registration-documents.service).
План C28-10, ветка onboarding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Полная сверка Σ L3 == L2 через линейный проход secondary-индекса bywallet
выполнялась на каждом walletop для обеих сторон USER_SHARED-кошельков
(стоимость O(N_users) на кошелёк × 2 стороны). На coop'ах с сотнями
пайщиков это даёт квадратичный рост CPU billing на пользовательских
транзакциях, а на тестнете дополнительно блокирует tx из-за исторических
расхождений после миграций 048/049.
Инвариант сохраняется по построению: walletop применяет одно и то же
amount к L2 и L3, а sender-guard (walletop.cpp:46-47) запрещает обход.
Полную сверку выносим на бэкенд («стол бухгалтера») вне hot path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- capital::regcontrib больше не делает dual-write openprogwall для
program_id=3/4 — остаётся только inline wallet::signagree (документы
попадают в источник правды wallet::users.programs[]).
- wallet::migrate3 принимает coopname@active помимо wallet@active —
чтобы контроллер кооператива мог сам бэкфиллить из своей БД.
- V2.3.1 миграция: достаёт реальные подписанные документы Благороста и
Генератора из blockchain_actions локального controller-PG (audit trail
capital::regcontrib actions), пушит wallet::migrate3 с реальным
doc_hash и signed_at для каждого orphan'а в program_id=3/4.
- migrationManager: прокинул VaultDomainService в Migration interface
(для blockchain.initialize(coopname, wif) перед transact).
Контракты задеплоены на testnet:
capital 22a882d25395b29692e85bdd10860469341f7783d51538ea96c9685748cd31ab
wallet 53a12723d77a2a7786044908c44bb6f0117cfc4225009b1f4d532ab5f59fa92a
На дев/тест-кооперативах controller стартовал со снапшота, старые подписи
(status='') до момента запуска sync не пришли через delta-stream — фронт
требует подписать user/signature/privacy заново, хотя они подписаны на цепи.
Миграция читает agreements3 целиком по scope=COOPNAME через RPC и UPSERT'ит
каждую запись по id. Программные (program_id > 0) пропускает — они идут на
чтение через wallet::users.programs[]. domain-status не трогаем.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
soviet::progwallets имеет blocked и membership_contribution как
binary_extension. На старых записях (например voskhod/enzqwnqsdqar/1)
eosjs возвращает undefined — pushAsset падает с
"Expected string containing asset". Защищаемся ?? zeroAssetLike(pw.available).
Для membership ?? уже был.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
У нас нет приватных ключей от кооперативов, которых мы мигрируем.
Поэтому ledger2::migrate3 и wallet::migrate3 теперь принимают auth от
get_self() (т.е. ledger2@active / wallet@active), и они же платят за RAM.
Mig-скрипты 047/048/049 пушат actions с актором = имя контракта.
Также правки в ledger2::migrate (Эпик 1) с прошлой сессии: default-case
для неизвестных legacy account IDs (862-867) — `break;` вместо
eosio_assert (ignore non-canonical), плюс IS_TESTNET-блок с clamp'ом
грязных legacy-данных.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Группа 862..867 (RESERVE/INDIVISIBLE/ECONOMIC/MUTUAL/DEVELOPMENT/DELEGATE_FEES)
в ledger2 отдельными кошельками не выделена. Падать на ненулевом 867
(найдено на testnet) — блокирует миграцию; вместо этого пропускаем.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Фильтр `status === 'confirmed'` пропускал всё: после Эпика 2 для program_id > 0
sndagreement пишет ""_n, а confirmagree больше не вызывается (воркфлоу ушёл
в wallet::signagree). Заменено на `status !== 'declined'`; wallet::migrate3
идемпотентен.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Этот файл — общая память агента для пяти чекаутов: `~/mono-ai-1`..`~/mono-ai-5`. Реальный файл лежит в `mono-ai-1/CLAUDE.md`, остальные четыре — симлинки сюда; правки коммитим из mono-ai-1 в ветку `dev`.
Когда план говорит «UI компонент» / «frontend integration» — путь `components/desktop/src/{features|widgets|pages|processes}/<name>/`, расширение `.vue` (composition API + `<script setup lang="ts">`), стили Quasar (QChip/QBtn/QCard/QDialog…), GraphQL через Apollo Client + сгенерированные типы из `components/sdk/`. Dev — `pnpm --filter @coopenomics/desktop run dev` или `devnet` (без SSR).
**НЕ путать:** НЕТ `components/app-cooperative/` — не предлагать. Все frontend-сессии работают в той же монорепе, что и backend. В стеке **никакого React нигде нет**.
## Worktree-политика
**Для mono-ai-4 (базовая ветка `marketplace2`, Стол заказов):****worktree приветствуется** — изоляция работ + параллельные ветки. Если worktree пуст от `node_modules` и `.env` (pnpm их не дублирует):
Аналогично для других пакетов, чьи тесты будут запускаться (`components/desktop/node_modules`, `components/sdk/node_modules`). `jest` из bin: `cd $WT/components/controller && ./node_modules/.bin/jest -i <test>` — корректно резолвит ts-jest и подхватывает `.env`.
**Подвох cooptypes:** когда `controller/node_modules` — симлинк на main, пакет внутри `node_modules/cooptypes -> ../../cooptypes` раскрывается **относительно main checkout**. `import { MarketContract } from 'cooptypes'` тянет d.ts из main, не из worktree. Если worktree обновлён, а main позади — TSC падает на отсутствующих типах. **Фикс:** перед TSC в worktree controller'а — `git pull` в main checkout до того же коммита (или хотя бы где cooptypes/src синхронен) + `pnpm build` в `main/components/cooptypes/`.
## PR-flow
**Базовая ветка mono-ai-1 — `dev`.** Прямые коммиты в `dev` для мелких фиксов разрешены и не требуют feature-ветки/PR (отменено 2026-05-22 пользователем). Для крупных задач — feature-ветка (`feat/...` или `fix/...`) от dev и PR в dev; merge делает пользователь на GitHub. Push в `main` / `mvp` — по-прежнему только через PR.
**Не stash'ить `-u` при unstaged WIP пользователя.** Это создаёт окно для потери при drop/конфликте. Если нужно временно убрать unstaged — `git stash push -- <конкретные-paths>` либо коммит-в-feature, и только свои файлы. Кейс 2026-05-18: после `git stash -u`+`drop` при конфликте потерял WIP пользователя (infra.ts/config.ts/quasar.config.cjs и др.).
### Story-by-story для проекта «Стол заказов» (mono-ai-4 на marketplace2)
Каждая story из MVP-эпиков «Стол заказов» (`coopenomics/mono`, базовая ветка `marketplace2`, чекаут `~/mono-ai-4`, BMad-spec'и `_blago/.../components/3-minimalnyy-produkt/_bmad-output/`) идёт через PR.
Workflow:
1. Worktree от `marketplace2` на feature-ветке `feat/<E>-<S>-<slug>`.
2. Edits + unit-тесты; tsc + jest должны быть зелёные.
5. Merge **пользователем на GitHub** (не вызывать `gh pr merge` без явной просьбы).
6. После merge — fetch + checkout marketplace2 в основной чекаут, удалить feature-ветку + worktree, запустить e2e / blockchain тесты против обновлённого marketplace2.
### Umbrella-PR на эпик vs цепочка PR
При работе story-by-story в одной feature-area **не плодить отдельные PR на каждую story с одинаковой целевой веткой**. Каждый последующий PR показывает кумулятивный diff (всё, что в head минус то, что уже мёрджнуто в base). Если предыдущие PR ещё не смерджены — в diff вылазят дубликаты файлов всех предыдущих stories.
**Default — umbrella-PR на эпик** (для крупных эпиков с >3 stories): одна `feat/<E>-epic` ветка + один PR; каждая story — отдельный коммит. Worktree последовательно, push после каждой story, PR обновляется. Так закрыт Эпик 1 Стола заказов (umbrella #380).
**Альтернатива — stacked-PRs с `base:<prev-feat>`:** каждый PR таргетится на предыдущую feature-ветку. После merge нижнего GitHub автоматически перетарджетит верхние. Требует дисциплины и тулинга.
**Анти-паттерн:** worktree от `feat/1-2-...`, потом от `feat/1-3-...`, и каждый PR в `marketplace2`. Цепочка branches правильная (изоляция), но цепочка PR — нет. Кейс Эпика 1 Стола заказов 2026-05-14: 11 PR `#370-#380` подряд от `marketplace2`, каждый +N stories назад. Пользователь дошёл до review #372 и обнаружил дубли. Закрыл #372-#379, оставил только #380.
## Локальные тесты
**Не запускать полный jest локально** ни в mono-ai-1, ни в mono-ai-4: живой dev-стек в docker (`nodeos`, `controller dev` nodemon, `parser dev`, `n8n`) вешает CPU/RAM и блокирует chain. Полный suite — задача CI после push'а PR.
В mono-ai-4 **запрещён параллельный режим jest** (worker-pool по умолчанию) — это вешает сервер. Если нужен unit-тест — точечный с `--runInBand`:
```bash
pnpm jest tests/unit/marketplace/marketplace-onboarding-service.test.ts --runInBand
```
`pnpm generate-schema` / `pnpm generate-client` — **не запускать локально**; та же memory/CPU полка вешает контейнер controller'а. Либо CI, либо пользователь сам когда контейнер остановлен.
Перед коммитом достаточно `tsc --noEmit` (быстрый, не блокирует).
## SDK login canon
`@coopenomics/sdk` экспортирует `Client.create({api_url, chain_url, chain_id})` + метод `client.login(email, wif)`. Он сам:
1. Генерит `now` (ISO timestamp).
2. Подписывает приватным ключом (WIF) через eosjs.
**Не дёргать `Mutations.Auth.Login` напрямую** — `LoginInput` ждёт `{email, now, signature}`, генерация подписи внутри SDK Client. Refresh: `Mutations.Auth.Refresh.mutation`с`{access_token, refresh_token}`. Канон используется в `blago-cli/src/session/index.ts` (loginInteractive) и в EMP-коннекторе `connectors/cooperative-tsk-login-connector` (Story 11.5).
## Backend (controller) каноны
### 3 базовые роли — User / Member / Chairman
- **User** — обычный пайщик. Базовые потребительские права (заказывать, публиковать оферту, видеть свои данные).
- **Member** — **член совета** (не «член кооператива»!). User-права + read-only admin (видит склад, поток заказов, повестку — но не модерирует и не подписывает финальные действия).
- **Chairman** — председатель. User + admin (модерация, KU/whitelist/витрины, closing signature АПП-приёмки/выдачи, повестка совета на write).
Маппинг core-роли на extension-роль явно: User → orderer + опционально offerer/operator; Chairman → admin (полный write); Member → read-only admin (board_readonly). Пайщик может быть одновременно в нескольких extension-ролях — массив, не enum. Guard'ы в расширениях — локальные сейчас, в Phase 2 переключатся на платформенный CASL.
### `agreements` ссылается на существующий document registry_id
Когда расширение controller'а (marketplace, blagorost, любое следующее) хранит факт подписи документа пайщиком в глобальной on-chain таблице `agreements`, ссылка идёт через **существующий `registry_id` из платформенного реестра документов**, не через отдельный type-string типа `marketplace.cpp.stol-zakazov-v1`.
Поле `document_id` в `agreements` — FK на существующий platform registry. Extension-таблицы `*_onboarding_requirement` — `document_registry_id` ссылается на существующий ID. API запросов вида «какие документы подписаны member'ом» — `agreementsByMember(member_id, document_id_filter=[...])`.
**Технический долг платформы:** «Договор УХД сейчас не проходит через `agreements`» — отдельная задача core controller'а, вне scope конкретных расширений.
### Трёхуровневый онбординг расширений
Платформенный паттерн, обязателен для всех новых расширений controller'а.
**L1 — Кооператив (one-time):** председатель/совет принимает решение совета о подключении ЦПП, принимается положение ЦПП (статический документ из platform registry, через document factory с подстановкой параметров кооператива), оферта регистрируется в `coop_registration_offers_registry`.
**L2 — Пайщик при вступлении (per-membership):** в registration-flow появляется **выбор** ЦПП. Пайщик отмечает интересующие, document factory рендерит оферту с `{cooperative_params, member_params, agreement_date}`. Подпись пишется в глобальную on-chain `agreements`. Эта подпись **нивелирует** gate на столе расширения.
**L3 — Пайщик на рабочем столе (per-extension first visit):** backend проверяет через `agreementsByMember` — подписана ли оферта ЦПП. ДА → gate не показывается. НЕТ → gate показывается как explicit consent.
При проектировании любого нового расширения — обязательно три истории под три уровня. На MVP допустимо упростить: одинаковый `document_registry_id` для положения и оферты (физически разные документы могут быть в Phase 2).
### Marketplace asset через DI
В сервисах `components/controller/src/extensions/marketplace/**/*.service.ts` запрещён хардкод вида `const ASSET_DECIMALS = 4` / `const ASSET_SYMBOL = 'RUB'`. Decimals и symbol — через DI `MARKETPLACE_ASSET_CONFIG`:
Канон-пример: `marketplace-order-create.service.ts`. См. `marketplace-asset.config.provider.ts` — мапит `config.blockchain.root_govern_symbol` / `root_govern_precision`. Разные среды (mainnet/testnet/dev) имеют разный symbol+precision.
### registry_id=800 (ReturnByAssetStatement) — клиринг, не членские взносы
Marketplace в монорепе живёт в **двух контурах**:
1.**Система клиринга** (старый, не используется) — registry_id=800. Не использовать в новых фичах.
2.**Система членских взносов** (текущий MVP) — документы лежат **рядом** с актами приёма-передачи и ТТН; другая группа registry_id.
Для нового документа Marketplace-членские-взносы: завести новый registry_id рядом с актами/ТТН и проложить цепочку `cooptypes → factory → controller → desktop`:
1.`@coopenomics/cooptypes`: новый тип документа + регистрация в registry.
3.`@coopenomics/controller`: signed-document DTO + verify в сервисе.
4.`@coopenomics/desktop`: подпись через `Classes.Document`с новым registry_id.
## GraphQL каноны (controller + desktop)
### Описания @Field — бизнес-языком
В`@Field({ description })`, `@InputType`, `@ObjectType` нельзя писать «Story 4.1», «Эпик 3», «FR11a», «composite-entity», «dispatch pipeline», «tx_snapshot», «backend deterministic order_hash». Только пользовательский язык: «Идентификатор заказа», «ПВЗ получения», «Кол-во единиц товара». Story-ссылки и инвариант-комменты — только в inline-комментариях внутри сервиса.
### Enum вместо строковых литералов
Запрещены `if (offer.cycle_type === 'volume_based')`, `status: 'ACTIVE'`, `'PENDING_MODERATION'`. Любое поле с фиксированным набором (cycle_type, status, type, kind, role) — TypeScript `enum`, при необходимости `registerEnumType` для GraphQL. В тестах константы — тоже из enum, не дублирующие строки. Перед добавлением сравнения по строке — `enum MarketplaceOfferCycleType` / `MarketplaceOrderStatus` в `domain/entities/*.types.ts`.
### Пагинация — единый паттерн
В controller-resolver'ах пагинация делается единым каноническим паттерном:
- Вход: `@Args('options', { nullable: true }) options?: PaginationInputDTO` (импорт из `~/application/common/dto/pagination.dto.ts`, поля page/limit/sortBy/sortOrder).
Обязательная процедура перед UI-кодом под новую GraphQL-операцию:
1. Добавить/изменить DTO/resolver в `components/controller/src/...` (code-first).
2.`pnpm run generate-schema` в `components/controller/` → пересоздать `controller/schema.gql`.
3.`pnpm run generate-client` в `components/controller/` → graphql-zeus кладёт клиент в `components/sdk/src/zeus/`.
4.`pnpm run build` в `components/sdk/` → unbuild собирает `dist/`.
5. В desktop импортировать `Mutations.<Domain>.<Name>` / `Queries.<Domain>.<Name>` из `@coopenomics/sdk` — типизировано end-to-end.
Если на шаге 1 не хватает поля — добавить и запустить весь цикл; не оставлять заглушку «пока».
### Строгая типизация desktop API из SDK через IInput['data']
Каждый вызов `client.Mutation` / `client.Query` в `components/desktop/src/**/api/index.ts`:
1. Принимает аргументом объект `data: IXxxInput`, где `IXxxInput = Mutations.<Domain>.<Action>.IInput['data']` (или `Queries...`) — тип берётся **прямо из @coopenomics/sdk**, не переописывается.
2. Передаёт в `variables` объект `data` целиком: `{ variables: { data } }`. Запрещено разворачивать поля: `{ variables: { data: { a, b, c } } }`.
Канон — `features/Branch/CreateBranch/{api,model}/index.ts`: тип в model `export type IXxxInput = Mutations.X.Y.IInput['data']`; функция в api `function (data: IXxxInput) { ... variables: { data } }`. Не делать `as` cast'ов.
## ⚠️ ДИЗАЙН-КАНОН desktop — ОБЯЗАТЕЛЕН К ИСПОЛЬЗОВАНИЮ
**ПРИ ЛЮБОЙ ВЁРСТКЕ В `components/desktop/` НЕ ВЫДУМЫВАТЬ СТИЛИ.** Всегда сначала свериться с каноном. Не строй гипотез о цветах/радиусах/типографике/паттернах из памяти — иди и читай.
**Полная спецификация канона — в skill `/mono-desktop-canon`** (правила обёрток, цвета/токены, иконки, структура страницы, stop-signals). Здесь — короткая выжимка; при расхождении побеждает skill.
**Источник истины — в самом репозитории** (НЕ внешние HTML/прототипы):
| **Живой эталон** (`/_dev/ui` в dev-сборке) | `components/desktop/src/pages/_dev/ui/index.vue` |
**При сомнении — открыть `tokens.css` и `_dev/ui/index.vue`, смотреть как сделано там.** Внешний `shared/MONO Design System.html` и `auth-prototype/` каноном НЕ являются — это устаревшие прототипы.
**Запреты:**
- Экран собирается из готовых компонентов: `shared/ui/base` (вместо сырых `q-input`/`q-btn`/`q-card`/`q-table`/`q-chip`/`q-dialog`/`q-select`), `shared/ui/domain` (WalletCard, DataRow, DocumentRow, IdentityPanel…), `shared/ui/layout` (PageHead, PageTabs, AppHeader/AppDrawer). Голый Quasar — только где обёртки нет (`q-icon`, `q-toggle`, `q-list`, `q-menu`, `q-tooltip`, `q-tabs`, `q-separator`, `q-spinner`, `q-inner-loading`…). Props обёрток не угадывать — читать `*.types.ts` рядом.
- Цвет — только токены `var(--p-*)` (поверхности `--p-surface*`, текст `--p-ink*`, линии `--p-line*`, акцент `--p-primary`, статусы `--p-pos/neg/warn/info`) либо utility-классы/color-props. Никаких сырых hex/rgb. Темы light/dark переключаются через `[data-theme]` на `<html>` — токены следуют сами.
- Spacing/радиусы/типографика — токены `--p-1..--p-10` (4px…72px), `--p-r-sm/md/lg/xl`, `--p-fs-*`/`--p-lh-*` либо классы `.t-*`. Без хардкод-px.
- Иконки — `q-icon(name='…')` именами Material Icons. **FontAwesome (`fa-*`) запрещён** — заменять на Material-эквивалент попутно.
- Никаких локальных переопределений `.q-btn`/`.q-card`/`.q-notification` в feature-файлах — Quasar-overrides централизованы в `quasar-canon.css`.
**Кейс 2026-05-28:** при сомнении в каноне пошёл искать его во внешнем HTML/auth-prototype вместо репозитория — перевёрстал не туда, переделывал. SoT — репо (`tokens.css` + `_dev/ui`) и skill `/mono-desktop-canon`, не внешние файлы.
## Frontend desktop — English имена
В`components/desktop/` и любом Vue/TS frontend коде **все имена идентификаторов — английские**:
- Имена Vue-компонентов: `BaseInput`, `BaseDialog`, `WalletCard`, `IdentityPanel`.
- Имена файлов и директорий: `shared/ui/BaseInput/BaseInput.vue`.
**Why:** Unicode-имена ломают тулчейн (Vite/Webpack резолверы и aliases часто на ASCII-only regex; TypeScript symbol-resolution на не-ASCII нестабильно; ESLint `vue/component-name-in-template-casing` ждёт PascalCase ASCII; импорты `import БазоваяКнопка from '@/shared/ui/БазоваяКнопка'` невыносимы при review).
**Заголовки в .vue, label-ы кнопок, тексты в UI — по-русски** (это user-facing strings). Не путать с правилом «онтологические class_id по-русски» — то про EMP/ТЭМ и blago-документы, не про frontend.
**Кейс 2026-05-18:** при подготовке UX-спецификации для components/desktop ошибочно применил правило русских имён к Vue-компонентам (`БазоваяКнопка`, `БазовоеПолеВвода`); пользователь поправил.
## Стандарты процессов (.standard.yaml) — бизнес-языком
`components/contracts/cpp/**/*.standard.yaml` — документация для методолога/бухгалтера, не для разработчиков контракта. В `purpose`, `description`, `note`, `human` запрещены технические термины:
- никаких «callback», «soviet::exec», «soviet::createagenda», «AUTHORIZE_CALLBACK_SIGNATURE», «type-string», «registry N», «proposed расширение enum'а»;
- никаких «backend formирует», «controller вызывает», «contract отдаёт» — пишем кто что делает на уровне бизнеса (председатель / совет / заказчик / поставщик);
- технические `marketplace::propwroff`-имена в полях `action`/`name`/`triggered_by` оставляем как identifier'ы, но всё человеко-читаемое в `human`/`purpose`/`description` — на бизнес-словаре;
- если процесс встроен в более общий — ссылаемся на стандарт по имени-человеку («типовой процесс решения совета»), не на техническую реализацию повестки.
Эталоны: `p.mkt.return.standard.yaml`, `p.cap.rid.standard.yaml`, `reg.coop.standard.yaml`. Антипример — `p.mkt.wroff.standard.yaml` в PR #399 review 2026-05-18 (был забит callback-описаниями и «type=mktwroff»).
## Vault & SERVER_SECRET
WIF админ-аккаунта (например `voskhod`) хранится в `vaults` PostgreSQL зашифрованным AES-256-CBC с ключом `sha256(SERVER_SECRET)`. Если `SERVER_SECRET` потом меняли — **старые записи разрушаются**, `decipher.final()` бросает `error:1C800064:Provider routines::bad decrypt`.
**Симптомы:**
-В логе coopback: `[VaultDomainService] Ошибка при получении WIF ключа для пользователя voskhod: bad decrypt`.
-В UI каскад `SignAgreementDialog` не закрывается; SPA отправляет `sendAgreement` корректно, но `wallet::signagree` on-chain не происходит.
- Следствие — `wallet::users[<coop>]` пуст, и любой последующий `is_can_transfer`/трансфер AXON падает на `Отправитель не является участником ЦПП кошелька`.
**Фикс:**
1. Достать живой WIF (для voskhod в dev — `5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3` из boot config.ini, signature-provider).
2. Шифрануть им current SERVER_SECRET (см. `controller/src/utils/aes.ts` — sha256(SECRET) → AES-256-CBC, IV 16 байт, формат `ivHex:cipherHex`).
3.`UPDATE vaults SET wif='...', updated_at=now() WHERE username='voskhod' AND permission='active';`.
4. Перезапуск coopback **не нужен** — он читает каждый раз.
**Дефолт всех mono-репозиториев — `SECRET`.** Если в каком-то `.env` стоит другое — девиация, не норма. Перед re-encrypt'ом vault'а ВСЕГДА сначала смотреть `~/mono-ai-1/components/controller/.env` (или соседнего) — это origin truth для SERVER_SECRET.
**Кейс 2026-05-18 (Эпик 0):** в `mono-ai-5/components/controller/.env` стоял `e2e-fixture-secret-DO-NOT-USE-IN-PROD`, а WIF voskhod в vaults был зашифрован оригиналом — `SECRET`. Фикс — откатить SERVER_SECRET к `SECRET` и подровнять provider'а.
## Capital — фиксы билетов времени (PR #387 merged 2026-05-15)
В`components/controller/src/extensions/capital/` была серия багов в распределении билетов времени, закрытая PR #387 в dev. Три бага:
1.**`recalcDoneEstimatesForContributorProject` / `applyExplicitEstimateToTimeEntries`** раздавал «общий остаток пула» `(estimate − total_committed) / N` всем creators, включая того, кто уже закоммитил. Фикс: личная доля `max(0, estimate/N − own_committed_estimate)` через общий helper `redistributeIssueEstimateEntries`.
2.**`commitTime` partial split** создавал committed-запись без `entry_type` и `estimate_snapshot` — БД по default'у писала `entry_type='hourly'`, что ломало последующий recalc (фильтрует только `entry_type='estimate'`). Фикс: явно копировать оба поля.
3.**`declineCommit` / `handleDeclineCommit`** меняли только `commit.status='declined'`, но не возвращали `time-entries` в `is_committed=false`. Часы оставались в `total_committed_hours`. Фикс: `revertEntriesForDeclinedCommit` через новые `findCommittedByCommitHash` / `revertCommittedEntriesByCommitHash`.
**При симптомах** «25 ч подтверждено непонятно откуда» / «доступные часы не вернулись после decline» / «парные коммиты на одну работу» — первый чек: `grep -c redistributeIssueEstimateEntries time-tracking.interactor.ts` в контейнере > 5. Если нет — деплой устарел. Если есть, но баг — посмотреть `entry_type`у committed-записей по issue: legacy записи из БАГ #2 могут до сих пор быть с `entry_type='hourly'` для split-результатов от estimate (видны по `commit_hash IS NOT NULL AND entry_type='hourly' AND estimate_snapshot IS NULL` на DONE-задаче с estimate>0). Чинятся UPDATE'ом `entry_type → estimate`с правильным `estimate_snapshot`.
**Legacy data caveat:**`capital_time_entries`с`entry_type='hourly'` + `commit_hash != NULL` могут быть как (а) настоящей hourly работой до установки estimate, так и (б) split-наследием БАГ #2. Различить: если у задачи `estimate>0` и `DONE`, и записи hourly от тех же creators что в estimate-долях — это (б), чинить.
**Edge-case дробных остатков:** при `estimate=2.5` и одном creator, после commit'а 2 ч (`Math.floor < 1` для остатка) остаётся 0.5 ч uncommitted estimate, который никогда не закоммитится. Либо ручной DELETE uncommitted остатка, либо изменить estimate на целое (но estimate в `capital_issues` — on-chain, через UI mutation, не DELETE'ом в БД).
## Стол Заказов MVP — текущее состояние
Проект `1-prilozhenie-stol-zakazov` в blago (coopname voskhod, hash `feabc749…3841f73`). Кооперативная закупка/распределение имущества участка (продукты, товары, услуги); пилот — Красногорск; цель 6 мес: 10 кооперативов / 1200+ пайщиков.
- **`components/3-minimalnyy-produkt/_bmad-output/planning-artifacts/epics.md`** (SoT для MVP) — `stepsCompleted=[1,2,3,4]`, `status: complete 2026-05-12`, 65 FRs → 11 эпиков → 57 stories. **НЕ создавать дубль на верхнем уровне `_bmad-output/planning-artifacts/`** — MVP-компонент имеет собственный bmad-output.
-`requirements/` (5 файлов): `04-brif`, `0b-protsessy`, `0f-prd`, `7e-uxui`, `d6-arkhitektura` — дубли _bmad-output (намеренно, см. правило о дублях BMad-артефактов в global memory).
- **Эпик 1** — MERGED в `marketplace2`: PR #368/#370/#371/umbrella#380.
- **Эпик 2 «Сеть ПВЗ»** — MERGED PR #381 (после rebase). Workspace `market-pvz`, KU details + Yandex geocoder, Zeus SDK.
- **Эпик 11 Story 11.1 (Ledger2 canonical actions)** — MERGED PR #375 на C++ стороне. **TS-сторона cooptypes НЕ закрыта**: `cooptypes/src/contracts/marketplace/actions/index.ts` экспортирует только LEGACY клиринговые actions, `interfaces/marketplace.ts` auto-generated из устаревшего ABI. **Блокер Эпика 4** — нужен отдельный pre-эпик PR `feat/S11-1-cooptypes-canonical`: ABI regen через `eosio-abi2ts` из новой `marketplace.abi.json` после `coopcontracts` build, либо ручное добавление 18 canonical actions + canonical tables + canonical interfaces.
| desktop/pages/Marketplace | нет canonical | 11 страниц под клиринг; `desktop/extensions/market` пуст |
**Решение 2026-05-12:** стратегии миграции не делаем — donor уже не работает по старой модели; собираем новую membership-модель в существующем контуре, donor-код переписывается / удаляется напрямую без переходников.
В`/bmad-create-architecture` MVP **не вводить adapter-слой и не описывать миграционные пути**. Прямо фиксировать: какие C++ actions/DTO/Vue-страницы из donor-листа удаляются, какие переписываются под canonical (`signsupp/signchair/signiss1/signiss2/acceptbatch/declinebatch/expirecycle/prepship/createorder(новая сигнатура)/cancelorder`), какие сохраняются (shipment/coopstock — если попадают в MVP scope, отдельно проверить).
**Обязательная enforcement-база для backend** — `mono-ai-4/components/controller/CLAUDE.md` (Composite-Entity `db/bc/derived`, Write-mutation pool с placeholder/sync_key dedup, ParserClient + Redis Streams, ForkRegistry, ADR-002/008/009/011/012; параметры через `config/blockchain.config.ts`, не magic numbers).
Полная пошаговая инструкция, как поднять backend (`coopback`) + parser (`cooparser`) + блокчейн-ноду + БД на macOS через Docker Desktop. Прошёл — отметь, ниже разобраны типичные грабли.
## TL;DR
```bash
cd ~/dacom-code/foundation/monocoop
docker compose up -d # 1. поднять базу
./scripts/dev-setup-macos.sh # 2. одной командой (см. ниже скрипт)
```
Если скрипта ещё нет — выполняй шаги ниже руками.
## Шаги
### 1. Поднять контейнеры
```bash
docker compose up -d
```
Поднимутся: `node` (NodeOS), `mongo`, `monoredis`, `postgres`, `coopback`, `cooparser`. **MinIO** входит в дефолт. **OpenSearch** — нет (тяжёлый, см. опц. сервисы).
### 2. Проверить чтоб .env'ы указывали на service-имена, а не localhost
**Почему service-имена:** все контейнеры в bridge-сети `monocoop_default`. Внутри контейнера `127.0.0.1` = сам контейнер, не host. На macOS `network_mode: host` в Docker Desktop работает плохо — используем bridge + service names.
И вписать в `components/controller/.env` → `CHAIN_ID=...`. **Если chain_id в .env не совпадает с живым — on-chain транзакции упадут на verify, хотя приложение запустится.**
### 4. Если правил .env — пересоздать контейнер (не restart!)
```bash
docker compose up -d --force-recreate --no-deps coopback cooparser
```
`docker restart`**не перечитывает**`env_file`. `--no-deps` — чтобы не пересоздавать БД (потеряются данные).
### 5. Native binding libxmljs2 — пересборка под Linux
На свежем чекауте `pnpm install` запускается на macOS, и `libxmljs2` собирает Mach-O бинарник. В Linux-контейнере он падает:
docker compose up -d --force-recreate --no-deps coopback
```
## Грабли (если опять провозился пол-дня)
| Симптом | Причина | Фикс |
|---|---|---|
| `invalid ELF header` | host'овый `pnpm install` положил Mach-O | См. п. 5 — пересборка libxmljs2 в контейнере |
| `MongooseServerSelectionError: ECONNREFUSED 127.0.0.1:27017` | .env на localhost вместо service-имени | См. п. 2 |
| `password authentication failed for user "postgres"` | в .env `POSTGRES_PASSWORD=postgres` вместо `postgres!23!23` | См. п. 2 |
| Coopback ушёл в restart-loop, но рядом есть другой `monocoop-coopback-1` Up | Случайно запустился старый `components/controller/docker-compose.yml` (network_mode: host, без bind-mount, без CMD) | `docker stop coopback && docker rm coopback` |
| `docker restart` не подхватил новые переменные .env | `restart` не перечитывает `env_file` | `docker compose up -d --force-recreate --no-deps <svc>` |
| Coopback зависает на 5+ минут, лога нет | Это нормально для cold-start ts-node | Подождать. CPU должен крутиться ≥50%. Если CPU = 0% — другая проблема |
| `[VaultDomainService] Ошибка при получении WIF... bad decrypt` | SERVER_SECRET не совпадает с тем, чем шифровали WIF в БД | См. CLAUDE.md → раздел Vault & SERVER_SECRET. Дефолт — `SECRET` |
| Транзакции падают на on-chain verify | CHAIN_ID в .env не совпадает с живым | См. п. 3 |
| `Custom endpoint \`minio://9000\` was not a valid URI` на bootstrap | `MINIO_ENDPOINT` без схемы | `MINIO_ENDPOINT=http://minio:9000` (со схемой!) |
| nodeos в restart-loop с `Database dirty flag set` после sleep/crash Docker Desktop | unclean shutdown chain state | `docker stop monocoop-node-1`; `mv blockchain-data/state-history{,.broken-$(date +%s)}`; `docker compose up -d node` — nodeos сам сделает replay |
| Cold-start coopback 10+ минут (вместо обычных 3-5) | ts-node без cache + bind-mount через osxfs + параллельная нагрузка (nodeos replay) | Подождать; в будущем — `tsx` / `ts-node --swc` для dev-режима |
Назначение скилла — собрать краткое содержание (memo) встречи по транскрипции звонка, согласовать его с пользователем и опубликовать в backend через `blago` так, чтобы оно появилось в desktop UI на странице транскрипции звонка (поле «Заметка о звонке»).
Право редактировать memo на сервере есть только у председателя и членов совета — убедись, что активная сессия `blago login` принадлежит подходящей роли.
Перед началом прочитай: `~/.claude/config/blago/config.yaml`. Запомни оттуда `workspace_base`, `active_workspace_env` и путь из `workspaces`, соответствующий `active_workspace_env` — назовём его `workspace_path`.
#### Входной артефакт
Транскрипции звонков лежат в `<workspace_path>/<project-slug>/meetings/<stem>.md`. Имя файла `<stem>` — UTC-метка окончания звонка (`YYYY-MM-DD_HHMMSS_mmm`). Этот файл — pull-only: его текст приходит с сервера и в `blago push` не уходит.
Связь meeting-файла с UUID транскрипции хранится в `.blago/index.json` рабочей копии: запись с `entity_type=call_transcription`, `relative_path` равен пути к meeting-файлу, `entity_hash` — UUID транскрипции. Сам `blago transcription memo` этот UUID резолвит автоматически по пути.
`blago pull` всегда кладёт рядом с meeting-файлом `meetings/<stem>.memo.md` (тип `call_transcription_memo`, `entity_hash = "<uuid>:memo"`). Если на сервере memo пустой — файл создаётся пустым. Это сделано специально, чтобы редактирование и публикация шли в одно действие: открыть существующий sibling, дописать, опубликовать. Никаких отдельных шагов «создать файл» делать не нужно.
#### Формат содержимого `.memo.md`
Жёсткие требования к телу memo (применяй всегда, даже без напоминания пользователя):
1.**Первая строка — резюме одним предложением, длиной ≤150 символов.** Это «о чём вообще был звонок». Без markdown-форматирования (без `#`, `*`, `-`, ссылок, цитат), без переноса строки внутри. Считай длину в символах (Unicode codepoints), а не в словах. Если не помещается — переформулируй до сути, не сокращай прилагательными.
2. Следующая пустая строка-разделитель, затем — развёртка по сути. Сжимай до минимума:
- оставляй: о чём договорились, что обсудили, какие решения, какие открытые вопросы и кто за что взялся;
- короткие маркированные списки лучше абзацев; имена/числа/даты сохраняй точно.
3. Markdown отображается в desktop UI редактором (Milkdown через `WrappedEditor`). Можно: заголовки `##`/`###`, списки `-`, инлайн `**жирный**`, ссылки. Не используй декоративный markdown, который ничего не добавляет к смыслу.
#### Процедура
1. Получи от пользователя путь к meeting-файлу (или скажи ему: «дай путь к нужному meeting-файлу»). Прочитай meeting-файл целиком.
2. Открой sibling-файл `meetings/<stem>.memo.md` (он уже создан `blago pull`, возможно пустой). Если файла нет — попроси пользователя выполнить `blago pull` для нужного проекта и повтори.
3. Собери summary по правилам выше: первая строка — одно предложение до 150 символов, дальше пустая строка и сжатая суть. По прямому промпту пользователя адаптируй акценты (что выделить, какие выводы). Если промпта нет — попроси его. Запиши результат в существующий sibling.
4. Покажи пользователю обновлённое содержимое `.memo.md` целиком (или дай явную ссылку на файл). Проверь сам: первая строка — одно предложение, ≤150 символов, без markdown. Если нет — переписывай, пока не выполнится. Спроси, всё ли устраивает. Не публикуй без явного «ок / публикуй / поехали».
По умолчанию команда читает `.memo.md` рядом с указанным meeting-файлом. Альтернативы:
-`blago transcription memo <stem>.md --file path/to/draft.md` — взять текст из произвольного файла;
-`blago transcription memo <UUID> --text "<строка>"` — передать текст строкой и адресовать транскрипцию напрямую по UUID.
6. Команда вернёт строку вида `Memo обновлено на сервере: транскрипция <uuid> (символов: N; источник: …)`. Если сервер ответил ошибкой (например, прав не хватает или сессия истекла) — останови работу, сообщи пользователю текст ошибки и предложи `blago login`.
#### Замечания
-`blago pull` всегда создаёт `meetings/<stem>.memo.md` рядом с каждой meeting-транскрипцией — пустой, если на сервере memo пустой. Файл сразу проиндексирован (`entity_type=call_transcription_memo`), поэтому редактирование→`blago transcription memo` идёт без warning'ов.
- Файл `.memo.md` в `blago push` не уходит (артефакты `meetings/` исключены из staging) — это «локальный канал» вокруг серверного поля `memo`.
- Если ты редактировал sibling локально и не публиковал, а на сервере появилась новая версия — повторный `blago pull` запишет git-style маркеры конфликта (`<<<<<<< blago/local` … `>>>>>>> blago/remote`); разрулите вручную и опубликуй через `blago transcription memo`.
- Если на сервере memo всё ещё пустой, а у тебя локальный черновик не публиковался — `blago pull` сохранит локальный текст и проиндексирует его как baseline; никакие правки не пропадают.
- Чтобы перезаписать опубликованный memo — повтори ту же команду; сервер хранит одно текущее значение поля `memo`.
- Ограничения на длину больше нет (ни на фронте, ни на бэкенде) — можно публиковать развёрнутые саммари с выводами и пунктами.
'Управление транскрипциями звонков: публикация краткого содержания (memo) на сервер (отображается в desktop на странице транскрипции).',
)
transcriptionCmd
.command('memo')
.description(
'Опубликовать краткое содержание (memo) для транскрипции звонка через GraphQL-мутацию chatcoopUpdateTranscriptionMemo. <pathOrId>: путь к `meetings/<stem>.md` (id берётся из .blago/index.json, entityType=call_transcription) или UUID транскрипции. Без --text/--file читает sibling-файл `<stem>.memo.md` рядом с meeting. Доступно председателю и членам совета.',
)
.argument(
'<pathOrId>',
'путь к meeting-файлу или UUID транскрипции',
)
.option('--file <path>','путь к файлу с текстом memo (markdown)')
.option('--text <inline>','текст memo строкой (приоритет над --file и sibling)')
`Staging обновлён: в списке ${stagedPaths.length} путь(ей). Пропущено без изменений относительно индекса: ${skippedUnchanged}; по .blagoignore: ${skippedIgnored}; артефакты только pull (messages/ и meetings/): ${skippedPullOnlyArtifacts}.`,
`Staging обновлён: в списке ${stagedPaths.length} путь(ей). Пропущено без изменений относительно индекса: ${skippedUnchanged}; по .blagoignore: ${skippedIgnored}; артефакты только pull (messages/ и meetings/): ${skippedPullOnlyArtifacts}; не blago-сущности (нет type=project/issue/story во frontmatter): ${skippedNonEntity}.`,
`Конфликт memo транскрипции ${transcriptionId}: локальный черновик и серверная версия различаются. В «${rel}» записаны маркеры слияния («<<<<<<< blago/local» … «>>>>>>> blago/remote»). Оставьте одну версию текста и опубликуйте через «blago transcription memo».`,
'Не указан текст memo. Передайте --text "<строка>" или --file <путь>, либо первым аргументом — путь к meeting-файлу (тогда читается `<stem>.memo.md` рядом).',
`Рядом с «${path.basename(resolved.meetingAbsPath)}» нет файла «${path.basename(siblingAbs)}». Выполните «blago pull» (он создаст sibling-файл; пустой, если на сервере memo пустой), затем отредактируйте и повторите команду — либо передайте --file / --text.`,
)
}
}
functiontoIso(v: unknown,fallback: Date):string{
if(vinstanceofDate){
returnv.toISOString()
}
if(typeofv==='string'||typeofv==='number'){
constd=newDate(v)
if(!Number.isNaN(d.getTime())){
returnd.toISOString()
}
}
returnfallback.toISOString()
}
/**
* После успешной мутации записываем `meetings/<stem>.memo.md` рядом с meeting-файлом и
* заносим запись в `.blago/index.json` с типом `call_transcription_memo`, чтобы:
* - следующий `blago pull` видел индексированный файл и шёл по штатной ветке syncEntityFile;
* - `blago push` его не подхватывал — он внутри `meetings/`, исключённого через `pull-only-paths`.
* Это шаг только при наличии meeting-пути (sibling-резолв из <pathOrId>); UUID-режим без файла — пропускаем.
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).
// баланса упразднена. Резерв средств под заявку на возврат паевого теперь
// выражается переводом на отдельный кошелёк-резерв `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, без 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 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
ISSUE=0,///< первичный вход средств на кошелёк wallet_to (wallet_from = empty)
TRANSFER=1,///< перемещение wallet_from → wallet_to (с Dr/Cr ИЛИ без — по парам account_id)
BURN=4,///< изъятие amount с wallet_from->available, без wallet_to. Покрывает оба кейса: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY; (b) зеркало ISSUE при `ledger2::revert` (различие — через operation_code: `o.adj.rev` для adjustment-mirror).
NONE=5,///< только бухпроводка без перемещения средств (wallet_from = empty, wallet_to = empty, debit ≠ 0, credit ≠ 0). Покрывает кейсы внутрибалансовых проводок типа Dr 04 / Cr 08 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
};
/**
@@ -144,7 +154,7 @@ enum class WalletOp : uint8_t {
staticconstexpreosio::nameWITHDRAWALS_SINK="w.wal.wthdrw"_n;///< DEPRECATED 2026-05-21: исторический sink возвратов. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
staticconstexpreosio::nameWITHDRAW_PENDING="w.wal.wpend"_n;///< Резерв паевого под заявку на возврат (COOPERATIVE-пул). o.wal.wthreq переводит сюда с w.wal.share, o.wal.wthdec возвращает обратно, o.wal.wthcpl сжигает отсюда. Заменил механику blocked/BLOCK/UNBLOCK 2026-05-24.
staticconstexpreosio::nameBLAGOROST_FUND="w.cap.blago"_n;///< Благорост — единый агрегированный кошелёк программы (USER_SHARED; ADR-009)
staticconstexpreosio::nameGENERATOR_FUND="w.cap.gen"_n;///< Генератор — единый агрегированный кошелёк программы (USER_SHARED; ADR-009)
staticconstexpreosio::nameGENERATOR_FUND="w.cap.gen"_n;///< Генератор — единый агрегированный кошелёк программы (COOPERATIVE — кооперативный пул, без L3-разреза по пайщику; L3-разрез из ADR-009 отменён из-за несовместимости с CRPS-перераспределением, см. wallets.hpp:107)
staticconstexpreosio::namePREIMP_FUND="w.cap.preimp"_n;///< Первичный учёт РИД-взносов до перехода на электронный учёт (USER_SHARED; o.cap.preimp / o.cap.drppre)
"____comment": "This file was generated with eosio-abigen. DO NOT EDIT ",
"version": "eosio::abi/1.2",
"types": [],
"structs": [
{
"name": "counts",
"base": "",
"fields": [
{
"name": "key",
"type": "name"
},
{
"name": "secondary_key",
"type": "name"
},
{
"name": "value",
"type": "uint64"
}
]
},
{
"name": "hello",
"base": "",
"fields": [
{
"name": "message",
"type": "string"
}
]
}
],
"actions": [
{
"name": "hello",
"type": "hello",
"ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Тестовое действие\nsummary: Контракт вызывает тестовое действие\nvars: \n - name: message\n label: Сообщение\n hint: Введите сообщение\n\n---\n\nСообщение: {{message}}\n\nВыражаю своё согласие с тем, что информация, отправляемая Обществом в sms-сообщениях на указанный мной номер телефона или в сообщениях на указанный мной адрес электронной почты или в сообщениях телеграм-бота, привязанного к моему телефону, приравняется к уведомлению меня Обществом в письменной форме."
}
],
"tables": [
{
"name": "counts",
"type": "counts",
"index_type": "i64",
"key_names": [],
"key_types": []
}
],
"ricardian_clauses": [
{
"id": "UserAgreement",
"body": "User agreement for the chain can go here."
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.