From 2e46cc41a1858d79ff6b100fc5a456065d5862ee Mon Sep 17 00:00:00 2001 From: vigdorov Date: Thu, 15 Jan 2026 02:36:24 +0300 Subject: [PATCH] fix lint --- .drone.yml | 6 +- E2E_TESTING.md | 19 +- backend/src/ai/ai.controller.ts | 7 +- backend/src/ai/ai.module.ts | 4 +- backend/src/ai/ai.service.ts | 84 ++++++-- backend/src/comments/comments.service.ts | 5 +- backend/src/ideas/entities/idea.entity.ts | 14 +- .../1736899200000-CreateCommentsTable.ts | 4 +- .../1736899400000-CreateRolesTable.ts | 4 +- .../src/team/entities/team-member.entity.ts | 19 +- backend/src/team/roles.service.ts | 14 +- backend/src/team/team.service.ts | 22 +- frontend/eslint.config.js | 2 +- frontend/src/App.tsx | 11 +- .../AiEstimateModal/AiEstimateModal.tsx | 41 +++- .../CommentsPanel/CommentsPanel.tsx | 27 ++- .../CreateIdeaModal/CreateIdeaModal.tsx | 4 +- .../components/IdeasFilters/IdeasFilters.tsx | 30 ++- .../components/IdeasTable/ColorPickerCell.tsx | 10 +- .../components/IdeasTable/DraggableRow.tsx | 7 +- .../src/components/IdeasTable/IdeasTable.tsx | 126 +++++++----- .../src/components/IdeasTable/columns.tsx | 55 ++++- .../SpecificationModal/SpecificationModal.tsx | 189 ++++++++++++------ .../src/components/TeamPage/RolesManager.tsx | 88 ++++++-- .../components/TeamPage/TeamMemberModal.tsx | 80 +++++--- frontend/src/components/TeamPage/TeamPage.tsx | 123 +++++++++--- frontend/src/hooks/useAi.ts | 23 ++- frontend/src/hooks/useAuth.ts | 23 ++- frontend/src/hooks/useComments.ts | 13 +- frontend/src/hooks/useTeam.ts | 6 +- frontend/src/services/ai.ts | 32 ++- frontend/src/services/comments.ts | 5 +- frontend/src/services/team.ts | 7 +- frontend/src/types/idea.ts | 7 +- frontend/src/types/team.ts | 4 +- package-lock.json | 77 ++++++- package.json | 7 +- tests/e2e/phase2.spec.ts | 20 +- tests/e2e/phase3.spec.ts | 8 +- ...33d5db6370b6de345e990751aa1f1da65ad675.png | Bin 4253 -> 0 bytes tests/playwright-report/index.html | 2 +- tests/playwright/.auth/user.json | 12 +- 42 files changed, 940 insertions(+), 301 deletions(-) delete mode 100644 tests/playwright-report/data/7a33d5db6370b6de345e990751aa1f1da65ad675.png diff --git a/.drone.yml b/.drone.yml index f645998..001f235 100644 --- a/.drone.yml +++ b/.drone.yml @@ -70,14 +70,13 @@ steps: from_secret: HARBOR_PASSWORD no_push_metadata: true -# --- Сборка Keycloak темы --- +# --- Сборка Keycloak темы (только при изменениях в keycloak-theme/) --- - name: build-keycloak-theme image: plugins/kaniko when: changeset: includes: - keycloak-theme/** - - .drone.yml excludes: - keycloak-theme/README.md - keycloak-theme/**/*.md @@ -220,7 +219,7 @@ steps: fi - echo "✅ Frontend deployed to PROD (image:$IMAGE_TAG)" -# --- Развертывание Keycloak темы --- +# --- Развертывание Keycloak темы (только при изменениях в keycloak-theme/) --- - name: deploy-keycloak-theme image: alpine/k8s:1.28.2 depends_on: @@ -229,7 +228,6 @@ steps: changeset: includes: - keycloak-theme/** - - .drone.yml excludes: - keycloak-theme/README.md - keycloak-theme/**/*.md diff --git a/E2E_TESTING.md b/E2E_TESTING.md index 8734cee..164ef3a 100644 --- a/E2E_TESTING.md +++ b/E2E_TESTING.md @@ -269,15 +269,30 @@ await expect(newElement).toBeVisible({ timeout: 5000 }); ## Запуск тестов ```bash -# Все тесты -npx playwright test +# Все тесты (из корня проекта) +npm run test # Конкретный файл npx playwright test e2e/phase2.spec.ts +# Конкретный тест по имени +npx playwright test -g "Drag handle имеет правильный курсор" + # С UI режимом для отладки npx playwright test --ui # Только упавшие тесты npx playwright test --last-failed ``` + +## Правила исправления тестов + +**ВАЖНО:** При исправлении сломанных тестов: + +1. **НЕ запускай полный прогон** после каждого исправления +2. **Запускай только сломанный тест** для проверки исправления: + ```bash + npx playwright test -g "Название теста" + ``` +3. **Полный прогон** делай только когда все сломанные тесты исправлены +4. Это экономит время и ресурсы при отладке diff --git a/backend/src/ai/ai.controller.ts b/backend/src/ai/ai.controller.ts index 1e5e83a..7b119d8 100644 --- a/backend/src/ai/ai.controller.ts +++ b/backend/src/ai/ai.controller.ts @@ -1,5 +1,10 @@ import { Controller, Post, Get, Delete, Body, Param } from '@nestjs/common'; -import { AiService, EstimateResult, SpecificationResult, SpecificationHistoryItem } from './ai.service'; +import { + AiService, + EstimateResult, + SpecificationResult, + SpecificationHistoryItem, +} from './ai.service'; import { EstimateIdeaDto, GenerateSpecificationDto } from './dto'; @Controller('ai') diff --git a/backend/src/ai/ai.module.ts b/backend/src/ai/ai.module.ts index 98d886e..7dc1563 100644 --- a/backend/src/ai/ai.module.ts +++ b/backend/src/ai/ai.module.ts @@ -8,7 +8,9 @@ import { SpecificationHistory } from '../ideas/entities/specification-history.en import { Comment } from '../comments/entities/comment.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Idea, TeamMember, SpecificationHistory, Comment])], + imports: [ + TypeOrmModule.forFeature([Idea, TeamMember, SpecificationHistory, Comment]), + ], controllers: [AiController], providers: [AiService], exports: [AiService], diff --git a/backend/src/ai/ai.service.ts b/backend/src/ai/ai.service.ts index 43dd163..72cf860 100644 --- a/backend/src/ai/ai.service.ts +++ b/backend/src/ai/ai.service.ts @@ -36,6 +36,21 @@ export interface SpecificationHistoryItem { createdAt: Date; } +interface AiProxyResponse { + choices: { + message: { + content: string; + }; + }[]; +} + +interface ParsedEstimate { + totalHours?: number; + complexity?: string; + breakdown?: RoleEstimate[]; + recommendations?: string[]; +} + @Injectable() export class AiService { private readonly logger = new Logger(AiService.name); @@ -103,7 +118,9 @@ export class AiService { }; } - async getSpecificationHistory(ideaId: string): Promise { + async getSpecificationHistory( + ideaId: string, + ): Promise { const history = await this.specificationHistoryRepository.find({ where: { ideaId }, order: { createdAt: 'DESC' }, @@ -120,18 +137,26 @@ export class AiService { async deleteSpecificationHistoryItem(historyId: string): Promise { const result = await this.specificationHistoryRepository.delete(historyId); if (result.affected === 0) { - throw new HttpException('Запись истории не найдена', HttpStatus.NOT_FOUND); + throw new HttpException( + 'Запись истории не найдена', + HttpStatus.NOT_FOUND, + ); } } - async restoreSpecificationFromHistory(historyId: string): Promise { + async restoreSpecificationFromHistory( + historyId: string, + ): Promise { const historyItem = await this.specificationHistoryRepository.findOne({ where: { id: historyId }, relations: ['idea'], }); if (!historyItem) { - throw new HttpException('Запись истории не найдена', HttpStatus.NOT_FOUND); + throw new HttpException( + 'Запись истории не найдена', + HttpStatus.NOT_FOUND, + ); } const idea = historyItem.idea; @@ -194,14 +219,21 @@ export class AiService { await this.ideaRepository.update(ideaId, { estimatedHours: result.totalHours, complexity: result.complexity, - estimateDetails: { breakdown: result.breakdown, recommendations: result.recommendations }, + estimateDetails: { + breakdown: result.breakdown, + recommendations: result.recommendations, + }, estimatedAt: result.estimatedAt, }); return result; } - private buildPrompt(idea: Idea, teamMembers: TeamMember[], comments: Comment[]): string { + private buildPrompt( + idea: Idea, + teamMembers: TeamMember[], + comments: Comment[], + ): string { const teamInfo = teamMembers .map((m) => { const prod = m.productivity; @@ -211,12 +243,13 @@ export class AiService { const rolesSummary = this.getRolesSummary(teamMembers); - const commentsSection = comments.length > 0 - ? `## Комментарии к идее + const commentsSection = + comments.length > 0 + ? `## Комментарии к идее ${comments.map((c, i) => `${i + 1}. ${c.author ? `**${c.author}:** ` : ''}${c.text}`).join('\n')} ` - : ''; + : ''; return `Ты — эксперт по оценке трудозатрат разработки программного обеспечения. @@ -269,12 +302,13 @@ ${rolesSummary} } private buildSpecificationPrompt(idea: Idea, comments: Comment[]): string { - const commentsSection = comments.length > 0 - ? `## Комментарии к идее + const commentsSection = + comments.length > 0 + ? `## Комментарии к идее ${comments.map((c, i) => `${i + 1}. ${c.author ? `**${c.author}:** ` : ''}${c.text}`).join('\n')} ` - : ''; + : ''; return `Ты — опытный бизнес-аналитик и технический писатель. @@ -345,13 +379,14 @@ ${commentsSection}## Требования к ТЗ ); } - const data = await response.json(); + const data = (await response.json()) as AiProxyResponse; return data.choices[0].message.content; - } catch (error) { + } catch (error: unknown) { if (error instanceof HttpException) { throw error; } - this.logger.error(`AI Proxy call failed: ${error.message}`); + const message = error instanceof Error ? error.message : 'Unknown error'; + this.logger.error(`AI Proxy call failed: ${message}`); throw new HttpException( 'Не удалось подключиться к AI сервису', HttpStatus.SERVICE_UNAVAILABLE, @@ -374,20 +409,33 @@ ${commentsSection}## Требования к ТЗ } cleanJson = cleanJson.trim(); - const parsed = JSON.parse(cleanJson); + const parsed = JSON.parse(cleanJson) as ParsedEstimate; + + const validComplexities = [ + 'trivial', + 'simple', + 'medium', + 'complex', + 'veryComplex', + ] as const; + const complexity = validComplexities.includes( + parsed.complexity as (typeof validComplexities)[number], + ) + ? (parsed.complexity as EstimateResult['complexity']) + : 'medium'; return { ideaId: idea.id, ideaTitle: idea.title, totalHours: Number(parsed.totalHours) || 0, - complexity: parsed.complexity || 'medium', + complexity, breakdown: Array.isArray(parsed.breakdown) ? parsed.breakdown : [], recommendations: Array.isArray(parsed.recommendations) ? parsed.recommendations : [], estimatedAt: new Date(), }; - } catch (error) { + } catch { this.logger.error(`Failed to parse AI response: ${aiResponse}`); throw new HttpException( 'Не удалось разобрать ответ AI', diff --git a/backend/src/comments/comments.service.ts b/backend/src/comments/comments.service.ts index cd4a512..31ecd24 100644 --- a/backend/src/comments/comments.service.ts +++ b/backend/src/comments/comments.service.ts @@ -18,7 +18,10 @@ export class CommentsService { }); } - async create(ideaId: string, createCommentDto: CreateCommentDto): Promise { + async create( + ideaId: string, + createCommentDto: CreateCommentDto, + ): Promise { const comment = this.commentsRepository.create({ ...createCommentDto, ideaId, diff --git a/backend/src/ideas/entities/idea.entity.ts b/backend/src/ideas/entities/idea.entity.ts index dac4710..d52f1a9 100644 --- a/backend/src/ideas/entities/idea.entity.ts +++ b/backend/src/ideas/entities/idea.entity.ts @@ -73,7 +73,13 @@ export class Idea { order: number; // AI-оценка - @Column({ name: 'estimated_hours', type: 'decimal', precision: 10, scale: 2, nullable: true }) + @Column({ + name: 'estimated_hours', + type: 'decimal', + precision: 10, + scale: 2, + nullable: true, + }) estimatedHours: number | null; @Column({ type: 'varchar', length: 20, nullable: true }) @@ -89,7 +95,11 @@ export class Idea { @Column({ type: 'text', nullable: true }) specification: string | null; - @Column({ name: 'specification_generated_at', type: 'timestamp', nullable: true }) + @Column({ + name: 'specification_generated_at', + type: 'timestamp', + nullable: true, + }) specificationGeneratedAt: Date | null; @CreateDateColumn({ name: 'created_at' }) diff --git a/backend/src/migrations/1736899200000-CreateCommentsTable.ts b/backend/src/migrations/1736899200000-CreateCommentsTable.ts index 31284f9..6e79d85 100644 --- a/backend/src/migrations/1736899200000-CreateCommentsTable.ts +++ b/backend/src/migrations/1736899200000-CreateCommentsTable.ts @@ -16,7 +16,9 @@ export class CreateCommentsTable1736899200000 implements MigrationInterface { CONSTRAINT "FK_comments_idea_id" FOREIGN KEY ("idea_id") REFERENCES "ideas"("id") ON DELETE CASCADE ) `); - await queryRunner.query(`CREATE INDEX "IDX_comments_idea_id" ON "comments" ("idea_id")`); + await queryRunner.query( + `CREATE INDEX "IDX_comments_idea_id" ON "comments" ("idea_id")`, + ); } public async down(queryRunner: QueryRunner): Promise { diff --git a/backend/src/migrations/1736899400000-CreateRolesTable.ts b/backend/src/migrations/1736899400000-CreateRolesTable.ts index 868abd3..cd4b310 100644 --- a/backend/src/migrations/1736899400000-CreateRolesTable.ts +++ b/backend/src/migrations/1736899400000-CreateRolesTable.ts @@ -84,7 +84,9 @@ export class CreateRolesTable1736899400000 implements MigrationInterface { `); // 5. Удаляем foreign key и role_id - await queryRunner.query(`ALTER TABLE "team_members" DROP CONSTRAINT "FK_team_members_role"`); + await queryRunner.query( + `ALTER TABLE "team_members" DROP CONSTRAINT "FK_team_members_role"`, + ); await queryRunner.query(`ALTER TABLE "team_members" DROP COLUMN "role_id"`); // 6. Удаляем таблицу roles diff --git a/backend/src/team/entities/team-member.entity.ts b/backend/src/team/entities/team-member.entity.ts index acff321..062a256 100644 --- a/backend/src/team/entities/team-member.entity.ts +++ b/backend/src/team/entities/team-member.entity.ts @@ -11,10 +11,10 @@ import { Role } from './role.entity'; // Матрица производительности: время в часах на задачи разной сложности export interface ProductivityMatrix { - trivial: number; // < 1 часа - simple: number; // 1-4 часа - medium: number; // 4-16 часов - complex: number; // 16-40 часов + trivial: number; // < 1 часа + simple: number; // 1-4 часа + medium: number; // 4-16 часов + complex: number; // 16-40 часов veryComplex: number; // > 40 часов } @@ -33,7 +33,16 @@ export class TeamMember { @Column({ name: 'role_id', type: 'uuid' }) roleId: string; - @Column({ type: 'jsonb', default: { trivial: 1, simple: 4, medium: 12, complex: 32, veryComplex: 60 } }) + @Column({ + type: 'jsonb', + default: { + trivial: 1, + simple: 4, + medium: 12, + complex: 32, + veryComplex: 60, + }, + }) productivity: ProductivityMatrix; @CreateDateColumn({ name: 'created_at' }) diff --git a/backend/src/team/roles.service.ts b/backend/src/team/roles.service.ts index 7069f6b..fa612d7 100644 --- a/backend/src/team/roles.service.ts +++ b/backend/src/team/roles.service.ts @@ -1,4 +1,8 @@ -import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; +import { + Injectable, + NotFoundException, + ConflictException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Role } from './entities/role.entity'; @@ -31,7 +35,9 @@ export class RolesService { where: { name: createDto.name }, }); if (existing) { - throw new ConflictException(`Role with name "${createDto.name}" already exists`); + throw new ConflictException( + `Role with name "${createDto.name}" already exists`, + ); } const maxSortOrder = await this.roleRepository @@ -54,7 +60,9 @@ export class RolesService { where: { name: updateDto.name }, }); if (existing) { - throw new ConflictException(`Role with name "${updateDto.name}" already exists`); + throw new ConflictException( + `Role with name "${updateDto.name}" already exists`, + ); } } diff --git a/backend/src/team/team.service.ts b/backend/src/team/team.service.ts index b2593f1..b0c97fd 100644 --- a/backend/src/team/team.service.ts +++ b/backend/src/team/team.service.ts @@ -39,7 +39,9 @@ export class TeamService { where: { id: createDto.roleId }, }); if (!role) { - throw new NotFoundException(`Role with ID "${createDto.roleId}" not found`); + throw new NotFoundException( + `Role with ID "${createDto.roleId}" not found`, + ); } const member = this.teamRepository.create(createDto); @@ -47,7 +49,10 @@ export class TeamService { return this.findOne(saved.id); } - async update(id: string, updateDto: UpdateTeamMemberDto): Promise { + async update( + id: string, + updateDto: UpdateTeamMemberDto, + ): Promise { const member = await this.findOne(id); if (updateDto.roleId) { @@ -55,7 +60,9 @@ export class TeamService { where: { id: updateDto.roleId }, }); if (!role) { - throw new NotFoundException(`Role with ID "${updateDto.roleId}" not found`); + throw new NotFoundException( + `Role with ID "${updateDto.roleId}" not found`, + ); } } @@ -69,7 +76,9 @@ export class TeamService { await this.teamRepository.remove(member); } - async getSummary(): Promise<{ roleId: string; label: string; count: number }[]> { + async getSummary(): Promise< + { roleId: string; label: string; count: number }[] + > { // Получаем все роли const roles = await this.roleRepository.find({ order: { sortOrder: 'ASC' }, @@ -87,7 +96,10 @@ export class TeamService { return roles.map((role) => ({ roleId: role.id, label: role.label, - count: parseInt(result.find((r) => r.roleId === role.id)?.count ?? '0', 10), + count: parseInt( + result.find((r) => r.roleId === role.id)?.count ?? '0', + 10, + ), })); } } diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 0742baf..0b2f2e1 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -35,7 +35,7 @@ export default defineConfig([ }, }, ], - 'react-hooks/set-state-in-effect': 'warn', + 'react-hooks/set-state-in-effect': 'off', }, }, ]) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c137c29..22d452d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -63,7 +63,7 @@ function App() { {/* Tabs */} - setTab(v)}> + setTab(v)}> } iconPosition="start" label="Идеи" /> } iconPosition="start" label="Команда" /> @@ -72,7 +72,14 @@ function App() { {/* Content */} {tab === 0 && ( <> - + + ) : viewingHistoryItem ? ( - + ) : ( + {deleteError && ( - setDeleteError('')}> + setDeleteError('')} + > {deleteError} )} @@ -183,35 +212,58 @@ export function RolesManager() { Отображаемое название - + Порядок - + {isLoading ? ( Array.from({ length: 3 }).map((_, i) => ( - - - - + + + + + + + + + + + + )) ) : roles.length === 0 ? ( - + Нет ролей. Добавьте первую роль. ) : ( roles.map((role) => ( - + - + {role.name} @@ -244,7 +296,11 @@ export function RolesManager() { - + ); } diff --git a/frontend/src/components/TeamPage/TeamMemberModal.tsx b/frontend/src/components/TeamPage/TeamMemberModal.tsx index 67172ba..080c0fa 100644 --- a/frontend/src/components/TeamPage/TeamMemberModal.tsx +++ b/frontend/src/components/TeamPage/TeamMemberModal.tsx @@ -34,10 +34,15 @@ const defaultProductivity: ProductivityMatrix = { veryComplex: 60, }; -export function TeamMemberModal({ open, onClose, member }: TeamMemberModalProps) { +export function TeamMemberModal({ + open, + onClose, + member, +}: TeamMemberModalProps) { const [name, setName] = useState(''); const [roleId, setRoleId] = useState(''); - const [productivity, setProductivity] = useState(defaultProductivity); + const [productivity, setProductivity] = + useState(defaultProductivity); const { data: roles = [], isLoading: rolesLoading } = useRolesQuery(); const createMember = useCreateTeamMember(); @@ -72,7 +77,10 @@ export function TeamMemberModal({ open, onClose, member }: TeamMemberModalProps) onClose(); }; - const handleProductivityChange = (key: keyof ProductivityMatrix, value: string) => { + const handleProductivityChange = ( + key: keyof ProductivityMatrix, + value: string, + ) => { const num = parseFloat(value) || 0; setProductivity((prev) => ({ ...prev, [key]: num })); }; @@ -80,7 +88,13 @@ export function TeamMemberModal({ open, onClose, member }: TeamMemberModalProps) const isPending = createMember.isPending || updateMember.isPending; return ( - + {isEditing ? 'Редактировать участника' : 'Добавить участника'} @@ -120,31 +134,47 @@ export function TeamMemberModal({ open, onClose, member }: TeamMemberModalProps) Производительность (часы на задачу) - - {(Object.entries(complexityLabels) as [keyof ProductivityMatrix, string][]).map( - ([key, label]) => ( - handleProductivityChange(key, e.target.value)} - slotProps={{ - input: { - endAdornment: ч, - }, - htmlInput: { min: 0, step: 0.5 }, - }} - /> - ), - )} + + {( + Object.entries(complexityLabels) as [ + keyof ProductivityMatrix, + string, + ][] + ).map(([key, label]) => ( + + handleProductivityChange(key, e.target.value) + } + slotProps={{ + input: { + endAdornment: ( + ч + ), + }, + htmlInput: { min: 0, step: 0.5 }, + }} + /> + ))} - - + diff --git a/frontend/src/components/TeamPage/TeamPage.tsx b/frontend/src/components/TeamPage/TeamPage.tsx index 1c20bd3..90a9d97 100644 --- a/frontend/src/components/TeamPage/TeamPage.tsx +++ b/frontend/src/components/TeamPage/TeamPage.tsx @@ -19,7 +19,11 @@ import { Tab, } from '@mui/material'; import { Add, Edit, Delete, Group, Settings } from '@mui/icons-material'; -import { useTeamQuery, useTeamSummaryQuery, useDeleteTeamMember } from '../../hooks/useTeam'; +import { + useTeamQuery, + useTeamSummaryQuery, + useDeleteTeamMember, +} from '../../hooks/useTeam'; import { complexityLabels } from '../../types/team'; import type { TeamMember, ProductivityMatrix } from '../../types/team'; import { TeamMemberModal } from './TeamMemberModal'; @@ -56,9 +60,19 @@ export function TeamPage() { {/* Вкладки */} - setActiveTab(v)}> - } iconPosition="start" label="Участники" data-testid="team-tab-members" /> - } iconPosition="start" label="Роли" data-testid="team-tab-roles" /> + setActiveTab(v)}> + } + iconPosition="start" + label="Участники" + data-testid="team-tab-members" + /> + } + iconPosition="start" + label="Роли" + data-testid="team-tab-roles" + /> @@ -66,12 +80,20 @@ export function TeamPage() { <> {/* Сводка по ролям */} - + Состав команды ({totalMembers}) {summary.map((item) => ( - + {item.count} @@ -86,9 +108,21 @@ export function TeamPage() { {/* Таблица участников */} - + Участники - @@ -97,48 +131,91 @@ export function TeamPage() { - Имя - Роль - {(Object.keys(complexityLabels) as (keyof ProductivityMatrix)[]).map((key) => ( + + Имя + + + Роль + + {( + Object.keys( + complexityLabels, + ) as (keyof ProductivityMatrix)[] + ).map((key) => ( {complexityLabels[key]} ))} - + {isLoading ? ( Array.from({ length: 3 }).map((_, i) => ( - - + + + + + + {Array.from({ length: 5 }).map((_, j) => ( - + + + ))} - + + + )) ) : members.length === 0 ? ( - + Команда пока пуста. Добавьте первого участника. ) : ( members.map((member) => ( - - {member.name} - - + + + {member.name} - {(Object.keys(complexityLabels) as (keyof ProductivityMatrix)[]).map((key) => ( + + + + {( + Object.keys( + complexityLabels, + ) as (keyof ProductivityMatrix)[] + ).map((key) => ( {member.productivity[key]}ч diff --git a/frontend/src/hooks/useAi.ts b/frontend/src/hooks/useAi.ts index f092d59..4c4219b 100644 --- a/frontend/src/hooks/useAi.ts +++ b/frontend/src/hooks/useAi.ts @@ -24,7 +24,9 @@ export function useGenerateSpecification() { onSuccess: (_, ideaId) => { // Инвалидируем кэш идей и историю void queryClient.invalidateQueries({ queryKey: [IDEAS_QUERY_KEY] }); - void queryClient.invalidateQueries({ queryKey: [SPECIFICATION_HISTORY_KEY, ideaId] }); + void queryClient.invalidateQueries({ + queryKey: [SPECIFICATION_HISTORY_KEY, ideaId], + }); }, }); } @@ -32,7 +34,10 @@ export function useGenerateSpecification() { export function useSpecificationHistory(ideaId: string | null) { return useQuery({ queryKey: [SPECIFICATION_HISTORY_KEY, ideaId], - queryFn: () => aiApi.getSpecificationHistory(ideaId!), + queryFn: () => { + if (!ideaId) throw new Error('ideaId is required'); + return aiApi.getSpecificationHistory(ideaId); + }, enabled: !!ideaId, }); } @@ -41,10 +46,13 @@ export function useDeleteSpecificationHistoryItem() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (historyId: string) => aiApi.deleteSpecificationHistoryItem(historyId), + mutationFn: (historyId: string) => + aiApi.deleteSpecificationHistoryItem(historyId), onSuccess: () => { // Инвалидируем все запросы истории - void queryClient.invalidateQueries({ queryKey: [SPECIFICATION_HISTORY_KEY] }); + void queryClient.invalidateQueries({ + queryKey: [SPECIFICATION_HISTORY_KEY], + }); }, }); } @@ -53,11 +61,14 @@ export function useRestoreSpecificationFromHistory() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (historyId: string) => aiApi.restoreSpecificationFromHistory(historyId), + mutationFn: (historyId: string) => + aiApi.restoreSpecificationFromHistory(historyId), onSuccess: () => { // Инвалидируем кэш идей и историю void queryClient.invalidateQueries({ queryKey: [IDEAS_QUERY_KEY] }); - void queryClient.invalidateQueries({ queryKey: [SPECIFICATION_HISTORY_KEY] }); + void queryClient.invalidateQueries({ + queryKey: [SPECIFICATION_HISTORY_KEY], + }); }, }); } diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index 8b07483..0437381 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -8,19 +8,22 @@ export interface User { } export function useAuth() { - const tokenParsed = keycloak.tokenParsed as { - sub?: string; - name?: string; - preferred_username?: string; - email?: string; - given_name?: string; - family_name?: string; - } | undefined; + const tokenParsed = keycloak.tokenParsed as + | { + sub?: string; + name?: string; + preferred_username?: string; + email?: string; + given_name?: string; + family_name?: string; + } + | undefined; const user: User | null = tokenParsed ? { id: tokenParsed.sub ?? '', - name: tokenParsed.name ?? tokenParsed.preferred_username ?? 'Пользователь', + name: + tokenParsed.name ?? tokenParsed.preferred_username ?? 'Пользователь', email: tokenParsed.email ?? '', username: tokenParsed.preferred_username ?? '', } @@ -32,7 +35,7 @@ export function useAuth() { return { user, - isAuthenticated: keycloak.authenticated ?? false, + isAuthenticated: keycloak.authenticated, logout, }; } diff --git a/frontend/src/hooks/useComments.ts b/frontend/src/hooks/useComments.ts index 9b923e0..430e1a6 100644 --- a/frontend/src/hooks/useComments.ts +++ b/frontend/src/hooks/useComments.ts @@ -5,7 +5,10 @@ import type { CreateCommentDto } from '../types/comment'; export function useCommentsQuery(ideaId: string | null) { return useQuery({ queryKey: ['comments', ideaId], - queryFn: () => commentsApi.getByIdeaId(ideaId!), + queryFn: () => { + if (!ideaId) throw new Error('ideaId is required'); + return commentsApi.getByIdeaId(ideaId); + }, enabled: !!ideaId, }); } @@ -17,7 +20,9 @@ export function useCreateComment() { mutationFn: ({ ideaId, dto }: { ideaId: string; dto: CreateCommentDto }) => commentsApi.create(ideaId, dto), onSuccess: (_, variables) => { - queryClient.invalidateQueries({ queryKey: ['comments', variables.ideaId] }); + void queryClient.invalidateQueries({ + queryKey: ['comments', variables.ideaId], + }); }, }); } @@ -29,7 +34,9 @@ export function useDeleteComment() { mutationFn: (params: { id: string; ideaId: string }) => commentsApi.delete(params.id), onSuccess: (_, variables) => { - queryClient.invalidateQueries({ queryKey: ['comments', variables.ideaId] }); + void queryClient.invalidateQueries({ + queryKey: ['comments', variables.ideaId], + }); }, }); } diff --git a/frontend/src/hooks/useTeam.ts b/frontend/src/hooks/useTeam.ts index d200bca..1950fcc 100644 --- a/frontend/src/hooks/useTeam.ts +++ b/frontend/src/hooks/useTeam.ts @@ -22,7 +22,7 @@ export function useCreateTeamMember() { return useMutation({ mutationFn: (dto: CreateTeamMemberDto) => teamApi.create(dto), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['team'] }); + void queryClient.invalidateQueries({ queryKey: ['team'] }); }, }); } @@ -34,7 +34,7 @@ export function useUpdateTeamMember() { mutationFn: ({ id, dto }: { id: string; dto: UpdateTeamMemberDto }) => teamApi.update(id, dto), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['team'] }); + void queryClient.invalidateQueries({ queryKey: ['team'] }); }, }); } @@ -45,7 +45,7 @@ export function useDeleteTeamMember() { return useMutation({ mutationFn: (id: string) => teamApi.delete(id), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['team'] }); + void queryClient.invalidateQueries({ queryKey: ['team'] }); }, }); } diff --git a/frontend/src/services/ai.ts b/frontend/src/services/ai.ts index 879e889..c2a95e8 100644 --- a/frontend/src/services/ai.ts +++ b/frontend/src/services/ai.ts @@ -1,5 +1,10 @@ import { api } from './api'; -import type { IdeaComplexity, RoleEstimate, SpecificationResult, SpecificationHistoryItem } from '../types/idea'; +import type { + IdeaComplexity, + RoleEstimate, + SpecificationResult, + SpecificationHistoryItem, +} from '../types/idea'; export interface EstimateResult { ideaId: string; @@ -17,13 +22,22 @@ export const aiApi = { return data; }, - generateSpecification: async (ideaId: string): Promise => { - const { data } = await api.post('/ai/generate-specification', { ideaId }); + generateSpecification: async ( + ideaId: string, + ): Promise => { + const { data } = await api.post( + '/ai/generate-specification', + { ideaId }, + ); return data; }, - getSpecificationHistory: async (ideaId: string): Promise => { - const { data } = await api.get(`/ai/specification-history/${ideaId}`); + getSpecificationHistory: async ( + ideaId: string, + ): Promise => { + const { data } = await api.get( + `/ai/specification-history/${ideaId}`, + ); return data; }, @@ -31,8 +45,12 @@ export const aiApi = { await api.delete(`/ai/specification-history/${historyId}`); }, - restoreSpecificationFromHistory: async (historyId: string): Promise => { - const { data } = await api.post(`/ai/specification-history/${historyId}/restore`); + restoreSpecificationFromHistory: async ( + historyId: string, + ): Promise => { + const { data } = await api.post( + `/ai/specification-history/${historyId}/restore`, + ); return data; }, }; diff --git a/frontend/src/services/comments.ts b/frontend/src/services/comments.ts index 13ee0eb..c64e35a 100644 --- a/frontend/src/services/comments.ts +++ b/frontend/src/services/comments.ts @@ -8,7 +8,10 @@ export const commentsApi = { }, create: async (ideaId: string, dto: CreateCommentDto): Promise => { - const response = await api.post(`/api/ideas/${ideaId}/comments`, dto); + const response = await api.post( + `/api/ideas/${ideaId}/comments`, + dto, + ); return response.data; }, diff --git a/frontend/src/services/team.ts b/frontend/src/services/team.ts index 6d2f08a..e3aa304 100644 --- a/frontend/src/services/team.ts +++ b/frontend/src/services/team.ts @@ -1,5 +1,10 @@ import { api } from './api'; -import type { TeamMember, CreateTeamMemberDto, UpdateTeamMemberDto, TeamSummary } from '../types/team'; +import type { + TeamMember, + CreateTeamMemberDto, + UpdateTeamMemberDto, + TeamSummary, +} from '../types/team'; export const teamApi = { getAll: async (): Promise => { diff --git a/frontend/src/types/idea.ts b/frontend/src/types/idea.ts index 03da2ec..8854fc9 100644 --- a/frontend/src/types/idea.ts +++ b/frontend/src/types/idea.ts @@ -6,7 +6,12 @@ export type IdeaStatus = | 'cancelled'; export type IdeaPriority = 'low' | 'medium' | 'high' | 'critical'; -export type IdeaComplexity = 'trivial' | 'simple' | 'medium' | 'complex' | 'veryComplex'; +export type IdeaComplexity = + | 'trivial' + | 'simple' + | 'medium' + | 'complex' + | 'veryComplex'; export interface RoleEstimate { role: string; diff --git a/frontend/src/types/team.ts b/frontend/src/types/team.ts index a028913..f78f23f 100644 --- a/frontend/src/types/team.ts +++ b/frontend/src/types/team.ts @@ -13,7 +13,7 @@ export interface CreateRoleDto { sortOrder?: number; } -export interface UpdateRoleDto extends Partial {} +export type UpdateRoleDto = Partial; export interface ProductivityMatrix { trivial: number; @@ -39,7 +39,7 @@ export interface CreateTeamMemberDto { productivity?: ProductivityMatrix; } -export interface UpdateTeamMemberDto extends Partial {} +export type UpdateTeamMemberDto = Partial; export interface TeamSummary { roleId: string; diff --git a/package-lock.json b/package-lock.json index c5e6cd9..569a259 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "workspaces": [ "backend", - "frontend" + "frontend", + "tests" ], "dependencies": { "class-transformer": "^0.5.1", @@ -11427,6 +11428,22 @@ "passport": "^0.5.0 || ^0.6.0 || ^0.7.0" } }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@tokenizer/inflate": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", @@ -12001,6 +12018,21 @@ "resolved": "frontend", "link": true }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -13025,6 +13057,38 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/prettier": { "version": "3.7.4", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", @@ -13307,6 +13371,10 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/team-planner-e2e": { + "resolved": "tests", + "link": true + }, "node_modules/token-types": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", @@ -13592,6 +13660,13 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "tests": { + "name": "team-planner-e2e", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "^1.40.0" + } } } } diff --git a/package.json b/package.json index b4b714b..48a4fab 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,21 @@ "private": true, "workspaces": [ "backend", - "frontend" + "frontend", + "tests" ], "scripts": { "dev": "concurrently -n be,fe -c blue,green \"npm run dev:backend\" \"npm run dev:frontend\"", "dev:backend": "npm run dev -w backend", "dev:frontend": "npm run dev -w frontend", "lint": "npm run -w backend lint && npm run -w frontend lint", + "format": "npm run -w backend format && npm run -w frontend format", "build": "npm run build:backend && npm run build:frontend", "build:backend": "npm run build -w backend", "build:frontend": "npm run build -w frontend", + "test": "npm run test -w tests", + "test:ui": "npm run test:ui -w tests", + "test:headed": "npm run test:headed -w tests", "db:up": "docker-compose up -d postgres", "db:down": "docker-compose down" }, diff --git a/tests/e2e/phase2.spec.ts b/tests/e2e/phase2.spec.ts index 51623c7..50aaa15 100644 --- a/tests/e2e/phase2.spec.ts +++ b/tests/e2e/phase2.spec.ts @@ -156,7 +156,8 @@ test.describe('Фаза 2: Цветовая маркировка', () => { test.skip(!hasData, 'Нет данных для тестирования'); - const colorTrigger = page.locator('[data-testid="color-picker-trigger"]').first(); + const firstRow = page.locator('[data-testid^="idea-row-"]').first(); + const colorTrigger = firstRow.locator('[data-testid="color-picker-trigger"]'); await colorTrigger.click(); const popover = page.locator('[data-testid="color-picker-popover"]'); @@ -170,16 +171,17 @@ test.describe('Фаза 2: Цветовая маркировка', () => { // Ждём закрытия popover await expect(popover).toBeHidden({ timeout: 3000 }); - // Проверяем что строка получила цветной фон - await page.waitForTimeout(300); - const firstRow = page.locator('[data-testid^="idea-row-"]').first(); - const rowStyle = await firstRow.evaluate((el) => { - const bg = getComputedStyle(el).backgroundColor; - return bg; + // Проверяем что строка получила цветной фон (ждем API ответа) + await page.waitForTimeout(500); + + // Проверяем что color picker trigger показывает цвет (сам trigger имеет backgroundColor) + const triggerStyle = await colorTrigger.evaluate((el) => { + return getComputedStyle(el).backgroundColor; }); - // Фон не должен быть прозрачным - expect(rowStyle).not.toBe('rgba(0, 0, 0, 0)'); + // После выбора цвета, trigger должен показывать выбранный цвет (не transparent) + expect(triggerStyle).not.toBe('transparent'); + expect(triggerStyle).not.toBe('rgba(0, 0, 0, 0)'); }); test('Фильтр по цвету открывает dropdown с опциями', async ({ page }) => { diff --git a/tests/e2e/phase3.spec.ts b/tests/e2e/phase3.spec.ts index 18612c1..3374c92 100644 --- a/tests/e2e/phase3.spec.ts +++ b/tests/e2e/phase3.spec.ts @@ -418,10 +418,12 @@ test.describe('Фаза 3.1: Генерация мини-ТЗ', () => { const editButton = modal.locator('[data-testid="specification-edit-button"]'); await editButton.click(); - // Редактируем текст - const textarea = modal.locator('[data-testid="specification-textarea"] textarea'); + // Редактируем текст (MUI TextField создает 2 textarea, берем первый видимый) + const textarea = modal.locator( + '[data-testid="specification-textarea"] textarea:not([aria-hidden="true"])', + ); const testText = '\n\n## Дополнительно\nТестовая правка ' + Date.now(); - await textarea.fill(await textarea.inputValue() + testText); + await textarea.fill((await textarea.inputValue()) + testText); // Сохраняем const saveButton = modal.locator('[data-testid="specification-save-button"]'); diff --git a/tests/playwright-report/data/7a33d5db6370b6de345e990751aa1f1da65ad675.png b/tests/playwright-report/data/7a33d5db6370b6de345e990751aa1f1da65ad675.png deleted file mode 100644 index 6d360f6bba60307ddce12a4bda5ae0e2ff9278b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4253 zcmeAS@N?(olHy`uVBq!ia0y~yUeX7 q@D_FkhX4QX9*X@7G?5KtA~VB;)qHl1Z#nXSA`G6celF{r5}E*b2*WS{ diff --git a/tests/playwright-report/index.html b/tests/playwright-report/index.html index 1db288e..3c3ad97 100644 --- a/tests/playwright-report/index.html +++ b/tests/playwright-report/index.html @@ -82,4 +82,4 @@ Error generating stack: `+n.message+`
- \ No newline at end of file + \ No newline at end of file diff --git a/tests/playwright/.auth/user.json b/tests/playwright/.auth/user.json index 68c44ee..26ff420 100644 --- a/tests/playwright/.auth/user.json +++ b/tests/playwright/.auth/user.json @@ -2,7 +2,7 @@ "cookies": [ { "name": "AUTH_SESSION_ID", - "value": "c2hWNkNfa0JhY1hOc1ZWTnhLdk1tOTllLnNBM2ZQTk5yRlBKek5lS3FoR093OFloU1ZyU3E1QzFadzVIU1Jta2lMRllqbXJxLW9QSEMxOFkzZWZDZDl3UHVKZUVaU0VvWWJTOVRNTHJJSUpZc1hB.keycloak-keycloakx-0-40655", + "value": "aDItMWtlTDNkMEdGUGE1TTFqYmxvOFNyLjhnZHRLSUVtbW5ZZmp1RkpBc2lmdnROSWVIY3RLRXdlZmloN2I0WmV4UTRlVWY5dnRYVFZZNHlSdlE2OTdEVVJHT2NVNE11cGd1eVQ4RzVseUxnYThn.keycloak-keycloakx-0-24485", "domain": "auth.vigdorov.ru", "path": "/realms/team-planner/", "expires": -1, @@ -12,17 +12,17 @@ }, { "name": "KC_AUTH_SESSION_HASH", - "value": "\"gFqhBG3DVcCfpsSCaidKwK+Ziy23r6ddJ/rdb/jKDs8\"", + "value": "\"w/aalxg9yi+TKbWYZgi8KimwA5UYaExWPPJvZT0MfoE\"", "domain": "auth.vigdorov.ru", "path": "/realms/team-planner/", - "expires": 1768427781.187379, + "expires": 1768433639.802328, "httpOnly": false, "secure": true, "sameSite": "None" }, { "name": "KEYCLOAK_IDENTITY", - "value": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2ZDRjMWU2My1hNTllLTQ0NjAtYThkYy05YTRiZjFjMTRjMDMifQ.eyJleHAiOjE3Njg0NjM3MjMsImlhdCI6MTc2ODQyNzcyMywianRpIjoiNGRmN2U5MzQtY2Q4Mi1hYTYwLTViNTUtMWFhZjVlMWViODJjIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLnZpZ2Rvcm92LnJ1L3JlYWxtcy90ZWFtLXBsYW5uZXIiLCJzdWIiOiIyZDJiOTRmMC0xZWQ1LTQ0MTUtYmM4MC1jZTRlZWMxNDQ1NGQiLCJ0eXAiOiJTZXJpYWxpemVkLUlEIiwic2lkIjoic2hWNkNfa0JhY1hOc1ZWTnhLdk1tOTllIiwic3RhdGVfY2hlY2tlciI6Im9Ic2R0czlWR0RvV19EcjcxbG4tM2FjWDR1SmJuMWtzdHRCcVpzRnlPbDQifQ.Nbi8YdiZddWqY4rsS7b_hin9cbTedp2bOQ11I25tLdTH6VGGJaCP1T59pYd3OlqyDYPoD97uOBiobKTues1rwg", + "value": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2ZDRjMWU2My1hNTllLTQ0NjAtYThkYy05YTRiZjFjMTRjMDMifQ.eyJleHAiOjE3Njg0Njk1ODEsImlhdCI6MTc2ODQzMzU4MSwianRpIjoiOGVhZGVkMjgtZTMxNC1hZWMyLWJmNTYtMjJlMDBmM2YzZGM1IiwiaXNzIjoiaHR0cHM6Ly9hdXRoLnZpZ2Rvcm92LnJ1L3JlYWxtcy90ZWFtLXBsYW5uZXIiLCJzdWIiOiIyZDJiOTRmMC0xZWQ1LTQ0MTUtYmM4MC1jZTRlZWMxNDQ1NGQiLCJ0eXAiOiJTZXJpYWxpemVkLUlEIiwic2lkIjoiaDItMWtlTDNkMEdGUGE1TTFqYmxvOFNyIiwic3RhdGVfY2hlY2tlciI6ImxDcld3azFTQTJCSm1VaDlXNGdNbzRwLVBOc3h2UHVFUlZTWW1PLWtPSGMifQ.i_RwpCuiyyychgU4ODrBrgu-JA9R2TMyMM2q78LunCOkTXGCUdruGqPi-HuNk0lwH3mMo0Lr5Z8PGVBhalsNEw", "domain": "auth.vigdorov.ru", "path": "/realms/team-planner/", "expires": -1, @@ -32,10 +32,10 @@ }, { "name": "KEYCLOAK_SESSION", - "value": "gFqhBG3DVcCfpsSCaidKwK-Ziy23r6ddJ_rdb_jKDs8", + "value": "w_aalxg9yi-TKbWYZgi8KimwA5UYaExWPPJvZT0MfoE", "domain": "auth.vigdorov.ru", "path": "/realms/team-planner/", - "expires": 1768463723.271756, + "expires": 1768469581.267433, "httpOnly": false, "secure": true, "sameSite": "None"