controller meets v2

This commit is contained in:
Alex Ant
2026-04-04 00:13:22 +05:00
parent 333553d283
commit 94ab81fa64
19 changed files with 242 additions and 49 deletions
+72
View File
@@ -1,3 +1,75 @@
# v2026.4.2-2
В этом релизе — стабилизация Благороста для вывода в продуктивную работу с результатами интеллектуальной деятельности. Отдельно заложена основа под отчётность в ФНС и ФСС и прототип поиска по документам.
**Благорост и проекты**
- Быстрые действия на странице программы: создать проект, компонент, задачу или требование.
- Требования к компонентам в репозитории: поддержка **Mermaid**, **Draw.io** и **BPMN**.
- Синхронизация проектов, компонентов, требований и задач с Git-репозиторием результатов.
- Встраивание полноразмерного видео (iframe) на страницах проектов.
- Скачивание пакета подписанных документов одной кнопкой.
- Комнаты проектов в кооперативном мессенджере.
**Мессенджер и звонки**
- Автосекретарь: запись синхронных звонков, текстовых и голосовых сообщений в проектных комнатах.
**Отчётность и инфраструктура**
- Прототип фабрики отчётов ФНС/ФСС: выгрузка в XML для дальнейшей отправки.
- Прототип поисковой системы по документам.
- Установщик для развёртывания на своих серверах (разработка или эксплуатация).
- Рефакторинг в сторону чистой архитектуры на бэкенде.
- Ускорение сборки фронтенда за счёт перехода на **Vite 8**.
**Исправления**
- Повторное общее собрание больше не мешало завершить онбординг кооператива.
- Центр уведомлений не блокировал загрузку рабочего стола при отключённом провайдере оповещений.
- Уведомления о собрании совета по свободным вопросам снова доходят до членов совета.
- В интерфейсе восстановлено отображение контактов кооператива.
#releases
---
# v2026.4.2-2
В этом релизе — стабилизация Благороста для вывода в продуктивную работу с результатами интеллектуальной деятельности. Отдельно заложена основа под отчётность в ФНС и ФСС и прототип поиска по документам.
**Благорост и проекты**
- Быстрые действия на странице программы: создать проект, компонент, задачу или требование.
- Требования к компонентам в репозитории: поддержка **Mermaid**, **Draw.io** и **BPMN**.
- Синхронизация проектов, компонентов, требований и задач с Git-репозиторием результатов.
- Встраивание полноразмерного видео (iframe) на страницах проектов.
- Скачивание пакета подписанных документов одной кнопкой.
- Комнаты проектов в кооперативном мессенджере.
**Мессенджер и звонки**
- Автосекретарь: запись синхронных звонков, текстовых и голосовых сообщений в проектных комнатах.
**Отчётность и инфраструктура**
- Прототип фабрики отчётов ФНС/ФСС: выгрузка в XML для дальнейшей отправки.
- Прототип поисковой системы по документам.
- Установщик для развёртывания на своих серверах (разработка или эксплуатация).
- Рефакторинг в сторону чистой архитектуры на бэкенде.
- Ускорение сборки фронтенда за счёт перехода на **Vite 8**.
**Исправления**
- Повторное общее собрание больше не мешало завершить онбординг кооператива.
- Центр уведомлений не блокировал загрузку рабочего стола при отключённом провайдере оповещений.
- Уведомления о собрании совета по свободным вопросам снова доходят до членов совета.
- В интерфейсе восстановлено отображение контактов кооператива.
#releases
---
# v2025.12.28-8
В этой версии представлен прототип трекера результатов интеллектуальной деятельности, реализован мост в 1С, обновлен интерфейс и существенно повышена стабильность системы.
+1 -1
View File
@@ -16,7 +16,7 @@
"max_transaction_lifetime": 3600,
"deferred_trx_expiration_window": 600,
"max_transaction_delay": 3888000,
"max_inline_action_size": 4096,
"max_inline_action_size": 65536,
"max_inline_action_depth": 10,
"max_authority_depth": 6
},
@@ -1,10 +1,13 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { IsArray, IsNotEmpty, IsOptional, IsString, MaxLength, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import type { CreateAnnualGeneralMeetInputDomainInterface } from '~/domain/meet/interfaces/create-annual-meet-input-domain.interface';
import { AgendaGeneralMeetPointInputDTO } from './agenda-meet-point-input.dto';
import { AnnualGeneralMeetingAgendaSignedDocumentInputDTO } from '~/application/document/documents-dto/annual-general-meeting-agenda-document.dto';
/** Согласовано с лимитом на desktop (повестка / форма собрания) */
const MEET_DETAILS_MAX_LEN = 10_000;
@InputType('CreateAnnualGeneralMeetInput')
export class CreateAnnualGeneralMeetInputDTO implements CreateAnnualGeneralMeetInputDomainInterface {
@Field(() => String, { description: 'Имя аккаунта кооператива' })
@@ -43,4 +46,13 @@ export class CreateAnnualGeneralMeetInputDTO implements CreateAnnualGeneralMeetI
@Field(() => AnnualGeneralMeetingAgendaSignedDocumentInputDTO, { description: 'Предложение повестки собрания' })
proposal!: AnnualGeneralMeetingAgendaSignedDocumentInputDTO;
@Field(() => String, {
nullable: true,
description: 'Дополнительная информация о формате собрания (ссылка, как участвовать и т.д.)',
})
@IsOptional()
@IsString()
@MaxLength(MEET_DETAILS_MAX_LEN)
details?: string | null;
}
@@ -65,6 +65,13 @@ export class MeetPreProcessingDTO implements MeetPreProcessingDomainInterface {
@IsOptional()
proposal?: DocumentAggregateDTO;
@Field(() => String, {
nullable: true,
description: 'Дополнительная информация о формате собрания',
})
@IsOptional()
details?: string | null;
constructor(
data: MeetPreProcessingDomainInterface,
presiderCertificate?: UserCertificateDomainInterface | null,
@@ -80,6 +87,7 @@ export class MeetPreProcessingDTO implements MeetPreProcessingDomainInterface {
this.open_at = data.open_at;
this.close_at = data.close_at;
this.proposal = data.proposal ? new DocumentAggregateDTO(data.proposal) : undefined;
this.details = data.details ?? null;
// Обрабатываем сертификат инициатора
if (initiatorCertificate) {
@@ -1,9 +1,11 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsString, IsDate, ValidateNested } from 'class-validator';
import { IsString, IsDate, IsOptional, MaxLength, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { AnnualGeneralMeetingAgendaSignedDocumentInputDTO } from '~/application/document/documents-dto/annual-general-meeting-agenda-document.dto';
import { RestartAnnualGeneralMeetInputDomainInterface } from '~/domain/meet/interfaces/restart-annual-general-meet-input-domain.interface';
const MEET_DETAILS_MAX_LEN = 10_000;
@InputType('RestartAnnualGeneralMeetInput', { description: 'DTO для перезапуска ежегодного общего собрания кооператива' })
export class RestartAnnualGeneralMeetInputDTO implements RestartAnnualGeneralMeetInputDomainInterface {
@Field(() => String, { description: 'Имя аккаунта кооператива' })
@@ -29,4 +31,14 @@ export class RestartAnnualGeneralMeetInputDTO implements RestartAnnualGeneralMee
@IsDate()
@Type(() => Date)
new_close_at!: Date;
@Field(() => String, {
nullable: true,
description:
'Доп. информация для нового слота; передайте явно, если нужна в повестке и уведомлениях. Поле не передано — у нового собрания details пустой',
})
@IsOptional()
@IsString()
@MaxLength(MEET_DETAILS_MAX_LEN)
details?: string | null;
}
@@ -19,6 +19,8 @@ import { ProcessMeetDecisionInputDomainInterface } from '~/domain/meet/interface
import { MeetProcessedDomainEntity } from '~/domain/meet/entities/meet-processed-domain.entity';
import { NotifyOnAnnualGeneralMeetInputDomainInterface } from '~/domain/meet/interfaces/notify-on-annual-general-meet-input-domain.interface';
import { generateUniqueHash } from '~/utils/generate-hash.util';
import { HttpApiError } from '~/utils/httpApiError';
import httpStatus from 'http-status';
@Injectable()
export class MeetInteractor {
@@ -48,10 +50,13 @@ export class MeetInteractor {
// Генерируем уникальный хэш для собрания
const hash = generateUniqueHash();
const detailsNormalized = MeetInteractor.normalizeMeetDetailsField(data.details);
const preProcessing = new MeetPreProcessingDomainEntity({
...data,
hash,
proposal: proposalAggregate,
details: detailsNormalized,
});
// Сохраняем данные в репозиторий
@@ -60,6 +65,8 @@ export class MeetInteractor {
// Вызов блокчейн порта для создания собрания
await this.meetBlockchainPort.createMeet({ ...data, hash });
// Задержка синхронизации на 1 секунду, чтобы избежать ошибки "row not found"
await new Promise(resolve => setTimeout(resolve, 1000));
// Получаем обновленные данные из блокчейна
const processingData = await this.meetBlockchainPort.getMeet({
coopname: data.coopname,
@@ -173,22 +180,59 @@ export class MeetInteractor {
}
async restartMeet(data: RestartAnnualGeneralMeetInputDomainInterface): Promise<MeetAggregate> {
// Вызов блокчейн порта для перезапуска собрания
const oldPre = await this.meetPreRepository.findByHash(data.hash);
const new_hash = await this.meetBlockchainPort.restartMeet(data);
// Получаем обновленные данные из обоих источников
const processingData = await this.meetBlockchainPort.getMeet({
coopname: data.coopname,
hash: new_hash,
});
if (!processingData) {
throw new HttpApiError(httpStatus.NOT_FOUND, 'Собрание после перезапуска не найдено в блокчейне');
}
const proposalAggregate = await this.documentAggregator.buildDocumentAggregate(data.newproposal);
const meetRow = processingData.meet;
const agendaFromChain = processingData.questions.map((q) => ({
context: q.context,
title: q.title,
decision: q.decision,
}));
const detailsForNewPre = MeetInteractor.normalizeMeetDetailsField(data.details);
const preProcessing = new MeetPreProcessingDomainEntity({
hash: new_hash,
coopname: meetRow.coopname,
initiator: oldPre?.initiator ?? meetRow.initiator,
presider: oldPre?.presider ?? meetRow.presider,
secretary: oldPre?.secretary ?? meetRow.secretary,
agenda: agendaFromChain,
open_at: meetRow.open_at,
close_at: meetRow.close_at,
proposal: proposalAggregate ?? undefined,
details: detailsForNewPre,
});
await this.meetPreRepository.create(preProcessing);
const preMeet = await this.meetPreRepository.findByHash(new_hash);
const processedMeetEntity = await this.meetProcessedRepository.findByHash(new_hash);
// Создаем агрегат из обоих источников данных
return new MeetAggregate(preMeet, processingData, processedMeetEntity);
}
/** Пустая строка → отсутствие значения в PG */
private static normalizeMeetDetailsField(value: string | null | undefined): string | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
const t = value.trim();
return t.length > 0 ? t : null;
}
async signBySecretaryOnAnnualGeneralMeet(
data: SignBySecretaryOnAnnualGeneralMeetInputDomainInterface
): Promise<MeetAggregate> {
@@ -11,6 +11,7 @@ export class MeetPreProcessingDomainEntity implements MeetPreProcessingDomainInt
public readonly open_at!: Date;
public readonly close_at!: Date;
public readonly proposal?: DocumentAggregateDomainInterface;
public readonly details?: string | null;
constructor(data: MeetPreProcessingDomainInterface) {
Object.assign(this, data);
@@ -9,4 +9,5 @@ export type CreateAnnualGeneralMeetInputDomainInterface = Omit<
open_at: Date;
close_at: Date;
hash?: string;
details?: string | null;
};
@@ -21,6 +21,8 @@ export interface MeetPreProcessingDomainInterface {
open_at: Date;
close_at: Date;
proposal?: DocumentAggregateDomainInterface;
/** Текст уровня собрания (ссылка на трансляцию, формат участия и т.п.), только в PG */
details?: string | null;
}
assertType<AssertKeysMatch<MeetContract.Actions.CreateMeet.IInput, MeetPreProcessingDomainInterface>>();
@@ -9,4 +9,6 @@ export interface RestartAnnualGeneralMeetInputDomainInterface {
newproposal: ISignedDocumentDomainInterface;
new_open_at: Date;
new_close_at: Date;
/** Если передано — пишем в meet_pre нового hash; если в мутации не передано — details у нового pre пустой (старый pre не подмешиваем) */
details?: string | null;
}
@@ -312,7 +312,21 @@ export class GenerationService {
return [];
}
const rooms = await this.projectCommArtifacts.listCommunicationRoomsForProject(anchorProjectHash);
const projectForStory = await this.projectRepository.findByHash(anchorProjectHash);
if (!projectForStory) {
return [];
}
// Компоненты не имеют отдельных записей чатов в реестре: комната Matrix у корневого проекта (parent_hash).
let matrixRoomProjectHash = anchorProjectHash.trim().toLowerCase();
if (projectForStory.isComponent()) {
const parentHash = projectForStory.parent_hash?.trim().toLowerCase() ?? '';
if (parentHash) {
matrixRoomProjectHash = parentHash;
}
}
const rooms = await this.projectCommArtifacts.listCommunicationRoomsForProject(matrixRoomProjectHash);
if (rooms.length === 0) {
return [];
}
@@ -1,4 +1,5 @@
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { MEET_REPOSITORY, MeetPreProcessingRepository } from '~/domain/meet/repositories/meet-pre.repository';
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
import { NovuWorkflowAdapter } from '~/infrastructure/novu/novu-workflow.adapter';
import { NOVU_WORKFLOW_PORT } from '~/domain/notification/interfaces/novu-workflow.port';
@@ -20,6 +21,8 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
private readonly novuWorkflowAdapter: NovuWorkflowAdapter,
@Inject(ACCOUNT_DATA_PORT)
private readonly accountPort: AccountDataPort,
@Inject(MEET_REPOSITORY)
private readonly meetPreRepository: MeetPreProcessingRepository,
private readonly logger: WinstonLoggerService
) {
this.logger.setContext(MeetWorkflowNotificationService.name);
@@ -56,6 +59,16 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
return config.timezone === 'Europe/Moscow' ? 'МСК' : config.timezone;
}
/**
* Текст из meet_pre только для workflow «до/у начала»: meet-initial, meet-reminder-start, meet-started.
* В reminder-end, restart, ended поле details в схемах Novu нет — не передаём, иначе ошибка валидации.
*/
private async meetDetailsPayloadPart(hash: string): Promise<{ details?: string }> {
const pre = await this.meetPreRepository.findByHash(hash);
const trimmed = pre?.details?.trim();
return trimmed ? { details: trimmed } : {};
}
// Получение всех email-адресов пользователей
private async getAllUserEmails(): Promise<Array<{ username: string; email: string }>> {
try {
@@ -113,6 +126,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
const timezone = this.getTimezoneDisplay();
const meetUrl = this.getNotificationUrl(meet);
const detailsPart = await this.meetDetailsPayloadPart(meet.hash);
const payload: Workflows.MeetInitial.IPayload = {
coopShortName,
meetId: meet.id,
@@ -122,6 +136,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
meetEndTime,
timezone,
meetUrl,
...detailsPart,
};
// Отправляем уведомления каждому пользователю в цикле
@@ -166,6 +181,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
const meetUrl = this.getNotificationUrl(meet);
const detailsPart = await this.meetDetailsPayloadPart(meet.hash);
const payload: Workflows.MeetReminderStart.IPayload = {
coopShortName,
meetId: meet.id,
@@ -173,6 +189,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
meetTime,
timeDescription,
meetUrl,
...detailsPart,
};
// Отправляем уведомления каждому пользователю в цикле
@@ -211,6 +228,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
const timezone = this.getTimezoneDisplay();
const meetUrl = this.getNotificationUrl(meet);
const detailsPart = await this.meetDetailsPayloadPart(meet.hash);
const payload: Workflows.MeetStarted.IPayload = {
coopShortName,
meetId: meet.id,
@@ -218,6 +236,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
meetEndTime,
timezone,
meetUrl,
...detailsPart,
};
// Отправляем уведомления каждому пользователю в цикле
@@ -263,6 +282,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
const timezone = this.getTimezoneDisplay();
const meetUrl = this.getNotificationUrl(meet);
const detailsPart = await this.meetDetailsPayloadPart(meet.hash);
const payload: Workflows.MeetReminderEnd.IPayload = {
coopShortName,
meetId: meet.id,
@@ -271,6 +291,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
timeDescription,
timezone,
meetUrl,
...detailsPart,
};
// Отправляем уведомления каждому пользователю в цикле
@@ -181,9 +181,11 @@ export class MeetBlockchainAdapter implements MeetBlockchainPort {
this.blockchainService.initialize(data.coopname, wif);
// Преобразуем доменный объект в инфраструктурный тип
const { details: _details, ...meetPayload } = data;
// Преобразуем доменный объект в инфраструктурный тип (поле details только в PG, не в цепи)
const blockchainData: MeetContract.Actions.CreateMeet.IInput = {
...data,
...meetPayload,
hash: data.hash || generateUniqueHash(),
proposal: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.proposal),
open_at: this.domainToBlockchainUtils.convertDateToBlockchainFormat(data.open_at),
@@ -34,6 +34,9 @@ export class MeetPreEntity implements MeetPreProcessingDomainInterface {
@Column('jsonb', { nullable: true })
proposal?: DocumentAggregateDomainInterface;
@Column('text', { nullable: true })
details?: string | null;
@CreateDateColumn()
created_at!: Date;
@@ -48,6 +51,7 @@ export class MeetPreEntity implements MeetPreProcessingDomainInterface {
this.open_at = data.open_at;
this.close_at = data.close_at;
this.proposal = data.proposal;
this.details = data.details ?? null;
}
}
@@ -63,6 +67,7 @@ export class MeetPreEntity implements MeetPreProcessingDomainInterface {
open_at: this.open_at,
close_at: this.close_at,
proposal: this.proposal,
details: this.details ?? null,
});
}
@@ -16,6 +16,7 @@ export const meetInitialPayloadSchema = z.object({
meetEndTime: z.string(),
timezone: z.string(),
meetUrl: z.string(),
details: z.string().optional(),
});
export type IPayload = z.infer<typeof meetInitialPayloadSchema>;
@@ -36,12 +37,12 @@ export const workflow: WorkflowDefinition<IWorkflow> = WorkflowBuilder
createEmailStep(
'meet-initial-email',
'Уведомление о общем собрании пайщиков №{{payload.meetId}} в {{payload.coopShortName}}',
'Уважаемый пайщик!<br><br>В кооперативе объявлено новое общее собрание №{{payload.meetId}}.<br><br>Дата и время начала: {{payload.meetDate}} в {{payload.meetTime}} ({{payload.timezone}})<br>Дата и время завершения: {{payload.meetEndDate}} в {{payload.meetEndTime}} ({{payload.timezone}})<br><br>Для ознакомления с повесткой, пожалуйста, перейдите по ссылке:<br><a href="{{payload.meetUrl}}">{{payload.meetUrl}}</a><br><br>С уважением, Совет {{payload.coopShortName}}.'
'Уважаемый пайщик!<br><br>В кооперативе объявлено новое общее собрание №{{payload.meetId}}.<br><br>Дата и время начала: {{payload.meetDate}} в {{payload.meetTime}} ({{payload.timezone}})<br>Дата и время завершения: {{payload.meetEndDate}} в {{payload.meetEndTime}} ({{payload.timezone}})<br><br>Для ознакомления с повесткой, пожалуйста, перейдите по ссылке:<br><a href="{{payload.meetUrl}}">{{payload.meetUrl}}</a>{{#if payload.details}}<br><br>Дополнительная информация:<div style="white-space:pre-wrap;">{{payload.details}}</div>{{/if}}<br><br>С уважением, Совет {{payload.coopShortName}}.'
),
createInAppStep(
'meet-initial-notification',
'Новое общее собрание №{{payload.meetId}}',
'Назначено собрание на {{payload.meetDate}} в {{payload.meetTime}}'
'Назначено собрание на {{payload.meetDate}} в {{payload.meetTime}}{{#if payload.details}}. {{payload.details}}{{/if}}'
),
createPushStep(
'meet-initial-push',
@@ -14,6 +14,7 @@ export const meetReminderStartPayloadSchema = z.object({
meetTime: z.string(),
timeDescription: z.string(),
meetUrl: z.string(),
details: z.string().optional(),
});
export type IPayload = z.infer<typeof meetReminderStartPayloadSchema>;
@@ -34,12 +35,12 @@ export const workflow: WorkflowDefinition<IWorkflow> = WorkflowBuilder
createEmailStep(
'meet-reminder-start-email',
'Напоминание о предстоящем общем собрании №{{payload.meetId}} в {{payload.coopShortName}}',
'Уважаемый пайщик!<br><br>Напоминаем, что {{payload.timeDescription}} состоится общее собрание пайщиков №{{payload.meetId}} ({{payload.meetDate}} в {{payload.meetTime}}).<br><br>Для ознакомления с повесткой и подписи уведомления, пожалуйста, перейдите по ссылке:<br><a href="{{payload.meetUrl}}">{{payload.meetUrl}}</a><br><br>С уважением, Совет {{payload.coopShortName}}.'
'Уважаемый пайщик!<br><br>Напоминаем, что {{payload.timeDescription}} состоится общее собрание пайщиков №{{payload.meetId}} ({{payload.meetDate}} в {{payload.meetTime}}).<br><br>Для ознакомления с повесткой и подписи уведомления, пожалуйста, перейдите по ссылке:<br><a href="{{payload.meetUrl}}">{{payload.meetUrl}}</a>{{#if payload.details}}<br><br>Дополнительная информация:<div style="white-space:pre-wrap;">{{payload.details}}</div>{{/if}}<br><br>С уважением, Совет {{payload.coopShortName}}.'
),
createInAppStep(
'meet-reminder-start-notification',
'Напоминание о собрании №{{payload.meetId}}',
'Собрание начнется {{payload.timeDescription}}'
'Собрание начнется {{payload.timeDescription}}{{#if payload.details}}. {{payload.details}}{{/if}}'
),
createPushStep(
'meet-reminder-start-push',
@@ -14,6 +14,7 @@ export const meetStartedPayloadSchema = z.object({
meetEndTime: z.string(),
timezone: z.string(),
meetUrl: z.string(),
details: z.string().optional(),
});
export type IPayload = z.infer<typeof meetStartedPayloadSchema>;
@@ -34,12 +35,12 @@ export const workflow: WorkflowDefinition<IWorkflow> = WorkflowBuilder
createEmailStep(
'meet-started-email',
'Собрание пайщиков №{{payload.meetId}} в {{payload.coopShortName}} началось',
'Уважаемый пайщик!<br><br>Сегодня началось общее собрание пайщиков №{{payload.meetId}}.<br>Собрание будет проходить до {{payload.meetEndDate}} {{payload.meetEndTime}} ({{payload.timezone}}).<br><br>Просим принять участие в голосовании по вопросам повестки дня.<br>Для голосования перейдите по ссылке:<br><a href="{{payload.meetUrl}}">{{payload.meetUrl}}</a><br><br>С уважением, Совет {{payload.coopShortName}}.'
'Уважаемый пайщик!<br><br>Сегодня началось общее собрание пайщиков №{{payload.meetId}}.<br>Собрание будет проходить до {{payload.meetEndDate}} {{payload.meetEndTime}} ({{payload.timezone}}).<br><br>Просим принять участие в голосовании по вопросам повестки дня.<br>Для голосования перейдите по ссылке:<br><a href="{{payload.meetUrl}}">{{payload.meetUrl}}</a>{{#if payload.details}}<br><br>Дополнительная информация:<div style="white-space:pre-wrap;">{{payload.details}}</div>{{/if}}<br><br>С уважением, Совет {{payload.coopShortName}}.'
),
createInAppStep(
'meet-started-notification',
'Собрание №{{payload.meetId}} началось',
'Примите участие в голосовании'
'Примите участие в голосовании{{#if payload.details}}. {{payload.details}}{{/if}}'
),
createPushStep(
'meet-started-push',
+26 -32
View File
@@ -1,39 +1,33 @@
# v2025.12.28-8
# v2026.4.2-2
В этой версии представлен прототип трекера результатов интеллектуальной деятельности, реализован мост в 1С, обновлен интерфейс и существенно повышена стабильность системы.
В этом релизе — стабилизация Благороста для вывода в продуктивную работу с результатами интеллектуальной деятельности. Отдельно заложена основа под отчётность в ФНС и ФСС и прототип поиска по документам.
---
**Благорост и проекты**
### ✨ Новые функции
- [#332](https://github.com/coopenomics/mono/issues/332): Прототип конструктора требований дополнительных документов при регистрации
- [#330](https://github.com/coopenomics/mono/issues/330): Прототип моста в 1С:Бухгалтерию для передачи документов и проводок
- [#329](https://github.com/coopenomics/mono/issues/329): Интеграция и тестирование LMS TUTOR для образовательных задач
- [#328](https://github.com/coopenomics/mono/issues/328): Размещение прототипов мульти-лендингов на цифровой-кооператив.рф и coopenomics.world
- [#326](https://github.com/coopenomics/mono/issues/326): Минимальный интерфейс трекера результатов интеллектуальной деятельности
- [#324](https://github.com/coopenomics/mono/issues/324): Смарт-контракт генерации и капитализации результатов интеллектуальной деятельности ("Благорост")
- [#322](https://github.com/coopenomics/mono/issues/322): Поставка обновлений ПО с нулевым даунтаймом по blue-green стратегии
- [#321](https://github.com/coopenomics/mono/issues/321): Внедрение системы проводок по фондам для контрактов
- [#319](https://github.com/coopenomics/mono/issues/319): Палитра команд и быстрый доступ к страницам рабочих столов (cmk+k)
- [#316](https://github.com/coopenomics/mono/issues/316): Переход рабочего стола на GraphQL SDK
- [#314](https://github.com/coopenomics/mono/issues/314): Развёртывание GlitchTip для мониторинга ошибок
- [#306](https://github.com/coopenomics/mono/issues/306): Модуль запросов и мутаций для контракта капитализации
- Быстрые действия на странице программы: создать проект, компонент, задачу или требование.
- Требования к компонентам в репозитории: поддержка **Mermaid**, **Draw.io** и **BPMN**.
- Синхронизация проектов, компонентов, требований и задач с Git-репозиторием результатов.
- Встраивание полноразмерного видео (iframe) на страницах проектов.
- Скачивание пакета подписанных документов одной кнопкой.
- Комнаты проектов в кооперативном мессенджере.
### 🐛 Исправления ошибок
- [#312](https://github.com/coopenomics/mono/issues/312): Исправление подписки на изменение статуса коммитов
- [#309](https://github.com/coopenomics/mono/issues/309): Исправление отображения чужих билетов времени в трекере
**Мессенджер и звонки**
### 🔧 Улучшения
- [#331](https://github.com/coopenomics/mono/issues/331): Настройка системы мониторинга сбоев и ошибок на базе GlitchTIP, Loki, Prometheus
- [#327](https://github.com/coopenomics/mono/issues/327): Пользовательская документация по интерфейсам цифрового кооператива
- [#325](https://github.com/coopenomics/mono/issues/325): Документирование смарт-контракта программы "Благорост"
- [#308](https://github.com/coopenomics/mono/issues/308): Улучшение отображения рабочих столов в магазине приложений
- [#307](https://github.com/coopenomics/mono/issues/307): Объединение настроек контракта с нативными настройками приложения
- [#305](https://github.com/coopenomics/mono/issues/305): Объединение полей title и description в проекте
- [#304](https://github.com/coopenomics/mono/issues/304): Доменная модель контракта капитализации на бэкенде
- [#303](https://github.com/coopenomics/mono/issues/303): Пересмотр архитектуры парсера и формирования локальной истории
- [#302](https://github.com/coopenomics/mono/issues/302): Рефакторинг архитектуры, внедрение двухконтурной шины данных и обработки микрофорков
- [#301](https://github.com/coopenomics/mono/issues/301): Доработка и отладка контракта "Капитализация РИД" v0.2
- [#222](https://github.com/coopenomics/mono/issues/222): Внедрение метода Водянова для распределения пула премий по программе "Благорост"
- [#212](https://github.com/coopenomics/mono/issues/212): Снижение точности валютных значений до двух знаков после запятой в документах
- Автосекретарь: запись синхронных звонков, текстовых и голосовых сообщений в проектных комнатах.
**Отчётность и инфраструктура**
- Прототип фабрики отчётов ФНС/ФСС: выгрузка в XML для дальнейшей отправки.
- Прототип поисковой системы по документам.
- Установщик для развёртывания на своих серверах (разработка или эксплуатация).
- Рефакторинг в сторону чистой архитектуры на бэкенде.
- Ускорение сборки фронтенда за счёт перехода на **Vite 8**.
**Исправления**
- Повторное общее собрание больше не мешало завершить онбординг кооператива.
- Центр уведомлений не блокировал загрузку рабочего стола при отключённом провайдере оповещений.
- Уведомления о собрании совета по свободным вопросам снова доходят до членов совета.
- В интерфейсе восстановлено отображение контактов кооператива.
#releases
+2 -2
View File
@@ -1,4 +1,4 @@
VERSION: v2025.12.28-8
FROM DATE: 2025-07-2T10:00:00+03:00
VERSION: v2026.4.2-2
FROM DATE: 2025-12-28T10:00:00+03:00
COMMENT:
Релиз содержит прототип трекера результатов интеллектуальной деятельности, прототип моста в 1С, обновленный интерфейс и значительно повышенную стабильность.