Compare commits

..
21 Commits
Author SHA1 Message Date
Rafał Tyński 67f4925bd3 chore(frontend): replace favicon with new TarninaHub mark 2026-10-04 05:42:48 +02:00
Rafał Tyński c23fa95640 docs(readme): document dev ports, TLS certs and default proxy 2026-10-04 05:42:48 +02:00
Rafał Tyński 69af054774 chore(dev): add HTTPS Vite dev server and HTTP launch profile
Serve Vite over HTTPS on :7081 using a certificate exported from dotnet dev-certs (scripts/dev-certs.ps1). Add an http launch profile on :5078 as the default backend and point the /api proxy at it, so dev runs on defaults without env vars.
2026-10-04 05:42:35 +02:00
Rafał Tyński e176d0c211 feat(client): add English default and Ukrainian language with switcher
Default language is English; detect the browser language (Polish -> pl, Ukrainian -> uk, otherwise en) and persist the user choice. Add full Ukrainian (Cyrillic) translations, a language switcher showing native endonyms, and keep vue-i18n, Vuetify locale, <html lang> and document.title in sync.
2026-10-04 04:12:36 +02:00
Rafał Tyński 9854133ac0 feat(client): add high-contrast theme option to theme switcher
Extend theme modes with an explicit high-contrast choice (WCAG 1.4.3) and expose i18n label keys for the theme menu.
2026-10-04 04:04:54 +02:00
Rafał Tyński 804ad617ad feat(client): add app skeleton with routing, state and i18n
Add vue-router routes with accessible navigation (document title + focus management), Pinia session store, vue-i18n (PL/EN) and the default layout. Replace the landing page with routed views and remove the old MainView.
2026-10-04 04:04:35 +02:00
Rafał Tyński 92a904b8a9 feat(client): add accessible design system and API client foundation
Add design tokens, base components (page header, section, status chip, empty state, loading, announcer), a11y composables (text scaling, announcements), high-contrast theme and a typed HTTP client with ProblemDetails error mapping.
2026-10-04 04:04:31 +02:00
Rafał Tyński 4b0225008c chore(client): add lint, format and test tooling
Configure ESLint 10 (flat, Vue + a11y), Prettier, Vitest + @vue/test-utils + jsdom, and add npm scripts for lint/format/test. Document VITE_API_BASE_URL.
2026-10-04 04:04:26 +02:00
Rafał Tyński cca20a9965 test(backend): cover step 17 modules and extract stubs
Add tests for partnership workflow, middleman adaptation and idea notifications; extract reusable test stubs into separate files; add architecture guard forbidding object-valued dictionaries in data contracts.
2026-10-04 03:49:51 +02:00
Rafał Tyński fa1199539b feat(backend): add communication, tester, admin and middleman modules
Implement step 17 modules: threads/messages, notifications with automation (submission.created, idea.created, grant_call.opened), innovation test feedback, admin panel (submissions, verify, metrics, grant call status), Middleman adaptation and partnerships. Split multi-type files into one type per file and replace object-valued dictionaries with string-valued maps in data contracts (migrations 0006/0007).
2026-10-04 03:49:45 +02:00
Rafał Tyński f4f3b3896a docs(backend): document C# conventions in AGENTS.md
Add C# language conventions: one type per file, file-scoped namespaces, sealed/internal types, DTO as records, ban on object-valued dictionaries with the enforcing test, layering, naming and formatting rules.
2026-10-04 03:49:39 +02:00
Rafał Tyński ded596d872 test(ideas): cover workflow, application window and controller outcomes
Add unit tests for idea status transitions, granted application windows and applications controller status mapping (409 when call closed, 201 on success).
2026-10-04 03:13:50 +02:00
Rafał Tyński 127ca92f63 feat(ideas): add idea creator, grant calls and applications
Implement the idea creator and application generator: idea status workflow, social canvas card, grant call windows with 409 when closed, and an LLM-backed idea assistant with offline fallback.
2026-10-04 03:13:45 +02:00
Rafał Tyński fd408b102b test(backend): add matchmaking LLM scoring and limiter tests 2026-10-04 02:52:45 +02:00
Rafał Tyński 0c10489a5e chore(config): configure LLM provider and cost limits 2026-10-04 02:52:45 +02:00
Rafał Tyński 03e2d66ca4 feat(backend): add LLM provider abstraction, contextual ranking and explanations
Add a pluggable LLM provider with an offline template fallback, prompt builder, contextual match scoring (area/location boosts, availability), per-solution explanations and in-memory request limiting for cost control.
2026-10-04 02:52:45 +02:00
Rafał Tyński 182c26d9f8 test(backend): add matchmaking relevance and ranking tests 2026-10-04 02:44:03 +02:00
Rafał Tyński 25ce901021 feat(backend): add semantic matchmaking with embedding pipeline
Add a pluggable embedding provider with an offline deterministic default, vector indexing for problems and innovations, cosine semantic ranking, similar cases and solution suggestions, area filtering and an admin reindex endpoint. Refresh the index when innovations or submissions change.
2026-10-04 02:44:03 +02:00
Rafał Tyński 2bc083fb95 feat(sql): add problem embedding vector index 2026-10-04 02:44:03 +02:00
Rafał Tyński 2676240440 test(backend): add embedding text and innovation stage tests 2026-10-04 02:32:33 +02:00
Rafał Tyński bad37a6ab9 feat(backend): add knowledge base, innovation library and trends modules
Add knowledge item and innovation CRUD with search/filtering, published-only visibility for non-staff, admin publish/update, challenge dictionary, problem aggregation with trends (admin only) and canonical embedding text preparation for matchmaking.
2026-10-04 02:32:33 +02:00
240 changed files with 10703 additions and 518 deletions
+9
View File
@@ -24,6 +24,15 @@ SurrealDb__DataDirectory=data
# LLM (F-8, matchmaking) — runtime secret only.
# LLM__ApiKey=
# LLM provider/cost control (optional; disabled by default -> offline template rationales).
# Llm__Enabled=false
# Llm__Provider=openai
# Llm__BaseUrl=https://api.openai.com/v1
# Llm__Model=gpt-4o-mini
# Llm__MaxTokens=256
# Llm__MaxRequestsPerMinute=30
# Llm__TimeoutSeconds=20
# Observability (optional).
# Logging__LogLevel__Default=Information
# OTEL_EXPORTER_OTLP_ENDPOINT=
+14 -4
View File
@@ -19,24 +19,34 @@ Platforma Małopolskiego Hubu Innowacji Społecznych (MVP hackathonu).
Windows nie kojarzy domyślnie plików `.ps1`, dlatego skrypty uruchamiaj przez
`pwsh` (np. `pwsh ./scripts/docker-push.ps1`), a nie przez dwuklik / samo
`./scripts/docker-push.ps1`. Instalacja: https://aka.ms/powershell
(PowerShell 7.6.6: https://github.com/powershell/powershell/releases#release-v7.6.6)
## Praca lokalna
Backend (dev) nasłuchuje na `https://localhost:7080`.
Backend (dev) domyślnie nasłuchuje na `http://localhost:5078` (profil `http`), a opcjonalnie na `https://localhost:7080` (profil `https`). Dev-serwer Vite działa po HTTPS na `https://localhost:7081` i przekierowuje `/api` do backendu (`http://localhost:5078`).
Certyfikat HTTPS dla Vite pochodzi z `dotnet dev-certs` — wygeneruj go raz (wymaga `pwsh`):
```pwsh
# terminal 1 — backend
pwsh ./scripts/dev-certs.ps1 # lub: npm --prefix client run certs
```
```pwsh
# terminal 1 — backend (profil http, port 5078)
dotnet run --project backend
# terminal 2 — client (Vite, proxy /api -> http://localhost:5078)
# terminal 2 — client (Vite, https://localhost:7081, proxy /api -> http://localhost:5078)
npm --prefix client install
npm --prefix client run dev
```
Aby użyć profilu `https` backendu (port 7080), uruchom `dotnet run --project backend --launch-profile https` i zmień `proxy.target` w `client/vite.config.ts` na `https://localhost:7080`. Domyślna konfiguracja nie wymaga żadnych zmiennych środowiskowych.
## Skrypty
```pwsh
npm --prefix client run dev # uruchamia Vite (dev server)
npm --prefix client run dev # uruchamia Vite (dev server, HTTPS :7081)
npm --prefix client run certs # eksport certyfikatu dev do client/.certs (dotnet dev-certs)
npm --prefix client run build # buduje aplikację Vite do client/dist
npm --prefix client run copy # kopiuje client/dist -> backend/wwwroot
node scripts/build.mjs # buduje całość (client + copy + backend)
+15 -2
View File
@@ -1,6 +1,6 @@
# AGENTS.md — Standardy warstwy Controller
# AGENTS.md — Założenia i standardy backendu (C#)
Dotyczy projektu `backend` (ASP.NET Core Web API, .NET 10).
Dotyczy projektu `backend` oraz testów `tests/TarninaHub.Tests` (ASP.NET Core Web API, .NET 10).
## Komunikaty commitów
@@ -8,6 +8,19 @@ Dotyczy projektu `backend` (ASP.NET Core Web API, .NET 10).
- Zasada obowiązuje **w obu projektach**: repozytorium `DevHeyah` oraz `20_projects/TarninaHub`.
- Stosujemy konwencję Conventional Commits (typ: `feat`, `fix`, `docs`, `chore` itd.).
## Założenia języka C#
- **Platforma:** .NET 10 (`net10.0`), `Nullable enable`, `ImplicitUsings enable`. Nie wyłączaj nullowalności; unikaj operatora `!` (jeśli musisz — uzasadnij).
- **Jeden typ = jeden plik:** każdy typ najwyższego poziomu (klasa, `record`, interfejs, `enum`, `struct`) w osobnym pliku, nazwa pliku zgodna z nazwą typu. Nie zagnieżdżaj typów pomocniczych poza przypadkami prywatnych szczegółów implementacji.
- **Przestrzeń nazw:** wyłącznie file-scoped (`namespace X;`). Kolejność: `using` → `namespace` → typy. Grupuj `using` bez zbędnych duplikatów.
- **Typy:** klasy usług i kontrolerów oznaczaj `sealed`; modele wierszy i typy wewnętrzne jako `internal`. DTO to `public sealed record` (niemutowalne, pozycyjne).
- **Zakaz map z wartościami `object`:** w DTO, interfejsach, `Domain` i modelach `*Row` nie stosuj `Dictionary<string, object>` ani `IReadOnlyDictionary<string, object>`. Elastyczne dane (`content`, `payload`, `contact`, `media`, `attachments`) to mapy wartości tekstowych (`IReadOnlyDictionary<string, string?>`), materializowane do konkretnych `Dictionary<string, string?>` przed przekazaniem do `RawQuery`. Powód: błąd deserializacji CBOR (Dahomey) i nieobsługiwana serializacja typów interfejsowych. Egzekwuje test `NoObjectValuedDictionariesTests`.
- **Wzorce:** promuj `record`, pattern matching, wyrażenia (`=>`), kolekcje `[]`; unikaj `null`-owych kolekcji (inicjalizuj puste).
- **Warstwy:** `Domain` (reguły, enumy), `Application` (DTO, abstrakcje, mapowania), `Infrastructure` (persistence, integracje), `Controllers` (tylko HTTP). Zależności kieruj do wewnątrz (Infrastructure → Application → Domain).
- **Nazewnictwo:** typy/metody/właściwości `PascalCase`, parametry i zmienne lokalne `camelCase`, pola prywatne `_camelCase`, interfejsy `I...`, typy generyczne `T...`. Metody asynchroniczne z sufiksem `Async`.
- **Formatowanie:** zgodnie z `.editorconfig` (C#: 4 spacje, LF, final newline, bez trailing whitespace). Weryfikacja: `dotnet format whitespace --verify-no-changes TarninaHub.slnx` i `dotnet format style --verify-no-changes --severity warn TarninaHub.slnx`.
- **Zmienne:** preferuj `var`, gdy typ jest oczywisty; jawne typy przy niejasności.
## Język nazewnictwa
- Wszystkie nazwy zmiennych, parametrów, pól, metod, klas i plików są **wyłącznie w języku angielskim**.
@@ -0,0 +1,18 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IAdminService
{
Task<IReadOnlyList<SubmissionResponse>> ListSubmissionsAsync(
string? status,
string? challengeId,
CancellationToken cancellationToken);
Task<KnowledgeItemResponse?> VerifyKnowledgeItemAsync(
string knowledgeItemId,
VerifyKnowledgeItemRequest request,
CancellationToken cancellationToken);
Task<AdminMetricsResponse> GetMetricsAsync(CancellationToken cancellationToken);
}
@@ -0,0 +1,12 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public enum CreateApplicationOutcome
{
Created,
CallNotFound,
CallNotOpen,
IdeaNotFound,
}
@@ -0,0 +1,8 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public sealed record CreateApplicationResult(
CreateApplicationOutcome Outcome,
ApplicationResponse? Application);
@@ -0,0 +1,18 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IApplicationService
{
Task<CreateApplicationResult> CreateAsync(
string userId,
CreateApplicationRequest request,
CancellationToken cancellationToken);
Task<ApplicationResponse?> GetAsync(string applicationId, CancellationToken cancellationToken);
Task<IReadOnlyList<ApplicationResponse>> ListForAuthorAsync(
string authorId,
CancellationToken cancellationToken);
}
@@ -6,6 +6,6 @@ public interface IAuditLogService
string? actorId,
string action,
string target,
IReadOnlyDictionary<string, object?> context,
IReadOnlyDictionary<string, string?> context,
CancellationToken cancellationToken);
}
@@ -0,0 +1,8 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IChallengeService
{
Task<IReadOnlyList<SocialChallengeResponse>> ListAsync(CancellationToken cancellationToken);
}
@@ -0,0 +1,16 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IGrantCallService
{
Task<IReadOnlyList<GrantCallResponse>> ListAsync(string? status, CancellationToken cancellationToken);
Task<GrantCallResponse?> GetAsync(string grantCallId, CancellationToken cancellationToken);
Task<GrantCallResponse?> UpdateStatusAsync(
string grantCallId,
string status,
CancellationToken cancellationToken);
}
@@ -0,0 +1,12 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IIdeaAssistant
{
Task<IdeaAssistResponse?> AssistAsync(
string ideaId,
IdeaAssistRequest request,
CancellationToken cancellationToken);
}
@@ -0,0 +1,21 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IIdeaService
{
Task<IdeaResponse> CreateAsync(string authorId, CreateIdeaRequest request, CancellationToken cancellationToken);
Task<IdeaResponse?> GetAsync(string ideaId, CancellationToken cancellationToken);
Task<IReadOnlyList<IdeaResponse>> ListForAuthorAsync(string authorId, CancellationToken cancellationToken);
Task<IReadOnlyList<string>> ListDistinctAuthorIdsAsync(CancellationToken cancellationToken);
Task<IdeaResponse?> UpdateAsync(string ideaId, UpdateIdeaRequest request, CancellationToken cancellationToken);
Task<IdeaCardResponse?> GetCardAsync(string ideaId, CancellationToken cancellationToken);
Task<IdeaCardResponse?> UpdateCardAsync(string ideaId, IdeaCardRequest request, CancellationToken cancellationToken);
}
@@ -0,0 +1,22 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IInnovationService
{
Task<IReadOnlyList<InnovationResponse>> ListAsync(
string? challengeId,
string? stage,
string? query,
bool onlyPublished,
CancellationToken cancellationToken);
Task<InnovationResponse?> GetAsync(string innovationId, CancellationToken cancellationToken);
Task<InnovationResponse> CreateAsync(InnovationRequest request, CancellationToken cancellationToken);
Task<InnovationResponse?> UpdateAsync(
string innovationId,
InnovationRequest request,
CancellationToken cancellationToken);
}
@@ -0,0 +1,24 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IKnowledgeItemService
{
Task<IReadOnlyList<KnowledgeItemResponse>> ListAsync(
string? kind,
string? challengeId,
string? query,
bool onlyPublished,
CancellationToken cancellationToken);
Task<KnowledgeItemResponse?> GetAsync(string knowledgeItemId, CancellationToken cancellationToken);
Task<KnowledgeItemResponse> CreateAsync(KnowledgeItemRequest request, CancellationToken cancellationToken);
Task<KnowledgeItemResponse?> UpdateAsync(
string knowledgeItemId,
KnowledgeItemRequest request,
CancellationToken cancellationToken);
Task<KnowledgeItemResponse?> PublishAsync(string knowledgeItemId, CancellationToken cancellationToken);
}
@@ -0,0 +1,19 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IMatchmakingService
{
Task IndexInnovationAsync(string innovationId, CancellationToken cancellationToken);
Task IndexProblemAsync(string problemId, CancellationToken cancellationToken);
Task IndexSubmissionAsync(string submissionId, CancellationToken cancellationToken);
Task<int> ReindexAsync(CancellationToken cancellationToken);
Task<MatchResponse?> MatchAsync(
string submissionId,
MatchRequest request,
CancellationToken cancellationToken);
}
@@ -0,0 +1,27 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface INotificationService
{
Task NotifyStaffAsync(
string type,
IReadOnlyDictionary<string, string?> payload,
CancellationToken cancellationToken);
Task NotifyUserAsync(
string userId,
string type,
IReadOnlyDictionary<string, string?> payload,
CancellationToken cancellationToken);
Task<IReadOnlyList<NotificationResponse>> ListForUserAsync(
string userId,
CancellationToken cancellationToken);
Task<NotificationResponse?> MarkReadAsync(
string notificationId,
string userId,
CancellationToken cancellationToken);
}
@@ -1,11 +1,5 @@
namespace TarninaHub.Application.Abstractions;
public sealed record SubmissionEvent(
string Type,
string SubmissionId,
string? ActorId,
string Status,
DateTimeOffset At);
public interface ISubmissionEventPublisher
{
@@ -0,0 +1,30 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IThreadService
{
Task<IReadOnlyList<ThreadResponse>> ListForUserAsync(
string userId,
bool isStaff,
CancellationToken cancellationToken);
Task<ThreadResponse?> GetAsync(
string threadId,
string userId,
bool isStaff,
CancellationToken cancellationToken);
Task<ThreadResponse> CreateAsync(
string creatorId,
CreateThreadRequest request,
CancellationToken cancellationToken);
Task<MessageResponse?> AddMessageAsync(
string threadId,
string authorId,
bool isStaff,
CreateMessageRequest request,
CancellationToken cancellationToken);
}
@@ -2,9 +2,8 @@ using TarninaHub.Application.Models;
namespace TarninaHub.Application.Abstractions;
public interface ITokenService
{
TokenResult CreateToken(UserAccount user);
}
public sealed record TokenResult(string AccessToken, DateTimeOffset ExpiresAt);
@@ -0,0 +1,12 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface ITrendService
{
Task<TrendsResponse> GetAsync(
string? challengeId,
DateTimeOffset from,
DateTimeOffset to,
CancellationToken cancellationToken);
}
@@ -0,0 +1,8 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IMiddlemanService
{
Task<MiddlemanResponse?> AdaptAsync(MiddlemanRequest request, CancellationToken cancellationToken);
}
@@ -0,0 +1,16 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface IPartnershipService
{
Task<IReadOnlyList<PartnershipResponse>> ListForUserAsync(
string userId,
bool isStaff,
CancellationToken cancellationToken);
Task<PartnershipResponse> CreateAsync(
string initiatorId,
CreatePartnershipRequest request,
CancellationToken cancellationToken);
}
@@ -0,0 +1,9 @@
namespace TarninaHub.Application.Abstractions;
public sealed record SubmissionEvent(
string Type,
string SubmissionId,
string? ActorId,
string Status,
DateTimeOffset At);
@@ -0,0 +1,16 @@
using TarninaHub.Application.Dtos;
namespace TarninaHub.Application.Abstractions;
public interface ITestFeedbackService
{
Task<TestFeedbackResponse?> CreateAsync(
string innovationId,
string authorId,
CreateTestFeedbackRequest request,
CancellationToken cancellationToken);
Task<IReadOnlyList<TestFeedbackResponse>> ListForInnovationAsync(
string innovationId,
CancellationToken cancellationToken);
}
@@ -0,0 +1,6 @@
using TarninaHub.Application.Models;
namespace TarninaHub.Application.Abstractions;
public sealed record TokenResult(string AccessToken, DateTimeOffset ExpiresAt);
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record AdminMetricsResponse(
int Submissions,
int Problems,
int Ideas,
int Innovations,
int KnowledgeItems,
int Users,
int OpenGrantCalls,
int Applications);
@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record ApplicationResponse(
string Id,
string GrantCallId,
string IdeaId,
IReadOnlyDictionary<string, string?> Content,
string Status,
DateTimeOffset CreatedAt);
-18
View File
@@ -1,18 +0,0 @@
namespace TarninaHub.Application.Dtos;
public sealed record RegisterRequest(string Phone, string? Email, string Password);
public sealed record LoginRequest(string Phone, string Password);
public sealed record UserResponse(
string Id,
string Phone,
string? Email,
string Role,
string? OrganizationId);
public sealed record TokenResponse(
string AccessToken,
string TokenType,
DateTimeOffset ExpiresAt,
UserResponse User);
@@ -1,6 +1,5 @@
namespace TarninaHub.Application.Dtos;
public sealed record GrantConsentRequest(string Scope, DateTimeOffset? ExpiresAt);
public sealed record ConsentResponse(
string Id,
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreateApplicationRequest(
[Required] string GrantCallId,
[Required] string IdeaId,
IReadOnlyDictionary<string, string?>? Content);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreateIdeaRequest(
[Required, StringLength(200, MinimumLength = 3)] string Title,
[Required, StringLength(4000, MinimumLength = 10)] string Description,
IdeaCardRequest? Card);
@@ -0,0 +1,7 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreateMessageRequest(
[Required, StringLength(4000, MinimumLength = 1)] string Body);
@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreatePartnershipRequest(
[Required, StringLength(200, MinimumLength = 3)] string Subject,
IReadOnlyList<string>? Partners);
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreateSubmissionRequest(
[Required, StringLength(200, MinimumLength = 3)] string Title,
[Required, StringLength(4000, MinimumLength = 10)] string Description,
[Required] string ChallengeId,
[Required] string LocationId,
IReadOnlyList<string>? Keywords);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreateTestFeedbackRequest(
[Range(1, 5)] int Rating,
string? Comment,
string? Improvements);
@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreateThreadRequest(
[Required, StringLength(200, MinimumLength = 3)] string Subject,
IReadOnlyList<string>? Participants);
@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record GrantCallResponse(
string Id,
string Name,
DateTimeOffset OpensAt,
DateTimeOffset ClosesAt,
string Status,
string? ExternalId);
@@ -0,0 +1,4 @@
namespace TarninaHub.Application.Dtos;
public sealed record GrantConsentRequest(string Scope, DateTimeOffset? ExpiresAt);
@@ -0,0 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record IdeaAssistRequest(string? Focus);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record IdeaAssistResponse(
IReadOnlyList<string> Suggestions,
string ImprovedDescription,
bool FromLlm);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record IdeaCardRequest(
string? Essence,
string? Audience,
string? Stage);
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record IdeaCardResponse(
string Id,
string IdeaId,
string Essence,
string Audience,
string Stage);
+13
View File
@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record IdeaResponse(
string Id,
string Title,
string Description,
string AuthorId,
string Status,
IdeaCardResponse? Card,
DateTimeOffset? UpdatedAt);
@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record InnovationRequest(
[Required, StringLength(200, MinimumLength = 3)] string Title,
[StringLength(2000)] string? Summary,
[StringLength(8000)] string? Description,
[Required] string ChallengeId,
[Required] string Stage,
string? Cost,
IReadOnlyDictionary<string, string?>? Contact,
IReadOnlyList<IReadOnlyDictionary<string, string?>>? Media,
bool Published);
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record InnovationResponse(
string Id,
string Title,
string Summary,
string Description,
string ChallengeId,
string Stage,
string? Cost,
IReadOnlyDictionary<string, string?>? Contact,
IReadOnlyList<IReadOnlyDictionary<string, string?>> Media,
bool Published,
DateTimeOffset? UpdatedAt);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record InstitutionProfile(
string? Type,
string? Area,
IReadOnlyList<string>? Audiences);
@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record KnowledgeItemRequest(
[Required, StringLength(200, MinimumLength = 3)] string Title,
[Required] string Kind,
string? ChallengeId,
string? SourceUrl,
IReadOnlyList<IReadOnlyDictionary<string, string?>>? Attachments,
bool Published);
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record KnowledgeItemResponse(
string Id,
string Title,
string Kind,
string? ChallengeId,
string? SourceUrl,
IReadOnlyList<IReadOnlyDictionary<string, string?>> Attachments,
bool Published,
DateTimeOffset? UpdatedAt);
+4
View File
@@ -0,0 +1,4 @@
namespace TarninaHub.Application.Dtos;
public sealed record LoginRequest(string Phone, string Password);
+6
View File
@@ -0,0 +1,6 @@
namespace TarninaHub.Application.Dtos;
public sealed record MatchRequest(
int? MaxResults,
string? ChallengeId);
@@ -0,0 +1,7 @@
namespace TarninaHub.Application.Dtos;
public sealed record MatchResponse(
IReadOnlyList<SimilarCaseItem> SimilarCases,
IReadOnlyList<SolutionItem> Solutions,
bool Fallback);
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record MessageResponse(
string Id,
string ThreadId,
string AuthorId,
string Body,
DateTimeOffset CreatedAt);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record MiddlemanRequest(
[Required] string InnovationId,
InstitutionProfile? InstitutionProfile,
string? Needs);
@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record MiddlemanResponse(
string ServiceForm,
IReadOnlyList<string> RequiredResources,
string Rationale,
bool FromLlm);
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record NotificationResponse(
string Id,
string Type,
IReadOnlyDictionary<string, string?> Payload,
DateTimeOffset? ReadAt,
DateTimeOffset CreatedAt);
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record PartnershipResponse(
string Id,
string InitiatorId,
string Subject,
IReadOnlyList<string> Partners,
string Status);
@@ -0,0 +1,4 @@
namespace TarninaHub.Application.Dtos;
public sealed record RegisterRequest(string Phone, string? Email, string Password);
@@ -0,0 +1,7 @@
namespace TarninaHub.Application.Dtos;
public sealed record SimilarCaseItem(
string Id,
string Title,
double Score);
@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record SocialChallengeResponse(
string Id,
string Name,
string Description,
int Order);
+10
View File
@@ -0,0 +1,10 @@
namespace TarninaHub.Application.Dtos;
public sealed record SolutionItem(
string InnovationId,
string Title,
string Stage,
string ChallengeId,
double Score,
string Rationale);
+8
View File
@@ -0,0 +1,8 @@
namespace TarninaHub.Application.Dtos;
public sealed record StorageInfo(
string Engine,
string DataDirectory,
string Namespace,
string Database);
-25
View File
@@ -1,25 +0,0 @@
namespace TarninaHub.Application.Dtos;
public sealed class StorageProbe
{
public string Label { get; set; } = "probe";
public DateTimeOffset CreatedAt { get; set; }
}
public sealed record StorageInfo(
string Engine,
string DataDirectory,
string Namespace,
string Database);
public sealed record StorageStatusResponse(
string Engine,
string DataDirectory,
string Namespace,
string Database,
int ProbeCount);
public sealed record StorageProbeResponse(
string Label,
DateTimeOffset CreatedAt);
+9
View File
@@ -0,0 +1,9 @@
namespace TarninaHub.Application.Dtos;
public sealed class StorageProbe
{
public string Label { get; set; } = "probe";
public DateTimeOffset CreatedAt { get; set; }
}
@@ -0,0 +1,6 @@
namespace TarninaHub.Application.Dtos;
public sealed record StorageProbeResponse(
string Label,
DateTimeOffset CreatedAt);
@@ -0,0 +1,9 @@
namespace TarninaHub.Application.Dtos;
public sealed record StorageStatusResponse(
string Engine,
string DataDirectory,
string Namespace,
string Database,
int ProbeCount);
@@ -2,15 +2,6 @@ using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record CreateSubmissionRequest(
[Required, StringLength(200, MinimumLength = 3)] string Title,
[Required, StringLength(4000, MinimumLength = 10)] string Description,
[Required] string ChallengeId,
[Required] string LocationId,
IReadOnlyList<string>? Keywords);
public sealed record UpdateSubmissionStatusRequest(
[Required] string Status);
public sealed record SubmissionResponse(
string Id,
@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record TestFeedbackResponse(
string Id,
string InnovationId,
string AuthorId,
int Rating,
string Comment,
string Improvements,
DateTimeOffset CreatedAt);
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record ThreadResponse(
string Id,
string Subject,
IReadOnlyList<string> Participants,
DateTimeOffset CreatedAt,
IReadOnlyList<MessageResponse> Messages);
@@ -0,0 +1,8 @@
namespace TarninaHub.Application.Dtos;
public sealed record TokenResponse(
string AccessToken,
string TokenType,
DateTimeOffset ExpiresAt,
UserResponse User);
+7
View File
@@ -0,0 +1,7 @@
namespace TarninaHub.Application.Dtos;
public sealed record TrendPoint(
string ChallengeId,
string ChallengeName,
int SubmissionCount);
@@ -0,0 +1,7 @@
namespace TarninaHub.Application.Dtos;
public sealed record TrendsResponse(
DateTimeOffset From,
DateTimeOffset To,
IReadOnlyList<TrendPoint> Points);
@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record UpdateApplicationRequest(
IReadOnlyDictionary<string, string?>? Content,
string? Status);
@@ -0,0 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record UpdateGrantCallStatusRequest([Required] string Status);
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record UpdateIdeaRequest(
[Required, StringLength(200, MinimumLength = 3)] string Title,
[Required, StringLength(4000, MinimumLength = 10)] string Description,
string? Status);
@@ -0,0 +1,7 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record UpdateSubmissionStatusRequest(
[Required] string Status);
+9
View File
@@ -0,0 +1,9 @@
namespace TarninaHub.Application.Dtos;
public sealed record UserResponse(
string Id,
string Phone,
string? Email,
string Role,
string? OrganizationId);
@@ -0,0 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace TarninaHub.Application.Dtos;
public sealed record VerifyKnowledgeItemRequest(bool Verified);
@@ -0,0 +1,26 @@
using TarninaHub.Domain.Enums;
namespace TarninaHub.Application.Mappings;
public static class IdeaStatusWire
{
public static string ToWire(this IdeaStatus status) => status switch
{
IdeaStatus.Draft => "draft",
IdeaStatus.Submitted => "submitted",
IdeaStatus.InReview => "in_review",
IdeaStatus.Accepted => "accepted",
IdeaStatus.Rejected => "rejected",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
};
public static IdeaStatus? Parse(string? value) => value switch
{
"draft" => IdeaStatus.Draft,
"submitted" => IdeaStatus.Submitted,
"in_review" => IdeaStatus.InReview,
"accepted" => IdeaStatus.Accepted,
"rejected" => IdeaStatus.Rejected,
_ => null,
};
}
@@ -0,0 +1,29 @@
using TarninaHub.Domain.Enums;
namespace TarninaHub.Application.Mappings;
public static class InnovationStageWire
{
private static readonly Dictionary<string, InnovationStage> ByWire = new(StringComparer.Ordinal)
{
["idea"] = InnovationStage.Idea,
["pilot"] = InnovationStage.Pilot,
["scaling"] = InnovationStage.Scaling,
["service"] = InnovationStage.Service,
};
public static bool IsValid(string? value) =>
!string.IsNullOrWhiteSpace(value) && ByWire.ContainsKey(value);
public static string ToWire(this InnovationStage stage) => stage switch
{
InnovationStage.Idea => "idea",
InnovationStage.Pilot => "pilot",
InnovationStage.Scaling => "scaling",
InnovationStage.Service => "service",
_ => throw new ArgumentOutOfRangeException(nameof(stage), stage, null),
};
public static InnovationStage? Parse(string? value) =>
value is not null && ByWire.TryGetValue(value, out var stage) ? stage : null;
}
@@ -0,0 +1,22 @@
using TarninaHub.Domain.Enums;
namespace TarninaHub.Application.Mappings;
public static class PartnershipStatusWire
{
public static string ToWire(this PartnershipStatus status) => status switch
{
PartnershipStatus.Proposed => "proposed",
PartnershipStatus.Active => "active",
PartnershipStatus.Closed => "closed",
_ => "proposed",
};
public static PartnershipStatus? Parse(string? value) => value?.Trim().ToLowerInvariant() switch
{
"proposed" => PartnershipStatus.Proposed,
"active" => PartnershipStatus.Active,
"closed" => PartnershipStatus.Closed,
_ => null,
};
}
@@ -0,0 +1,7 @@
namespace TarninaHub.Application.Matchmaking;
public static class EmbeddingConstants
{
/// <summary>Embedding dimension; must match the HNSW index definition.</summary>
public const int Dimension = 1536;
}
@@ -0,0 +1,103 @@
using System.Text;
namespace TarninaHub.Application.Matchmaking;
/// <summary>
/// Deterministic, offline embedding provider (feature hashing). Produces
/// L2-normalized vectors so cosine similarity reflects shared vocabulary.
/// Interchangeable with a real LLM provider behind <see cref="IEmbeddingProvider"/>
/// (see ADR-0005). Used as the default until an external provider is wired.
/// </summary>
public sealed class HashingEmbeddingProvider : IEmbeddingProvider
{
public int Dimension => EmbeddingConstants.Dimension;
public Task<float[]> EmbedAsync(string text, CancellationToken cancellationToken)
{
return Task.FromResult(Embed(text));
}
public Task<IReadOnlyList<float[]>> EmbedBatchAsync(
IReadOnlyList<string> texts,
CancellationToken cancellationToken)
{
IReadOnlyList<float[]> result = texts.Select(Embed).ToList();
return Task.FromResult(result);
}
public float[] Embed(string text)
{
var vector = new float[Dimension];
if (string.IsNullOrWhiteSpace(text))
{
return vector;
}
foreach (var token in Tokenize(text))
{
var index = (int)(Fnv1a(token) % Dimension);
vector[index] += 1f;
}
Normalize(vector);
return vector;
}
private static IEnumerable<string> Tokenize(string text)
{
var buffer = new StringBuilder();
foreach (var character in text)
{
if (char.IsLetterOrDigit(character))
{
buffer.Append(char.ToLowerInvariant(character));
}
else if (buffer.Length > 0)
{
yield return buffer.ToString();
buffer.Clear();
}
}
if (buffer.Length > 0)
{
yield return buffer.ToString();
}
}
private static void Normalize(float[] vector)
{
double sumOfSquares = vector.Sum(value => (double)value * value);
var magnitude = Math.Sqrt(sumOfSquares);
if (magnitude <= 0)
{
return;
}
for (var i = 0; i < vector.Length; i++)
{
vector[i] = (float)(vector[i] / magnitude);
}
}
private static uint Fnv1a(string token)
{
const uint offset = 2166136261;
const uint prime = 16777619;
var hash = offset;
foreach (var character in token)
{
hash ^= character;
hash *= prime;
}
return hash;
}
}
@@ -0,0 +1,12 @@
namespace TarninaHub.Application.Matchmaking;
public interface IEmbeddingProvider
{
int Dimension { get; }
Task<float[]> EmbedAsync(string text, CancellationToken cancellationToken);
Task<IReadOnlyList<float[]>> EmbedBatchAsync(
IReadOnlyList<string> texts,
CancellationToken cancellationToken);
}
@@ -0,0 +1,13 @@
namespace TarninaHub.Application.Matchmaking;
public interface ILlmProvider
{
/// <summary>True when a real provider is configured (API key etc.).</summary>
bool IsConfigured { get; }
/// <summary>Returns the completion text or null when unavailable.</summary>
Task<string?> CompleteAsync(
string systemPrompt,
string userPrompt,
CancellationToken cancellationToken);
}
@@ -0,0 +1,10 @@
namespace TarninaHub.Application.Matchmaking;
public interface ILlmRequestLimiter
{
/// <summary>
/// Tries to reserve one LLM request within the current window.
/// Returns false when the limit is exceeded (caller should fall back).
/// </summary>
bool TryAcquire();
}
@@ -0,0 +1,4 @@
namespace TarninaHub.Application.Matchmaking;
public sealed record LlmPrompt(string System, string User);
@@ -0,0 +1,32 @@
namespace TarninaHub.Application.Matchmaking;
public static class MatchPromptBuilder
{
private const string SystemPrompt =
"Jesteś asystentem Małopolskiego Hubu Innowacji Społecznych. " +
"Wyjaśnij po polsku, prostym językiem (maks. 2 zdania), dlaczego proponowana " +
"innowacja pasuje do zgłoszonej potrzeby. Nie wymyślaj faktów.";
public static LlmPrompt BuildExplanation(
string problemTitle,
string problemDescription,
string challengeId,
string innovationTitle,
string innovationSummary,
string stage,
double score)
{
var user =
$"Potrzeba: {problemTitle}\n" +
$"Opis: {problemDescription}\n" +
$"Obszar: {challengeId}\n" +
$"Proponowana innowacja: {innovationTitle}\n" +
$"Streszczenie: {innovationSummary}\n" +
$"Etap: {stage}\n" +
$"Ocena dopasowania: {score:0.00}\n" +
"Uzasadnij krótko dopasowanie.";
return new LlmPrompt(SystemPrompt, user);
}
}
@@ -0,0 +1,37 @@
namespace TarninaHub.Application.Matchmaking;
/// <summary>
/// Combines semantic similarity with context signals (area, location) and
/// availability into a final 0..1 match score.
/// </summary>
public static class MatchScoring
{
private const double ChallengeBoost = 0.10;
private const double LocationBoost = 0.05;
public static double Combine(
double semanticScore,
bool challengeMatch,
bool locationMatch,
bool available)
{
if (!available)
{
return 0;
}
var score = semanticScore;
if (challengeMatch)
{
score += ChallengeBoost;
}
if (locationMatch)
{
score += LocationBoost;
}
return Math.Clamp(score, 0, 1);
}
}
@@ -0,0 +1,15 @@
namespace TarninaHub.Application.Matchmaking;
/// <summary>
/// Default provider used when no external LLM is configured. Keeps the system
/// fully offline/deterministic; matchmaking falls back to template rationales.
/// </summary>
public sealed class NullLlmProvider : ILlmProvider
{
public bool IsConfigured => false;
public Task<string?> CompleteAsync(
string systemPrompt,
string userPrompt,
CancellationToken cancellationToken) => Task.FromResult<string?>(null);
}
@@ -0,0 +1,4 @@
namespace TarninaHub.Application.Matchmaking;
public sealed record RankedItem<T>(T Item, double Score);
@@ -0,0 +1,29 @@
namespace TarninaHub.Application.Matchmaking;
/// <summary>
/// Deterministic, offline explanation of "why this fits". Used as the default
/// and as a fallback when the LLM is unavailable or rate-limited.
/// </summary>
public static class RationaleBuilder
{
public static string Build(bool challengeMatch, bool locationMatch, double score)
{
var reasons = new List<string>();
if (challengeMatch)
{
reasons.Add("ten sam obszar problemowy");
}
if (locationMatch)
{
reasons.Add("zbliżona lokalizacja");
}
reasons.Add(score >= 0.3
? "wysoka zbieżność opisu potrzeby"
: "zbieżność semantyczna opisu");
return "Proponowane dopasowanie: " + string.Join(", ", reasons) + ".";
}
}
@@ -0,0 +1,19 @@
namespace TarninaHub.Application.Matchmaking;
public static class SemanticRanker
{
public static IReadOnlyList<RankedItem<T>> Rank<T>(
IReadOnlyList<float> queryVector,
IEnumerable<(T Item, IReadOnlyList<float> Vector)> candidates,
int topK,
double minScore = 0)
{
return candidates
.Select(candidate => new RankedItem<T>(candidate.Item, VectorMath.CosineSimilarity(queryVector, candidate.Vector)))
.Where(ranked => ranked.Score >= minScore)
.OrderByDescending(ranked => ranked.Score)
.Take(topK)
.ToList();
}
}
@@ -0,0 +1,30 @@
namespace TarninaHub.Application.Matchmaking;
public static class VectorMath
{
public static double CosineSimilarity(IReadOnlyList<float> a, IReadOnlyList<float> b)
{
if (a.Count == 0 || a.Count != b.Count)
{
return 0;
}
double dot = 0;
double magnitudeA = 0;
double magnitudeB = 0;
for (var i = 0; i < a.Count; i++)
{
dot += (double)a[i] * b[i];
magnitudeA += (double)a[i] * a[i];
magnitudeB += (double)b[i] * b[i];
}
if (magnitudeA <= 0 || magnitudeB <= 0)
{
return 0;
}
return dot / (Math.Sqrt(magnitudeA) * Math.Sqrt(magnitudeB));
}
}
@@ -0,0 +1,16 @@
namespace TarninaHub.Application.Security;
public static class ConsentScopes
{
/// <summary>Access granted by the data subject's consent.</summary>
public const string ByConsent = "by-consent";
/// <summary>Time-limited access (uses consent.expires_at).</summary>
public const string Temporary = "temporary";
/// <summary>Fully anonymized data (no personal identifiers).</summary>
public const string Anonymized = "anonymized";
public static readonly IReadOnlyList<string> All = [ByConsent, Temporary, Anonymized];
}
+20
View File
@@ -0,0 +1,20 @@
namespace TarninaHub.Application.Security;
public static class Policies
{
/// <summary>operator, manager, admin — ROPS/expert staff.</summary>
public const string Staff = "Staff";
/// <summary>operator, admin.</summary>
public const string OperatorOrAdmin = "OperatorOrAdmin";
/// <summary>manager, operator, admin.</summary>
public const string ManagerOrAbove = "ManagerOrAbove";
/// <summary>admin only.</summary>
public const string AdminOnly = "AdminOnly";
/// <summary>jst and above (jst, operator, manager, admin).</summary>
public const string JstOrAbove = "JstOrAbove";
}
+16
View File
@@ -0,0 +1,16 @@
namespace TarninaHub.Application.Security;
public static class RoleNames
{
public const string User = "user";
public const string Jst = "jst";
public const string Operator = "operator";
public const string Manager = "manager";
public const string Admin = "admin";
public static readonly IReadOnlyList<string> All =
[User, Jst, Operator, Manager, Admin];
public static bool IsValid(string role) => All.Contains(role);
}
@@ -1,47 +0,0 @@
namespace TarninaHub.Application.Security;
public static class RoleNames
{
public const string User = "user";
public const string Jst = "jst";
public const string Operator = "operator";
public const string Manager = "manager";
public const string Admin = "admin";
public static readonly IReadOnlyList<string> All =
[User, Jst, Operator, Manager, Admin];
public static bool IsValid(string role) => All.Contains(role);
}
public static class Policies
{
/// <summary>operator, manager, admin — ROPS/expert staff.</summary>
public const string Staff = "Staff";
/// <summary>operator, admin.</summary>
public const string OperatorOrAdmin = "OperatorOrAdmin";
/// <summary>manager, operator, admin.</summary>
public const string ManagerOrAbove = "ManagerOrAbove";
/// <summary>admin only.</summary>
public const string AdminOnly = "AdminOnly";
/// <summary>jst and above (jst, operator, manager, admin).</summary>
public const string JstOrAbove = "JstOrAbove";
}
public static class ConsentScopes
{
/// <summary>Access granted by the data subject's consent.</summary>
public const string ByConsent = "by-consent";
/// <summary>Time-limited access (uses consent.expires_at).</summary>
public const string Temporary = "temporary";
/// <summary>Fully anonymized data (no personal identifiers).</summary>
public const string Anonymized = "anonymized";
public static readonly IReadOnlyList<string> All = [ByConsent, Temporary, Anonymized];
}
@@ -0,0 +1,35 @@
namespace TarninaHub.Application.Text;
/// <summary>
/// Builds the canonical text used to compute an innovation embedding
/// (matchmaking, steps 14-15).
/// </summary>
public static class EmbeddingTextBuilder
{
public static string Build(string title, string? summary, string? description, string? challengeName = null)
{
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(title))
{
parts.Add(title.Trim());
}
if (!string.IsNullOrWhiteSpace(summary))
{
parts.Add(summary.Trim());
}
if (!string.IsNullOrWhiteSpace(description))
{
parts.Add(description.Trim());
}
if (!string.IsNullOrWhiteSpace(challengeName))
{
parts.Add(challengeName.Trim());
}
return string.Join("\n", parts);
}
}
+98
View File
@@ -0,0 +1,98 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TarninaHub.Application.Abstractions;
using TarninaHub.Application.Dtos;
using TarninaHub.Application.Security;
namespace TarninaHub.Controllers;
[ApiController]
[ApiVersion("1.0")]
[Route("api/admin")]
[Authorize(Policy = Policies.OperatorOrAdmin)]
public class AdminController : ControllerBase
{
private readonly IAdminService _admin;
private readonly IGrantCallService _grantCalls;
private readonly IIdeaService _ideas;
private readonly INotificationService _notifications;
public AdminController(
IAdminService admin,
IGrantCallService grantCalls,
IIdeaService ideas,
INotificationService notifications)
{
_admin = admin;
_grantCalls = grantCalls;
_ideas = ideas;
_notifications = notifications;
}
[HttpGet("submissions")]
public async Task<ActionResult<IReadOnlyList<SubmissionResponse>>> ListSubmissions(
[FromQuery] string? status,
[FromQuery] string? challengeId,
CancellationToken cancellationToken)
{
var submissions = await _admin.ListSubmissionsAsync(status, challengeId, cancellationToken);
return Ok(submissions);
}
[HttpPut("knowledge-items/{knowledgeItemId}/verify")]
public async Task<ActionResult<KnowledgeItemResponse>> VerifyKnowledgeItem(
string knowledgeItemId,
VerifyKnowledgeItemRequest request,
CancellationToken cancellationToken)
{
var item = await _admin.VerifyKnowledgeItemAsync(knowledgeItemId, request, cancellationToken);
return item is null ? NotFound() : Ok(item);
}
[HttpGet("metrics")]
public async Task<ActionResult<AdminMetricsResponse>> Metrics(CancellationToken cancellationToken)
{
var metrics = await _admin.GetMetricsAsync(cancellationToken);
return Ok(metrics);
}
[HttpPut("grant-calls/{grantCallId}/status")]
public async Task<ActionResult<GrantCallResponse>> UpdateGrantCallStatus(
string grantCallId,
UpdateGrantCallStatusRequest request,
CancellationToken cancellationToken)
{
var updated = await _grantCalls.UpdateStatusAsync(grantCallId, request.Status, cancellationToken);
if (updated is null)
{
return NotFound();
}
if (string.Equals(request.Status, "open", StringComparison.Ordinal))
{
await NotifyIdeaAuthorsAsync(updated, cancellationToken);
}
return Ok(updated);
}
private async Task NotifyIdeaAuthorsAsync(GrantCallResponse call, CancellationToken cancellationToken)
{
var authorIds = await _ideas.ListDistinctAuthorIdsAsync(cancellationToken);
var payload = new Dictionary<string, string?>
{
["grantCallId"] = call.Id,
["name"] = call.Name,
};
foreach (var authorId in authorIds)
{
await _notifications.NotifyUserAsync(authorId, "grant_call.opened", payload, cancellationToken);
}
}
}
@@ -0,0 +1,104 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TarninaHub.Application.Abstractions;
using TarninaHub.Application.Dtos;
using TarninaHub.Application.Security;
namespace TarninaHub.Controllers;
[ApiController]
[ApiVersion("1.0")]
[Route("api/applications")]
[Authorize]
public class ApplicationsController : ControllerBase
{
private readonly IApplicationService _applicationService;
private readonly IIdeaService _ideaService;
public ApplicationsController(IApplicationService applicationService, IIdeaService ideaService)
{
_applicationService = applicationService;
_ideaService = ideaService;
}
[HttpPost]
public async Task<ActionResult<ApplicationResponse>> Create(
CreateApplicationRequest request,
CancellationToken cancellationToken)
{
var userId = User.GetUserId();
if (string.IsNullOrWhiteSpace(userId))
{
return Unauthorized();
}
var result = await _applicationService.CreateAsync(userId, request, cancellationToken);
return result.Outcome switch
{
CreateApplicationOutcome.Created when result.Application is not null =>
CreatedAtAction(nameof(GetById), new { applicationId = result.Application.Id }, result.Application),
CreateApplicationOutcome.CallNotFound => NotFound(new ProblemDetails
{
Title = "Grant call not found",
Status = StatusCodes.Status404NotFound,
}),
CreateApplicationOutcome.IdeaNotFound => NotFound(new ProblemDetails
{
Title = "Idea not found",
Status = StatusCodes.Status404NotFound,
}),
_ => Conflict(new ProblemDetails
{
Title = "Grant call is not open",
Detail = "Applications are only accepted while the call is open and within its window.",
Status = StatusCodes.Status409Conflict,
}),
};
}
[HttpGet]
public async Task<ActionResult<IReadOnlyList<ApplicationResponse>>> ListMine(CancellationToken cancellationToken)
{
var userId = User.GetUserId();
if (string.IsNullOrWhiteSpace(userId))
{
return Unauthorized();
}
var applications = await _applicationService.ListForAuthorAsync(userId, cancellationToken);
return Ok(applications);
}
[HttpGet("{applicationId}")]
public async Task<ActionResult<ApplicationResponse>> GetById(
string applicationId,
CancellationToken cancellationToken)
{
var application = await _applicationService.GetAsync(applicationId, cancellationToken);
if (application is null)
{
return NotFound();
}
if (!IsStaff())
{
var idea = await _ideaService.GetAsync(application.IdeaId, cancellationToken);
if (idea is null || !string.Equals(idea.AuthorId, User.GetUserId(), StringComparison.Ordinal))
{
return Forbid();
}
}
return Ok(application);
}
private bool IsStaff() =>
User.IsInRole(RoleNames.Operator) || User.IsInRole(RoleNames.Manager) || User.IsInRole(RoleNames.Admin);
}

Some files were not shown because too many files have changed in this diff Show More