From 3c2bf2da3c5b9061a1cb2af76383015fbf632344 Mon Sep 17 00:00:00 2001 From: fan4cz Date: Fri, 17 Apr 2026 12:03:21 +0500 Subject: [PATCH 01/59] =?UTF-8?q?=D0=B2=D1=8B=D1=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nginx.conf | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/nginx.conf b/nginx.conf index 3155f2e..532c1c4 100644 --- a/nginx.conf +++ b/nginx.conf @@ -15,14 +15,41 @@ http { listen 80; server_name _; + location /api/ { + proxy_pass http://backend:8080/api/; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /swagger/ { + proxy_pass http://backend:8080/swagger/; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location / { root /var/www/html; index index.html; - try_files $uri $uri.html $uri/ =404; + + try_files $uri $uri.html $uri @backend; + } + + location @backend { + proxy_pass http://backend:8080; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } - # error_page 500 502 503 504 /50x.html; - # location = /50x.html { - # root html; - # } } } \ No newline at end of file From 4b63f08c9132547711d2dc6dda404848ac5b5e51 Mon Sep 17 00:00:00 2001 From: fan4cz Date: Fri, 17 Apr 2026 12:28:11 +0500 Subject: [PATCH 02/59] tag ids --- .../Features/Project/GetProjects/GetProjectsHandle.cs | 6 +++++- Client.Models/Models/DTO/ProjectCatalogFilter.cs | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Api/Application/Features/Project/GetProjects/GetProjectsHandle.cs b/Api/Application/Features/Project/GetProjects/GetProjectsHandle.cs index 9f854a4..14511d3 100644 --- a/Api/Application/Features/Project/GetProjects/GetProjectsHandle.cs +++ b/Api/Application/Features/Project/GetProjects/GetProjectsHandle.cs @@ -5,12 +5,16 @@ namespace Api.Application.Features.Project.GetProject; -public class GetProjectsHandle(IProjectRepository projectRepository) +public class GetProjectsHandle(IProjectRepository projectRepository, ITagRepository tagRepository) : IRequestHandler> { public async Task> Handle(GetProjectsQuery request, CancellationToken cancellationToken) { + var filter = request.filter; + if (filter.TagNames is not null) + filter.TagIds = await tagRepository.GetTagsIdsByNameAsync(filter.TagNames, cancellationToken); + return await projectRepository.SearchAsync(request.filter, cancellationToken) ?? throw new KeyNotFoundException(); } diff --git a/Client.Models/Models/DTO/ProjectCatalogFilter.cs b/Client.Models/Models/DTO/ProjectCatalogFilter.cs index c589f96..e837c4b 100644 --- a/Client.Models/Models/DTO/ProjectCatalogFilter.cs +++ b/Client.Models/Models/DTO/ProjectCatalogFilter.cs @@ -3,7 +3,8 @@ public sealed class ProjectCatalogFilter { public string? Search { get; init; } - public List? TagIds {get; init;} + public List? TagIds {get; set;} + public List? TagNames {get; init;} public int? TeamMemberCount { get; init; } public int? Year {get; init;} public int? Semester {get; init;} From 6d894de5db27e6874bd5bf5fe694d0d9feb14f93 Mon Sep 17 00:00:00 2001 From: Arina Kiskina Date: Fri, 17 Apr 2026 20:16:45 +0500 Subject: [PATCH 03/59] aboba --- Api/Program.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Api/Program.cs b/Api/Program.cs index 7bef1f1..2e538bf 100644 --- a/Api/Program.cs +++ b/Api/Program.cs @@ -1,5 +1,6 @@ using Api.Application.Common; using Infrastructure.Extensions; +using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); @@ -14,6 +15,11 @@ .AddDatabase() .AddInfrastructureServices(); +builder.Services.Configure(options => +{ + options.ForwardedHeaders = + ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; +}); var app = builder.Build(); if (app.Environment.IsDevelopment()) @@ -21,14 +27,12 @@ app.UseSwagger(); app.UseSwaggerUI(); } - +app.UseForwardedHeaders(); app.InitializeDatabase(); app.UseDefaultFiles(); app.UseStaticFiles(); app.UseOpenTelemetryPrometheusScrapingEndpoint(); -app.MapControllers(); -app.Run(); app.UseExceptionHandler(); From d40c3401ec2bdd59218d1f3662015d2b68c69916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9A=D0=BE=D1=82=D0=BE=D0=B2?= Date: Sun, 19 Apr 2026 17:18:37 +0500 Subject: [PATCH 04/59] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD=D0=BA=D1=83=20=D0=B2=20?= =?UTF-8?q?=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Infrastructure/Parsers/CsvProjectParser.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Infrastructure/Parsers/CsvProjectParser.cs b/Infrastructure/Parsers/CsvProjectParser.cs index eb32fba..07bd189 100644 --- a/Infrastructure/Parsers/CsvProjectParser.cs +++ b/Infrastructure/Parsers/CsvProjectParser.cs @@ -40,20 +40,21 @@ private async Task ParseProjectAsync(TextFieldParser parser, Ca var rawYear = fields[6]; var rawSemester = fields[7]; var shortDescription = fields[8]; - var rawDescriptionLink = fields[9]; - var rawCastDevLink = fields[10]; - var rawMvpLink = fields[11]; - var rawRoadMapLink = fields[12]; - var rawGitLink = fields[13]; - var rawNonGitLink = fields[14]; - var rawTagsJson = fields[15]; + var description = fields[9]; + var rawDescriptionLink = fields[10]; + var rawCastDevLink = fields[11]; + var rawMvpLink = fields[12]; + var rawRoadMapLink = fields[13]; + var rawGitLink = fields[14]; + var rawNonGitLink = fields[15]; + var rawTagsJson = fields[16]; return new FullProjectInfo { Name = name, Year = int.Parse(rawYear), Semester = int.Parse(rawSemester), - Description = shortDescription, + Description = description, ShortDescription = shortDescription, TeamMembers = ParseTeamMembers(rawParticipantFields), Files = ParseFiles(rawDescriptionLink, rawCastDevLink, rawMvpLink, rawRoadMapLink, rawGitLink, From 2bc01d9d32903099cfa3b4262f5d6ca3d88375fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9A=D0=BE=D1=82=D0=BE=D0=B2?= Date: Sun, 19 Apr 2026 17:27:41 +0500 Subject: [PATCH 05/59] =?UTF-8?q?=D0=BF=D0=BE=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Infrastructure/Parsers/CsvProjectParser.cs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Infrastructure/Parsers/CsvProjectParser.cs b/Infrastructure/Parsers/CsvProjectParser.cs index 07bd189..a39fa09 100644 --- a/Infrastructure/Parsers/CsvProjectParser.cs +++ b/Infrastructure/Parsers/CsvProjectParser.cs @@ -40,24 +40,23 @@ private async Task ParseProjectAsync(TextFieldParser parser, Ca var rawYear = fields[6]; var rawSemester = fields[7]; var shortDescription = fields[8]; - var description = fields[9]; - var rawDescriptionLink = fields[10]; - var rawCastDevLink = fields[11]; - var rawMvpLink = fields[12]; - var rawRoadMapLink = fields[13]; - var rawGitLink = fields[14]; - var rawNonGitLink = fields[15]; - var rawTagsJson = fields[16]; + var rawDescription = fields[9]; + var rawCustDevLink = fields[10]; + var rawMvpLink = fields[11]; + var rawRoadMapLink = fields[12]; + var rawGitLink = fields[13]; + var rawNonGitLink = fields[14]; + var rawTagsJson = fields[15]; return new FullProjectInfo { Name = name, Year = int.Parse(rawYear), Semester = int.Parse(rawSemester), - Description = description, + Description = rawDescription, ShortDescription = shortDescription, TeamMembers = ParseTeamMembers(rawParticipantFields), - Files = ParseFiles(rawDescriptionLink, rawCastDevLink, rawMvpLink, rawRoadMapLink, rawGitLink, + Files = ParseFiles(rawDescription, rawCustDevLink, rawMvpLink, rawRoadMapLink, rawGitLink, rawNonGitLink), Tags = await ParseTagsAsync(rawTagsJson, ct) ?? [] }; @@ -129,4 +128,4 @@ private static FileDto ParseFiles( ? uri : null; } -} \ No newline at end of file +} From d2b3af72ce8cd621147761a357a10c3a44d26662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9A=D0=BE=D1=82=D0=BE=D0=B2?= Date: Sun, 19 Apr 2026 17:36:37 +0500 Subject: [PATCH 06/59] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Infrastructure/Parsers/CsvProjectParser.cs | 19 +++++++++-------- Test/CsvProjectParserTests.cs | 24 ++++++++++++---------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Infrastructure/Parsers/CsvProjectParser.cs b/Infrastructure/Parsers/CsvProjectParser.cs index a39fa09..17138cb 100644 --- a/Infrastructure/Parsers/CsvProjectParser.cs +++ b/Infrastructure/Parsers/CsvProjectParser.cs @@ -40,23 +40,24 @@ private async Task ParseProjectAsync(TextFieldParser parser, Ca var rawYear = fields[6]; var rawSemester = fields[7]; var shortDescription = fields[8]; - var rawDescription = fields[9]; - var rawCustDevLink = fields[10]; - var rawMvpLink = fields[11]; - var rawRoadMapLink = fields[12]; - var rawGitLink = fields[13]; - var rawNonGitLink = fields[14]; - var rawTagsJson = fields[15]; + var description = fields[9]; + var rawDescriptionLink = fields[10]; + var rawCastDevLink = fields[11]; + var rawMvpLink = fields[12]; + var rawRoadMapLink = fields[13]; + var rawGitLink = fields[14]; + var rawNonGitLink = fields[15]; + var rawTagsJson = fields[16]; return new FullProjectInfo { Name = name, Year = int.Parse(rawYear), Semester = int.Parse(rawSemester), - Description = rawDescription, + Description = description, ShortDescription = shortDescription, TeamMembers = ParseTeamMembers(rawParticipantFields), - Files = ParseFiles(rawDescription, rawCustDevLink, rawMvpLink, rawRoadMapLink, rawGitLink, + Files = ParseFiles(rawDescriptionLink, rawCastDevLink, rawMvpLink, rawRoadMapLink, rawGitLink, rawNonGitLink), Tags = await ParseTagsAsync(rawTagsJson, ct) ?? [] }; diff --git a/Test/CsvProjectParserTests.cs b/Test/CsvProjectParserTests.cs index 4892b78..289482e 100644 --- a/Test/CsvProjectParserTests.cs +++ b/Test/CsvProjectParserTests.cs @@ -31,8 +31,8 @@ public async Task ParseTable_HeaderOnlyCsv_ReturnsEmptyList() public async Task ParseTable_CommaSeparatedCsv_MapsBasicProjectFields() { var csv = """ - Название продукта,Участник 1 (Фамилия Имя),Участник 2 (Фамилия Имя),Участник 3 (Фамилия Имя),Участник 4 (Фамилия Имя),Участник 5? (Фамилия Имя),Год,Семестр,Короткое описание,"Описание продукта (текст, ссылка на документ)",ЦА и вопросы CustDev,Описание MVP (ссылка на документ),Дорожная карта проектов,Гиты,Продукты не гит,Код теги - LearnTogether,Иван Иванов,Мария Петрова,,,,2024,1,Краткое описание,https://example.com/description,https://example.com/custdev,https://example.com/mvp,https://example.com/roadmap,https://github.com/example/project,, + Название продукта,Участник 1 (Фамилия Имя),Участник 2 (Фамилия Имя),Участник 3 (Фамилия Имя),Участник 4 (Фамилия Имя),Участник 5? (Фамилия Имя),Год,Семестр,Короткое описание,Описание,"Описание продукта (текст, ссылка на документ)",ЦА и вопросы CustDev,Описание MVP (ссылка на документ),Дорожная карта проектов,Гиты,Продукты не гит,Код теги + LearnTogether,Иван Иванов,Мария Петрова,,,,2024,1,Краткое описание,Полное описание проекта,https://example.com/description,https://example.com/custdev,https://example.com/mvp,https://example.com/roadmap,https://github.com/example/project,, """; using var stream = CreateCsvStream(csv); @@ -42,9 +42,11 @@ public async Task ParseTable_CommaSeparatedCsv_MapsBasicProjectFields() { Assert.That(result, Has.Count.EqualTo(1)); Assert.That(result[0].Name, Is.EqualTo("LearnTogether")); + Assert.That(result[0].Description, Is.EqualTo("Полное описание проекта")); Assert.That(result[0].ShortDescription, Is.EqualTo("Краткое описание")); Assert.That(result[0].Year, Is.EqualTo(2024)); Assert.That(result[0].Semester, Is.EqualTo(1)); + Assert.That(result[0].Files.Description?.ToString(), Is.EqualTo("https://example.com/description")); }); } @@ -52,12 +54,12 @@ public async Task ParseTable_CommaSeparatedCsv_MapsBasicProjectFields() public async Task ParseTable_MultilineQuotedTagsField_DoesNotBreakRecordBoundaries() { var csv = """ - Название продукта,Участник 1 (Фамилия Имя),Участник 2 (Фамилия Имя),Участник 3 (Фамилия Имя),Участник 4 (Фамилия Имя),Участник 5? (Фамилия Имя),Год,Семестр,Короткое описание,"Описание продукта (текст, ссылка на документ)",ЦА и вопросы CustDev,Описание MVP (ссылка на документ),Дорожная карта проектов,Гиты,Продукты не гит,Код теги - LearnTogether,Иван Иванов,,,,,2024,1,Описание 1,https://example.com/description-1,,https://example.com/mvp-1,,https://github.com/example/project-1,,"{ + Название продукта,Участник 1 (Фамилия Имя),Участник 2 (Фамилия Имя),Участник 3 (Фамилия Имя),Участник 4 (Фамилия Имя),Участник 5? (Фамилия Имя),Год,Семестр,Короткое описание,Описание,"Описание продукта (текст, ссылка на документ)",ЦА и вопросы CustDev,Описание MVP (ссылка на документ),Дорожная карта проектов,Гиты,Продукты не гит,Код теги + LearnTogether,Иван Иванов,,,,,2024,1,Описание 1,Полное описание 1,https://example.com/description-1,,https://example.com/mvp-1,,https://github.com/example/project-1,,"{ ""Platform"": [""web"", ""server""], ""Language"": [""c#""] }" - GuidesAi,Петр Петров,,,,,2024,2,Описание 2,https://example.com/description-2,,https://example.com/mvp-2,,https://github.com/example/project-2,, + GuidesAi,Петр Петров,,,,,2024,2,Описание 2,Полное описание 2,https://example.com/description-2,,https://example.com/mvp-2,,https://github.com/example/project-2,, """; using var stream = CreateCsvStream(csv); @@ -74,8 +76,8 @@ public async Task ParseTable_MultilineQuotedTagsField_DoesNotBreakRecordBoundari public async Task ParseTable_TagsJson_MapsTitlesAndIdsFromMockRepository() { var csv = """ - Название продукта,Участник 1 (Фамилия Имя),Участник 2 (Фамилия Имя),Участник 3 (Фамилия Имя),Участник 4 (Фамилия Имя),Участник 5? (Фамилия Имя),Год,Семестр,Короткое описание,"Описание продукта (текст, ссылка на документ)",ЦА и вопросы CustDev,Описание MVP (ссылка на документ),Дорожная карта проектов,Гиты,Продукты не гит,Код теги - LearnTogether,Иван Иванов,,,,,2024,1,Описание,https://example.com/description,,https://example.com/mvp,,https://github.com/example/project,,"{ + Название продукта,Участник 1 (Фамилия Имя),Участник 2 (Фамилия Имя),Участник 3 (Фамилия Имя),Участник 4 (Фамилия Имя),Участник 5? (Фамилия Имя),Год,Семестр,Короткое описание,Описание,"Описание продукта (текст, ссылка на документ)",ЦА и вопросы CustDev,Описание MVP (ссылка на документ),Дорожная карта проектов,Гиты,Продукты не гит,Код теги + LearnTogether,Иван Иванов,,,,,2024,1,Описание,Полное описание,https://example.com/description,,https://example.com/mvp,,https://github.com/example/project,,"{ ""Platform"": [""web"", ""server""], ""Language"": [""c#""] }" @@ -99,8 +101,8 @@ public async Task ParseTable_TagsJson_MapsTitlesAndIdsFromMockRepository() public async Task ParseTable_BlankOptionalParticipants_DoesNotCreateEmptyTeamMembers() { var csv = """ - Название продукта,Участник 1 (Фамилия Имя),Участник 2 (Фамилия Имя),Участник 3 (Фамилия Имя),Участник 4 (Фамилия Имя),Участник 5? (Фамилия Имя),Год,Семестр,Короткое описание,"Описание продукта (текст, ссылка на документ)",ЦА и вопросы CustDev,Описание MVP (ссылка на документ),Дорожная карта проектов,Гиты,Продукты не гит,Код теги - SoloProject,Иван Иванов,,,,,2024,1,Описание,https://example.com/description,,https://example.com/mvp,,https://github.com/example/project,, + Название продукта,Участник 1 (Фамилия Имя),Участник 2 (Фамилия Имя),Участник 3 (Фамилия Имя),Участник 4 (Фамилия Имя),Участник 5? (Фамилия Имя),Год,Семестр,Короткое описание,Описание,"Описание продукта (текст, ссылка на документ)",ЦА и вопросы CustDev,Описание MVP (ссылка на документ),Дорожная карта проектов,Гиты,Продукты не гит,Код теги + SoloProject,Иван Иванов,,,,,2024,1,Описание,Полное описание,https://example.com/description,,https://example.com/mvp,,https://github.com/example/project,, """; using var stream = CreateCsvStream(csv); @@ -122,6 +124,6 @@ private static MemoryStream CreateCsvStream(string content) private const string Header = """ - Название продукта,Участник 1 (Фамилия Имя),Участник 2 (Фамилия Имя),Участник 3 (Фамилия Имя),Участник 4 (Фамилия Имя),Участник 5? (Фамилия Имя),Год,Семестр,Короткое описание,"Описание продукта (текст, ссылка на документ)",ЦА и вопросы CustDev,Описание MVP (ссылка на документ),Дорожная карта проектов,Гиты,Продукты не гит,Код теги + Название продукта,Участник 1 (Фамилия Имя),Участник 2 (Фамилия Имя),Участник 3 (Фамилия Имя),Участник 4 (Фамилия Имя),Участник 5? (Фамилия Имя),Год,Семестр,Короткое описание,Описание,"Описание продукта (текст, ссылка на документ)",ЦА и вопросы CustDev,Описание MVP (ссылка на документ),Дорожная карта проектов,Гиты,Продукты не гит,Код теги """; -} \ No newline at end of file +} From 6ac38d8ea6b138a1924f3448d2daf6c6949a2c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9A=D0=BE=D1=82=D0=BE=D0=B2?= Date: Mon, 20 Apr 2026 18:18:35 +0500 Subject: [PATCH 07/59] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D1=80=D0=B5?= =?UTF-8?q?=D1=82=D0=B5=D0=BD=D1=88=D0=B8=D0=BD=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Infrastructure/Repositories/MetricRepository.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Infrastructure/Repositories/MetricRepository.cs b/Infrastructure/Repositories/MetricRepository.cs index f57ec8e..0a8c194 100644 --- a/Infrastructure/Repositories/MetricRepository.cs +++ b/Infrastructure/Repositories/MetricRepository.cs @@ -63,9 +63,9 @@ public async Task RebuildOpenCohortsRetentionAsync(CancellationToken ct) ? 0 : userRetentionState.SecondOpen.DayNumber - userRetentionState.FirstOpen.DayNumber; - userRetentionState.r7D = retentionDays >= 7; - userRetentionState.r14D = retentionDays >= 14; - userRetentionState.r30D = retentionDays >= 30; + userRetentionState.r7D = retentionDays <= 7; + userRetentionState.r14D = retentionDays <= 14; + userRetentionState.r30D = retentionDays <= 30; } await metricsContext.SaveChangesAsync(ct); From 3518a20815a488125ef67af5f451a4fcfbf28d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9A=D0=BE=D1=82=D0=BE=D0=B2?= Date: Mon, 20 Apr 2026 18:29:07 +0500 Subject: [PATCH 08/59] =?UTF-8?q?=D0=BF=D0=BE=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Test/MetricRepositoryTests.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Test/MetricRepositoryTests.cs b/Test/MetricRepositoryTests.cs index 77849c8..542c985 100644 --- a/Test/MetricRepositoryTests.cs +++ b/Test/MetricRepositoryTests.cs @@ -182,13 +182,17 @@ public async Task RebuildOpenCohortsRetentionAsync_ShouldUpdateFlagsAndGroupedMe Assert.Multiple(() => { - Assert.That(states[0].r7D, Is.False); + Assert.That(states[0].r7D, Is.True); Assert.That(states[1].r7D, Is.True); + Assert.That(states[0].r14D, Is.True); + Assert.That(states[1].r14D, Is.True); + Assert.That(states[0].r30D, Is.True); + Assert.That(states[1].r30D, Is.True); Assert.That(cohort.CohortDate, Is.EqualTo(new DateOnly(2026, 4, 1))); Assert.That(cohort.Users, Is.EqualTo(2)); - Assert.That(cohort.r7D, Is.EqualTo(1)); - Assert.That(cohort.r14D, Is.EqualTo(0)); - Assert.That(cohort.r30D, Is.EqualTo(0)); + Assert.That(cohort.r7D, Is.EqualTo(2)); + Assert.That(cohort.r14D, Is.EqualTo(2)); + Assert.That(cohort.r30D, Is.EqualTo(2)); }); } } From 38524ee39496c42a07965680d0016842c97f3ccf Mon Sep 17 00:00:00 2001 From: Arina Kiskina Date: Mon, 20 Apr 2026 18:36:31 +0500 Subject: [PATCH 09/59] =?UTF-8?q?links=20fixed,=20=D0=BA=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20mostly=20?= =?UTF-8?q?fixed,=20checkpoin=20before=20big=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Api/Api.http | 2 +- Api/Program.cs | 30 +++++-- Api/wwwroot/css/filters.css | 9 +- Api/wwwroot/css/footer.css | 2 +- Api/wwwroot/css/header.css | 2 +- Api/wwwroot/css/style.css | 14 ++- Api/wwwroot/index.html | 4 +- Api/wwwroot/js/api/projectApi.js | 8 +- Api/wwwroot/js/linkOverlay.js | 90 +++++++++++++++---- Api/wwwroot/js/project.js | 3 + Api/wwwroot/js/projects.js | 40 +++++++++ Api/wwwroot/js/tabs.js | 49 +++++++--- Api/wwwroot/resources/components/filters.html | 3 - 13 files changed, 199 insertions(+), 57 deletions(-) diff --git a/Api/Api.http b/Api/Api.http index 3002a3b..f13ed0d 100644 --- a/Api/Api.http +++ b/Api/Api.http @@ -1,4 +1,4 @@ -@Api_HostAddress = http://projectblueprint.ru/ +@Api_HostAddress = 'http://localhost:63342' GET {{Api_HostAddress}}/ Accept: application/json diff --git a/Api/Program.cs b/Api/Program.cs index 4a1c82c..e8082ef 100644 --- a/Api/Program.cs +++ b/Api/Program.cs @@ -4,8 +4,22 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); -builder.Services.AddProblemDetails(Options => { Options.CustomizeProblemDetails = ProblemDetailsConfig.Configure; }); +builder.Services.AddProblemDetails(options => +{ + options.CustomizeProblemDetails = ProblemDetailsConfig.Configure; +}); + +builder.Services.AddCors(options => +{ + options.AddPolicy("FrontendDev", policy => + { + policy + .WithOrigins("http://localhost:63342") + .AllowAnyHeader() + .AllowAnyMethod(); + }); +}); builder .LoadEnvFiles() .AddTelemetry() @@ -15,26 +29,24 @@ .AddInfrastructureServices(); var app = builder - .Build() - .InitializeDatabase(); - -// await app.EnsureDatabaseReadyAsync(); + .Build() + .InitializeDatabase(); +app.UseExceptionHandler(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } + app.UseCors("FrontendDev"); + app.UseDefaultFiles(); app.UseStaticFiles(); app.UseOpenTelemetryPrometheusScrapingEndpoint(); -app.MapControllers(); -app.Run(); - -app.UseExceptionHandler(); app.MapControllers(); + app.Run(); \ No newline at end of file diff --git a/Api/wwwroot/css/filters.css b/Api/wwwroot/css/filters.css index 514dd32..da95cee 100644 --- a/Api/wwwroot/css/filters.css +++ b/Api/wwwroot/css/filters.css @@ -52,18 +52,13 @@ scrollbar-width: thin; } -.filter-section-title { - margin-top: 28px; - margin-bottom: 18px; -} - .filter-section-title:first-child { margin-top: 0; } .filter-subsection-title { - margin-top: 18px; - margin-bottom: 10px; + margin-top: 20px; + margin-bottom: 20px; } .filter-subsection-title h3 { diff --git a/Api/wwwroot/css/footer.css b/Api/wwwroot/css/footer.css index 8f04f54..410bd91 100644 --- a/Api/wwwroot/css/footer.css +++ b/Api/wwwroot/css/footer.css @@ -91,7 +91,7 @@ .footer-bottom { text-align: center; - padding-top: px; + padding-top: 30px; border-top: 1px solid rgba(255, 255, 255, 0.06); } diff --git a/Api/wwwroot/css/header.css b/Api/wwwroot/css/header.css index b2a0f32..4cdc4d4 100644 --- a/Api/wwwroot/css/header.css +++ b/Api/wwwroot/css/header.css @@ -22,7 +22,7 @@ .logo { display: flex; align-items: center; - gap: 59px; + gap: 30px; text-decoration: none; color: inherit; cursor: pointer; diff --git a/Api/wwwroot/css/style.css b/Api/wwwroot/css/style.css index fb664a3..eb86924 100644 --- a/Api/wwwroot/css/style.css +++ b/Api/wwwroot/css/style.css @@ -291,9 +291,22 @@ body.filters-open { color: #fff; font-size: 22px; cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + line-height: 1; + flex: 0 0 44px; + transition: transform 0.2s ease, opacity 0.2s ease, background 0.2s ease; } +.pagination-btn span { + display: block; + line-height: 1; + transform: translateY(-2.5px); +} + .pagination-btn:hover:not(:disabled) { transform: translateY(-1px); background: #181818; @@ -310,7 +323,6 @@ body.filters-open { color: #C3C3C3; font-size: 15px; } - @media (max-width: 900px) { .projects-grid { grid-template-columns: repeat(2, 1fr); diff --git a/Api/wwwroot/index.html b/Api/wwwroot/index.html index 043cb5e..c7932c9 100644 --- a/Api/wwwroot/index.html +++ b/Api/wwwroot/index.html @@ -61,13 +61,13 @@

Страница 1 из 1
diff --git a/Api/wwwroot/js/api/projectApi.js b/Api/wwwroot/js/api/projectApi.js index 3ed86db..31ae981 100644 --- a/Api/wwwroot/js/api/projectApi.js +++ b/Api/wwwroot/js/api/projectApi.js @@ -1,9 +1,11 @@ console.log('projectApi.js loaded'); console.log('project window.location.origin =', window.location.origin); +const BASE_URL = 'http://localhost'; + function buildUrl(path, params = new URLSearchParams()) { const query = params.toString(); - return `${path}${query ? `?${query}` : ''}`; + return `${BASE_URL}${path}${query ? `?${query}` : ''}`; } async function fetchJson(path, params = new URLSearchParams(), errorText = 'Request failed') { @@ -12,12 +14,12 @@ async function fetchJson(path, params = new URLSearchParams(), errorText = 'Requ const response = await fetch(url, { headers: { - Accept: '*/*' + Accept: 'application/json' } }); if (!response.ok) { - throw new Error(errorText); + throw new Error(`${errorText} (${response.status})`); } return await response.json(); diff --git a/Api/wwwroot/js/linkOverlay.js b/Api/wwwroot/js/linkOverlay.js index 9a503c8..9faab8a 100644 --- a/Api/wwwroot/js/linkOverlay.js +++ b/Api/wwwroot/js/linkOverlay.js @@ -1,31 +1,87 @@ -function openOverlay() { - document.querySelector('.overlay').style.display = 'block'; - document.querySelector('.overlay-bg').style.display = 'block'; +function getBaseProjectLink() { + return `${window.location.origin}${window.location.pathname}${window.location.search}`; +} + +function getActiveTabId() { + const activeButton = document.querySelector(".tab-btn.active"); + if (activeButton?.dataset.tab) { + return activeButton.dataset.tab; + } + + const hashTab = window.location.hash.replace("#", ""); + return hashTab || "short"; +} + +function updateOverlayLinks() { + const fileLinkInput = document.getElementById("fileLink"); + const projectLinkInput = document.getElementById("projectLink"); + + if (!fileLinkInput || !projectLinkInput) return; + + const projectLink = getBaseProjectLink(); + const activeTab = getActiveTabId(); + const fileLink = `${projectLink}#${activeTab}`; + + fileLinkInput.value = fileLink; + projectLinkInput.value = projectLink; +} + +function openOverlay() { + updateOverlayLinks(); + + const overlay = document.querySelector(".overlay"); + const overlayBg = document.querySelector(".overlay-bg"); + + if (!overlay || !overlayBg) return; + + overlay.style.display = 'block'; + overlayBg.style.display = 'block'; document.body.style.overflow = 'hidden'; } function closeOverlay() { - document.querySelector('.overlay').style.display = 'none'; - document.querySelector('.overlay-bg').style.display = 'none'; + const overlay = document.querySelector('.overlay'); + const overlayBg = document.querySelector('.overlay-bg'); + + if (!overlay || !overlayBg) return; + + overlay.style.display = 'none'; + overlayBg.style.display = 'none'; document.body.style.overflow = ''; } function copyLink(id) { const input = document.getElementById(id); - input.select(); - input.setSelectionRange(0, 99999); + if (!input) return; + navigator.clipboard.writeText(input.value).then(() => { alert("Ссылка скопирована!"); }); } -document.addEventListener("DOMContentLoaded", function () { - fetch('linkOverlay.html') - .then(response => response.text()) - .then(html => { - const container = document.createElement('div'); - container.innerHTML = html; - document.body.appendChild(container); - }) - .catch(err => console.error('Ошибка загрузки оверлея:', err)); -}); \ No newline at end of file +async function loadOverlay() { + if (document.querySelector(".overlay")) { + updateOverlayLinks(); + return; + } + + try { + const response = await fetch('linkOverlay.html'); + const html = await response.text(); + + const container = document.createElement('div'); + container.innerHTML = html; + document.body.appendChild(container); + + updateOverlayLinks(); + } catch (err) { + console.error('Ошибка загрузки оверлея:', err); + } +} + +window.openOverlay = openOverlay; +window.closeOverlay = closeOverlay; +window.copyLink = copyLink; +window.updateOverlayLinks = updateOverlayLinks; + +document.addEventListener("DOMContentLoaded", loadOverlay); \ No newline at end of file diff --git a/Api/wwwroot/js/project.js b/Api/wwwroot/js/project.js index e7e6d19..b0f8466 100644 --- a/Api/wwwroot/js/project.js +++ b/Api/wwwroot/js/project.js @@ -241,6 +241,9 @@ function renderProject(project) { renderStack(project.tags); generateTabs(project.files); + if (typeof window.activateTabFromHash === "function") { + window.activateTabFromHash(false); + } } loadProject(); \ No newline at end of file diff --git a/Api/wwwroot/js/projects.js b/Api/wwwroot/js/projects.js index e818f9f..a7bcd63 100644 --- a/Api/wwwroot/js/projects.js +++ b/Api/wwwroot/js/projects.js @@ -18,6 +18,29 @@ let searchDebounce = null; console.log('projects.js loaded'); console.log('window.location.origin =', window.location.origin); + +function scrollToProjectsTop() { + const target = document.querySelector('.search-section') || document.querySelector('.projects'); + const headerOffset = 110; + + requestAnimationFrame(() => { + if (!target) { + window.scrollTo({ + top: 0, + behavior: 'smooth' + }); + return; + } + + const top = target.getBoundingClientRect().top + window.scrollY - headerOffset; + + window.scrollTo({ + top: Math.max(top, 0), + behavior: 'smooth' + }); + }); +} + function renderPagination() { const pagination = document.getElementById('projects-pagination'); const prevBtn = document.getElementById('pagination-prev'); @@ -35,7 +58,24 @@ function renderPagination() { nextBtn.disabled = currentPage >= totalPages; pagination.style.display = totalPages > 1 ? 'flex' : 'none'; + + prevBtn.onclick = async () => { + if (state.page <= 1) return; + + state.page--; + await loadProjects(); + scrollToProjectsTop(); + }; + + nextBtn.onclick = async () => { + if (state.page >= state.totalPages) return; + + state.page++; + await loadProjects(); + scrollToProjectsTop(); + }; } + function createTagChip(tag) { if (tag.icon) { const img = document.createElement('img'); diff --git a/Api/wwwroot/js/tabs.js b/Api/wwwroot/js/tabs.js index d77b592..12f468a 100644 --- a/Api/wwwroot/js/tabs.js +++ b/Api/wwwroot/js/tabs.js @@ -1,23 +1,48 @@ const buttons = document.querySelectorAll(".tab-btn"); -buttons.forEach(button => { +function activateTab(tabId, updateHash = true) { + const tabs = document.querySelectorAll(".tab"); - button.addEventListener("click", () => { + let targetId = tabId || "short"; - const tabId = button.dataset.tab; + const targetTab = document.getElementById(targetId); + const targetButton = document.querySelector(`.tab-btn[data-tab="${targetId}"]`); - const tabs = document.querySelectorAll(".tab"); + if (!targetTab || !targetButton) { + targetId = "short"; + } - buttons.forEach(btn => btn.classList.remove("active")); - tabs.forEach(tab => tab.classList.remove("active")); + buttons.forEach(btn => { + btn.classList.toggle("active", btn.dataset.tab === targetId); + }); + + document.querySelectorAll(".tab").forEach(tab => { + tab.classList.toggle("active", tab.id === targetId); + }); - button.classList.add("active"); + if (updateHash) { + history.replaceState(null, "", `${window.location.pathname}${window.location.search}#${targetId}`); + } +} - const targetTab = document.getElementById(tabId); - if (targetTab) { - targetTab.classList.add("active"); - } +function activateTabFromHash(updateHash = false) { + const hash = window.location.hash.replace("#", "").trim(); + activateTab(hash || "short", updateHash); +} +buttons.forEach(button => { + button.addEventListener("click", () => { + activateTab(button.dataset.tab, true); }); +}); + +document.addEventListener("DOMContentLoaded", () => { + activateTabFromHash(false); +}); + +window.addEventListener("hashchange", () => { + activateTabFromHash(false); +}); -}); \ No newline at end of file +window.activateTabFromHash = activateTabFromHash; +window.activateTab = activateTab; \ No newline at end of file diff --git a/Api/wwwroot/resources/components/filters.html b/Api/wwwroot/resources/components/filters.html index 6539377..b4e40b4 100644 --- a/Api/wwwroot/resources/components/filters.html +++ b/Api/wwwroot/resources/components/filters.html @@ -5,9 +5,6 @@

Фильтры

-
-

Стек

-
From 07ddce5efc9761e40b14a4021aefc54ba350e7f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9A=D0=BE=D1=82=D0=BE=D0=B2?= Date: Mon, 20 Apr 2026 18:38:16 +0500 Subject: [PATCH 10/59] =?UTF-8?q?=D0=BF=D0=BE=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83=20=D0=B2=20?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D1=80=D0=B8=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/MetricRepository.cs | 15 ++++--- Test/MetricRepositoryTests.cs | 43 ++++++++++++------- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/Infrastructure/Repositories/MetricRepository.cs b/Infrastructure/Repositories/MetricRepository.cs index 0a8c194..b08f494 100644 --- a/Infrastructure/Repositories/MetricRepository.cs +++ b/Infrastructure/Repositories/MetricRepository.cs @@ -59,13 +59,14 @@ public async Task RebuildOpenCohortsRetentionAsync(CancellationToken ct) foreach (var userRetentionState in userRetentionStates) { - var retentionDays = userRetentionState.SecondOpen == default - ? 0 - : userRetentionState.SecondOpen.DayNumber - userRetentionState.FirstOpen.DayNumber; - - userRetentionState.r7D = retentionDays <= 7; - userRetentionState.r14D = retentionDays <= 14; - userRetentionState.r30D = retentionDays <= 30; + var hasSecondOpen = userRetentionState.SecondOpen != default; + var retentionDays = hasSecondOpen + ? userRetentionState.SecondOpen.DayNumber - userRetentionState.FirstOpen.DayNumber + : int.MaxValue; + + userRetentionState.r7D = hasSecondOpen && retentionDays <= 7; + userRetentionState.r14D = hasSecondOpen && retentionDays <= 14; + userRetentionState.r30D = hasSecondOpen && retentionDays <= 30; } await metricsContext.SaveChangesAsync(ct); diff --git a/Test/MetricRepositoryTests.cs b/Test/MetricRepositoryTests.cs index 542c985..bca5b61 100644 --- a/Test/MetricRepositoryTests.cs +++ b/Test/MetricRepositoryTests.cs @@ -156,41 +156,54 @@ public async Task RegisterFilteredViewAsync_ShouldCreateView() [Test] public async Task RebuildOpenCohortsRetentionAsync_ShouldUpdateFlagsAndGroupedMetrics() { + var returnedWithinSevenDaysUserId = Guid.NewGuid(); + var returnedWithinFourteenDaysUserId = Guid.NewGuid(); + var didNotReturnUserId = Guid.NewGuid(); + metricsContext.UserRetentionStates.AddRange( new UserRetentionState { - UserId = Guid.NewGuid(), + UserId = returnedWithinSevenDaysUserId, FirstOpen = new DateOnly(2026, 4, 1), - SecondOpen = new DateOnly(2026, 4, 8), + SecondOpen = new DateOnly(2026, 4, 2), }, new UserRetentionState { - UserId = Guid.NewGuid(), + UserId = returnedWithinFourteenDaysUserId, + FirstOpen = new DateOnly(2026, 4, 1), + SecondOpen = new DateOnly(2026, 4, 9), + }, + new UserRetentionState + { + UserId = didNotReturnUserId, FirstOpen = new DateOnly(2026, 4, 1), - SecondOpen = new DateOnly(2026, 4, 2), }); await metricsContext.SaveChangesAsync(); await repository.RebuildOpenCohortsRetentionAsync(CancellationToken.None); - var states = await metricsContext.UserRetentionStates - .OrderBy(x => x.SecondOpen) - .ToListAsync(); + var states = await metricsContext.UserRetentionStates.ToDictionaryAsync(x => x.UserId); var cohort = await metricsContext.RetentionByCohorts.SingleAsync(); Assert.Multiple(() => { - Assert.That(states[0].r7D, Is.True); - Assert.That(states[1].r7D, Is.True); - Assert.That(states[0].r14D, Is.True); - Assert.That(states[1].r14D, Is.True); - Assert.That(states[0].r30D, Is.True); - Assert.That(states[1].r30D, Is.True); + Assert.That(states[returnedWithinSevenDaysUserId].r7D, Is.True); + Assert.That(states[returnedWithinSevenDaysUserId].r14D, Is.True); + Assert.That(states[returnedWithinSevenDaysUserId].r30D, Is.True); + + Assert.That(states[returnedWithinFourteenDaysUserId].r7D, Is.False); + Assert.That(states[returnedWithinFourteenDaysUserId].r14D, Is.True); + Assert.That(states[returnedWithinFourteenDaysUserId].r30D, Is.True); + + Assert.That(states[didNotReturnUserId].r7D, Is.False); + Assert.That(states[didNotReturnUserId].r14D, Is.False); + Assert.That(states[didNotReturnUserId].r30D, Is.False); + Assert.That(cohort.CohortDate, Is.EqualTo(new DateOnly(2026, 4, 1))); - Assert.That(cohort.Users, Is.EqualTo(2)); - Assert.That(cohort.r7D, Is.EqualTo(2)); + Assert.That(cohort.Users, Is.EqualTo(3)); + Assert.That(cohort.r7D, Is.EqualTo(1)); Assert.That(cohort.r14D, Is.EqualTo(2)); Assert.That(cohort.r30D, Is.EqualTo(2)); }); From 1ccf27bf1d7956f2ee418e9772a123966d1d18a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9A=D0=BE=D1=82=D0=BE=D0=B2?= Date: Mon, 20 Apr 2026 20:57:49 +0500 Subject: [PATCH 11/59] loki --- docker-compose.yaml | 30 ++++++++++ monitoring/alloy/config.alloy | 53 +++++++++++++++++ .../provisioning/datasources/banana-loki.yml | 11 ++++ .../datasources/banana-prometheus.yml | 2 +- monitoring/loki/loki-config.yaml | 59 +++++++++++++++++++ 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 monitoring/alloy/config.alloy create mode 100644 monitoring/grafana/provisioning/datasources/banana-loki.yml create mode 100644 monitoring/loki/loki-config.yaml diff --git a/docker-compose.yaml b/docker-compose.yaml index 6260272..bf1355e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -52,6 +52,33 @@ services: volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + loki: + image: grafana/loki:3.7.0 + container_name: loki + command: -config.file=/etc/loki/loki-config.yaml + ports: + - "3100:3100" + volumes: + - ./monitoring/loki/loki-config.yaml:/etc/loki/loki-config.yaml:ro + - loki_data:/loki + + alloy: + image: grafana/alloy:latest + container_name: alloy + command: + - run + - --server.http.listen-addr=0.0.0.0:12345 + - --storage.path=/var/lib/alloy/data + - /etc/alloy/config.alloy + ports: + - "12345:12345" + depends_on: + - loki + volumes: + - ./monitoring/alloy/config.alloy:/etc/alloy/config.alloy:ro + - /var/run/docker.sock:/var/run/docker.sock + - alloy_data:/var/lib/alloy/data + grafana: image: grafana/grafana:latest container_name: grafana @@ -59,6 +86,7 @@ services: - "3000:3000" depends_on: - prometheus + - loki - db environment: GF_SECURITY_ADMIN_USER: ${GF_USER} @@ -72,3 +100,5 @@ services: volumes: postgres_data: + loki_data: + alloy_data: diff --git a/monitoring/alloy/config.alloy b/monitoring/alloy/config.alloy new file mode 100644 index 0000000..6b2d656 --- /dev/null +++ b/monitoring/alloy/config.alloy @@ -0,0 +1,53 @@ +discovery.docker "banana" { + host = "unix:///var/run/docker.sock" + refresh_interval = "5s" +} + +discovery.relabel "banana" { + targets = [] + + rule { + source_labels = ["__meta_docker_container_name"] + regex = "/(.*)" + target_label = "container" + } + + rule { + source_labels = ["__meta_docker_container_label_com_docker_compose_service"] + target_label = "service" + } + + rule { + source_labels = ["__meta_docker_container_label_com_docker_compose_project"] + target_label = "compose_project" + } +} + +loki.source.docker "banana" { + host = "unix:///var/run/docker.sock" + targets = discovery.docker.banana.targets + relabel_rules = discovery.relabel.banana.rules + refresh_interval = "5s" + labels = { job = "docker" } + forward_to = [loki.process.banana.receiver] +} + +loki.process "banana" { + stage.static_labels { + values = { + stack = "banana", + } + } + + forward_to = [loki.write.banana.receiver] +} + +loki.write "banana" { + endpoint { + url = "http://loki:3100/loki/api/v1/push" + } +} + +livedebugging { + enabled = true +} diff --git a/monitoring/grafana/provisioning/datasources/banana-loki.yml b/monitoring/grafana/provisioning/datasources/banana-loki.yml new file mode 100644 index 0000000..8115761 --- /dev/null +++ b/monitoring/grafana/provisioning/datasources/banana-loki.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +datasources: + - name: Banana Loki + uid: banana-loki + type: loki + access: proxy + url: http://loki:3100 + isDefault: false + jsonData: + maxLines: 1000 diff --git a/monitoring/grafana/provisioning/datasources/banana-prometheus.yml b/monitoring/grafana/provisioning/datasources/banana-prometheus.yml index 1ef291c..daa60e3 100644 --- a/monitoring/grafana/provisioning/datasources/banana-prometheus.yml +++ b/monitoring/grafana/provisioning/datasources/banana-prometheus.yml @@ -5,7 +5,7 @@ datasources: uid: banana-prometheus type: prometheus access: proxy - url: prometheus:9090 + url: http://prometheus:9090 isDefault: false jsonData: httpMethod: POST diff --git a/monitoring/loki/loki-config.yaml b/monitoring/loki/loki-config.yaml new file mode 100644 index 0000000..7c60077 --- /dev/null +++ b/monitoring/loki/loki-config.yaml @@ -0,0 +1,59 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + grpc_listen_port: 9096 + log_level: info + grpc_server_max_concurrent_streams: 1000 + +common: + instance_addr: 127.0.0.1 + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +query_range: + results_cache: + cache: + embedded_cache: + enabled: true + max_size_mb: 100 + +limits_config: + metric_aggregation_enabled: true + allow_structured_metadata: true + volume_enabled: true + retention_period: 168h + +schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +pattern_ingester: + enabled: true + metric_aggregation: + loki_address: localhost:3100 + +ruler: + enable_alertmanager_discovery: true + enable_api: true + +frontend: + encoding: protobuf + +compactor: + working_directory: /loki/retention + delete_request_store: filesystem + retention_enabled: true From 367f2c4a80e01c2c77740ce49d9ea71aa18a719f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9A=D0=BE=D1=82=D0=BE=D0=B2?= Date: Mon, 20 Apr 2026 21:34:01 +0500 Subject: [PATCH 12/59] =?UTF-8?q?=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D0=BB=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=80=D1=82=D1=8B=20=D1=81=D0=BE=20=D0=B2=D0=BD?= =?UTF-8?q?=D0=B5=D1=88=D0=BD=D0=B8=D1=85=20=D1=83=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B9=D1=81=D1=82=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yaml | 20 +++++++++++++++----- nginx.conf | 9 --------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index bf1355e..d08cd0e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -12,8 +12,10 @@ services: depends_on: db: condition: service_healthy + expose: + - "8080" ports: - - "8080:8080" + - "127.0.0.1:8080:8080" volumes: - ./etc/SeedData/tag.json:/etc/SeedData/tag.json:ro - ./etc/SeedData/tagTypes.json:/etc/SeedData/tagTypes.json:ro @@ -29,8 +31,10 @@ services: test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ] interval: 5s retries: 10 + expose: + - "5432" ports: - - "5332:5432" + - "127.0.0.1:5332:5432" nginx: image: nginx:latest @@ -47,8 +51,10 @@ services: prometheus: image: prom/prometheus:latest container_name: prometheus + expose: + - "9090" ports: - - "9090:9090" + - "127.0.0.1:9090:9090" volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro @@ -56,8 +62,10 @@ services: image: grafana/loki:3.7.0 container_name: loki command: -config.file=/etc/loki/loki-config.yaml + expose: + - "3100" ports: - - "3100:3100" + - "127.0.0.1:3100:3100" volumes: - ./monitoring/loki/loki-config.yaml:/etc/loki/loki-config.yaml:ro - loki_data:/loki @@ -70,8 +78,10 @@ services: - --server.http.listen-addr=0.0.0.0:12345 - --storage.path=/var/lib/alloy/data - /etc/alloy/config.alloy + expose: + - "12345" ports: - - "12345:12345" + - "127.0.0.1:12345:12345" depends_on: - loki volumes: diff --git a/nginx.conf b/nginx.conf index c7dd6a4..e1b2833 100644 --- a/nginx.conf +++ b/nginx.conf @@ -49,15 +49,6 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } - location = /metrics { - proxy_pass http://backend:8080; - proxy_http_version 1.1; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } location / { root /var/www/html; From 131a2ed8e476426cc5aa35f7c913c73ad0de9b36 Mon Sep 17 00:00:00 2001 From: Arina Kiskina Date: Mon, 20 Apr 2026 22:49:33 +0500 Subject: [PATCH 13/59] =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BB=D0=B5=D1=87?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B0=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Api/wwwroot/css/filters.css | 4 +- Api/wwwroot/css/linkOverlay.css | 7 +- Api/wwwroot/css/style.css | 277 ++++++++++-------- Api/wwwroot/index.html | 5 - Api/wwwroot/js/api/projectApi.js | 7 +- Api/wwwroot/js/{app.js => cookie.js} | 0 Api/wwwroot/js/header.js | 35 ++- Api/wwwroot/js/project.js | 9 +- Api/wwwroot/js/projects.js | 169 ++++++++++- Api/wwwroot/js/searchAndFilterHandler.js | 2 +- Api/wwwroot/resources/components/filters.html | 4 +- Api/wwwroot/resources/components/footer.html | 6 +- Api/wwwroot/resources/images/hlopin.png | Bin 0 -> 31927 bytes 13 files changed, 353 insertions(+), 172 deletions(-) rename Api/wwwroot/js/{app.js => cookie.js} (100%) create mode 100644 Api/wwwroot/resources/images/hlopin.png diff --git a/Api/wwwroot/css/filters.css b/Api/wwwroot/css/filters.css index da95cee..4d6b5ae 100644 --- a/Api/wwwroot/css/filters.css +++ b/Api/wwwroot/css/filters.css @@ -2,7 +2,9 @@ width: 100%; height: calc(100vh - 130px); min-height: 0; - background: #111; + position: sticky; + top: 110px; + background: transparent; border-radius: 20px; color: white; font-family: 'Raleway', sans-serif; diff --git a/Api/wwwroot/css/linkOverlay.css b/Api/wwwroot/css/linkOverlay.css index 877ef4d..6e4a851 100644 --- a/Api/wwwroot/css/linkOverlay.css +++ b/Api/wwwroot/css/linkOverlay.css @@ -5,7 +5,7 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - background: white; + background: #C3C3C3; padding: 5% 6%; border-radius: 20px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5); @@ -51,6 +51,7 @@ } .scroll-link-wrapper { + display: flex; overflow-y: auto; max-height: 200px; flex-wrap: wrap; @@ -111,7 +112,7 @@ margin-left: 15px; cursor: pointer; border: none; - background: #ffffff; + background: #C3C3C3; padding: 10px 18px; border-radius: 10px; font-size: 16px; @@ -120,5 +121,5 @@ } .copy-btn:hover { - background: #e0e0e0; + transform: scale(1.1); } \ No newline at end of file diff --git a/Api/wwwroot/css/style.css b/Api/wwwroot/css/style.css index eb86924..e10f0cf 100644 --- a/Api/wwwroot/css/style.css +++ b/Api/wwwroot/css/style.css @@ -6,7 +6,7 @@ body { background: #191919; - color: white; + color: #fff; font-family: Raleway, sans-serif; } @@ -23,14 +23,20 @@ body.filters-open { /* SEARCH */ .search-section { - margin-top: 0; - padding-top: 80px; + display: none; + margin: 0; + padding-top: 0; +} + +.search-section.active { + display: block; + padding-bottom: 60px; } .search-row { display: flex; - gap: 20px; align-items: center; + gap: 20px; flex-wrap: nowrap; } @@ -39,14 +45,14 @@ body.filters-open { align-items: center; justify-content: center; gap: 8px; - padding: 0 20px; min-height: 48px; - border-radius: 30px; + padding: 0 20px; border: none; + border-radius: 30px; + background: #111; + color: #fff; font-family: Raleway, sans-serif; font-size: 16px; - background: #111; - color: white; cursor: pointer; flex: 0 0 auto; white-space: nowrap; @@ -57,14 +63,14 @@ body.filters-open { } .search { - min-height: 48px; - padding: 0 20px; display: flex; align-items: center; - background: white; - border-radius: 30px; flex: 1; min-width: 0; + min-height: 48px; + padding: 0 20px; + background: #fff; + border-radius: 30px; } .search-icon { @@ -75,87 +81,38 @@ body.filters-open { .search label { display: flex; - flex: 1; align-items: center; + flex: 1; min-width: 0; height: 100%; } .search input { - border: none; - outline: none; - flex: 1; width: 100%; height: 100%; - font-size: 16px; - background: transparent; + flex: 1; min-width: 0; + border: none; + outline: none; + background: transparent; + font-size: 16px; } .clear { - cursor: pointer; - font-size: 20px; - color: gray; - border: none; - background: none; display: none; flex-shrink: 0; + border: none; + background: none; + color: gray; + font-size: 20px; + cursor: pointer; } .clear.active { display: block; } -/* PROJECTS */ - -.projects { - margin-top: 60px; -} - -.projects-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 40px; -} - -.card { - display: flex; - flex-direction: column; - background: #111; - border-radius: 16px; - padding: 25px; - min-height: 260px; - text-decoration: none; - color: inherit; - transition: transform 0.2s ease, box-shadow 0.2s ease; -} - -.card:hover { - transform: translateY(-5px); - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3); -} - -.card h1, -.card h3 { - font-size: 20px; - margin-bottom: 10px; -} - -.card p { - flex: 1; - margin-bottom: 14px; - font-size: 16px; - line-height: 1.4; - color: #d7d7d7; -} - -.stack { - margin-top: auto; -} - -.search-section { - padding-top: 0; -} +/* CATALOG */ .catalog-page { padding-top: 80px; @@ -165,7 +122,7 @@ body.filters-open { display: grid; grid-template-columns: 0 minmax(0, 1fr); gap: 0; - align-items: start; + align-items: stretch; transition: grid-template-columns 0.25s ease, gap 0.25s ease; } @@ -175,56 +132,111 @@ body.filters-open { } .catalog-sidebar { - min-width: 0; width: 0; + min-width: 0; overflow: hidden; opacity: 0; + align-self: stretch; + background: #111; + border-radius: 20px; transition: width 0.25s ease, opacity 0.2s ease; } .catalog-layout.filters-open .catalog-sidebar { width: 369px; opacity: 1; + overflow: visible; +} + +#filters-container { + width: 100%; + height: 100%; } .catalog-content { min-width: 0; } -.search-section { - padding-top: 0; +#filters-container { + width: 100%; + display: flex; } -@media (max-width: 980px) { - .catalog-layout, - .catalog-layout.filters-open { - grid-template-columns: 1fr; - gap: 0; - } +/* PROJECTS */ - .catalog-sidebar, - .catalog-layout.filters-open .catalog-sidebar { - width: 100%; - opacity: 1; - display: none; - } +.projects { + margin-top: 0px; +} - .catalog-layout.filters-open .catalog-sidebar { - display: block; - margin-bottom: 24px; - } +.projects-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 40px; +} +.search-stub { + grid-column: 1 / -1; + display: flex; + flex-direction: column; + align-items: center; + gap: 18px; + padding: 28px 0 12px; + text-align: center; } -@media (max-width: 980px) { - .catalog-layout { - grid-template-columns: 1fr; - } +.search-stub__text { + margin: 0; + max-width: 480px; + font-size: 20px; + line-height: 1.5; + color: #fff; +} - .catalog-sidebar { - position: static; - height: auto; - } +.search-stub__text span { + color: #9D6A69; + font-weight: 700; +} + +.search-stub__image { + width: 180px; + height: auto; + opacity: 0.95; + filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.12)); +} +.card { + display: flex; + flex-direction: column; + min-height: 260px; + padding: 25px; + background: #111; + border-radius: 16px; + color: inherit; + text-decoration: none; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3); +} + +.card h1, +.card h3 { + margin-bottom: 10px; + font-size: 20px; } + +.card p { + flex: 1; + margin-bottom: 14px; + color: #d7d7d7; + font-size: 16px; + line-height: 1.4; +} + +.stack { + margin-top: auto; +} + .stack-title { display: block; margin-bottom: 8px; @@ -233,9 +245,9 @@ body.filters-open { .icons { display: flex; + flex-wrap: wrap; gap: 8px; margin-top: 5px; - flex-wrap: wrap; } .icons img { @@ -261,43 +273,46 @@ body.filters-open { font-size: 14px; } -.projects-empty { +.projects-empty, +.projects-error { grid-column: 1 / -1; padding: 36px 0; - color: #c3c3c3; font-size: 18px; } +.projects-empty { + color: #c3c3c3; +} + .projects-error { - grid-column: 1 / -1; - padding: 36px 0; color: #ffb3b3; - font-size: 18px; } + +/* PAGINATION */ + .projects-pagination { - margin-top: 36px; display: flex; align-items: center; justify-content: center; gap: 16px; + margin-top: 36px; } .pagination-btn { + display: inline-flex; + align-items: center; + justify-content: center; width: 44px; height: 44px; + flex: 0 0 44px; + padding: 0; border: none; border-radius: 50%; background: #111; color: #fff; font-size: 22px; - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; line-height: 1; - flex: 0 0 44px; - + cursor: pointer; transition: transform 0.2s ease, opacity 0.2s ease, background 0.2s ease; } @@ -320,17 +335,41 @@ body.filters-open { .pagination-info { min-width: 140px; text-align: center; - color: #C3C3C3; + color: #c3c3c3; font-size: 15px; } + +/* ADAPTIVE */ + +@media (max-width: 980px) { + .catalog-layout, + .catalog-layout.filters-open { + grid-template-columns: 1fr; + gap: 0; + } + + .catalog-sidebar, + .catalog-layout.filters-open .catalog-sidebar { + width: 100%; + opacity: 1; + display: none; + position: static; + height: auto; + } + + .catalog-layout.filters-open .catalog-sidebar { + display: block; + margin-bottom: 24px; + } +} + @media (max-width: 900px) { .projects-grid { grid-template-columns: repeat(2, 1fr); gap: 24px; } - .search-section { - margin-top: 0; + .search-section.active { padding-top: 60px; } } @@ -387,9 +426,9 @@ body.filters-open { } .filter-btn { - min-height: 44px; width: 44px; min-width: 44px; + min-height: 44px; padding: 0; gap: 0; border-radius: 50%; diff --git a/Api/wwwroot/index.html b/Api/wwwroot/index.html index c7932c9..475c35d 100644 --- a/Api/wwwroot/index.html +++ b/Api/wwwroot/index.html @@ -25,11 +25,6 @@
- -