Rozwój Produktu

Release Notes — 29 maja 2026

Release Notes — 29 maja 2026

Sprint dwa dni po release’ie 27 maja — krótki kalendarz, ale duża paczka. Centralnym tematem są dwie nowe rzeczy w warsztacie klinicznym: Mapa zasobów pacjenta jako nowa analiza w sekcji CBT (Strengths-Based taxonomy) oraz dojrzalszy Asystent Praktyki, który po kliknięciu floating button od razu otwiera pełny panel. Drugim nurtem jest architektoniczne posprzątanie po incydencie SCRUM-1621 GosiBlobTextService.SaveAsync jest teraz safe-by-default dzięki EF Core SaveChangesInterceptor i scoped cleanup queue (znika cała klasa bugu, ~60 callsitów naprawionych jednym ruchem). Trzeci — ujednolicenie numeracji sesji w PDF (SCRUM-1649): wcześniej PDF analiz pokazywał inny numer niż UI, bo liczył też sesje odwołane i NoShow. Czwarty — seria walutowych fixów w mailach, dashboardzie pacjenta i finance.reminder (terapeuci z Currency=EUR wreszcie widzą wszędzie poprawną walutę). Plus auto-stop nagrywania po 2h, hardening EmailProbe i porządki w MAUI.

💚 Podziękowania dla zespołu testowego: Martyny (SCRUM-1626, SCRUM-1649), Gosi (SCRUM-1621) oraz wszystkich, którzy zgłaszali walutowe dziwactwa i banner “Powiadomienia e-mail down” na DEV.


1. 🗺️ Mapa zasobów — nowa analiza CBT (SCRUM-1644)

Nowa pozycja w sidebarze ANALIZY CBT, obok Konceptualizacji pozytywnej. Mapa zasobów to strukturyzowany inwentarz mocnych stron pacjenta, generowany przez AI z wybranych adaptacyjnych ABCs (adaptiveAbc sub-property w AbcV2Json — ADR-0017 layered read).

Co dostaje terapeuta

Cztery kolumny z kartami zasobów, każda w jednej z kategorii:

  • Osobiste — cechy wewnętrzne (cierpliwość, ciekawość, dyscyplina)
  • Społeczne — relacje, sieć wsparcia, role
  • Behawioralne — strategie radzenia sobie, nawyki
  • Poznawcze — przekonania pomocne, schematy adaptacyjne

Każda karta to nazwa + opis + badge strength (rare / sometimes / frequent — hipoteza AI o częstotliwości użycia). Picker adaptive ABCs na górze pozwala wybrać konkretne sesje, z których ma być wygenerowana mapa.

Pełny workflow (parity z Beck/Positive)

  • POST /resource-map/generate-async — uruchamia ResourceMapLlmJobHandler (JobType resource-map-generate)
  • GET /resource-map — odczyt aktualnej mapy
  • PUT /resource-map — edycja (resetuje approval flags)
  • POST /resource-map/approve / revoke-approval — zatwierdzenie przez terapeutę
  • POST /resource-map/publish-to-patient — publikacja w portalu pacjenta (wymaga approve)
  • POST /resource-map/unpublish-from-patient — cofnięcie publikacji
  • PDF — reuse generic /ai-analysis/GENERATE_RESOURCE_MAP/pdf

Edit mode

W trybie edycji terapeuta może dodawać/usuwać zasoby, edytować nazwę i opis, zmieniać badge strength. Persystencja przez tabelę PatientInformation (bez migracji DB — feature wjeżdża transparentnie).

Tłumaczenia + gating

40 nowych kluczy therapyInfo.resourceMap.* w PL/EN (inne locale fallback). Feature gated przez localStorage.admin_preview_key === 'PromptyAI' — admin może włączyć dla konkretnych terapeutów testowych przed wyjściem na wszystkich.


2. 🤖 Asystent Praktyki — pełny panel od razu + copy odpowiedzi

Po feedbacku terapeutów uprościliśmy interakcję z Asystentem Praktyki — floating button po kliknięciu od razu otwiera prawy panel (slide-in), pomijając pośredni mały widget “Start conversation” z polem tekstowym, który dotąd zmuszał do dwukrotnego kliknięcia.

Zmiany w kontekście pacjenta

  • Kliknięcie floating button → setAiChatOpen(true) → prawy panel slide-in z pełnym chatem, gotowy do pisania.

Zmiany poza kontekstem pacjenta

  • GeneralAIChatWidget (np. na dashboardzie) — ta sama mechanika: click → pełny panel.
  • Przycisk “Kopiuj” / “Skopiowano” przy każdej odpowiedzi asystenta. Klucze common.copy / common.copied już istnieją w 7 lokalach.

Co to daje: szybsza interakcja (jeden klik mniej), łatwiejsze przenoszenie odpowiedzi AI do notatek terapeuty lub dokumentów zewnętrznych.


3. 🛑 Auto-stop nagrywania po 2h z grace period

Zabezpieczenie przed sytuacjami, w których terapeuta zapomniał wyłączyć nagrywanie po sesji (telefon w kieszeni, drugi pacjent, koniec dnia).

  • Po 2 godzinach nagrywania wyświetla alert z opcją przedłużenia o 1h.
  • Jeśli użytkownik nie zareaguje w ciągu 3 minut — nagrywanie kończy się automatycznie: zapis pliku + transkrypcja jak normalnie.
  • Wersja 1.7.18 (build 39).

Bez tego nagrywanie potrafiło lecieć w tle godzinami, generując ogromne pliki audio do transkrypcji i zwiększając ryzyko przekroczenia limitu pamięci urządzenia.


4. 🧱 SaveAsync safe-by-default — architektura po SCRUM-1621 (Gosia)

Audyt po incydencie Gosi (commit a82926e3 + 054711e6 z poprzednich release’ów) znalazł ~60 callsitów BlobTextService.SaveAsync z tą samą klasą buga: kasowanie starego bloba PRZED SaveChanges → przy awarii zapisu DB referencja zostaje na nieistniejący blob → “wywiad znika”. Zamiast naprawiać 60 miejsc po jednym, zmieniliśmy architekturę. Praca Bohdana, w pełni opisana w ADR-0029.

Nowe komponenty

  • IBlobCleanupQueue / BlobCleanupQueue (Scoped DI) — per-request kolejka blob IDs do skasowania PO udanym SaveChanges, lock-protected.
  • BlobCleanupSaveChangesInterceptor — EF Core SaveChangesInterceptor, drenuje kolejkę w SavedChangesAsync, discarduje w SaveChangesFailedAsync. Symetryczne wsparcie sync (SavedChanges / SaveChangesFailed).

Zmiana zachowania

  • BlobTextService.SaveAsync zamiast _blob.DeleteAsync(oldBlob) inline robi _cleanupQueue.EnqueueDelete(oldBlob).
  • SaveChanges OK → interceptor kasuje stary blob.
  • SaveChanges fail → interceptor discarduje kolejkę → stary blob zostaje, DB ref valid → brak “wywiad znika”.
  • Scope end bez SaveChanges (np. ExecuteUpdate only) → kolejka discarded → stary blob jako orphan (akceptowalne; lepsze niż data loss).

Edge case: ścieżki ExecuteUpdate (interceptor nie odpala)

  1. LlmJobBackgroundService.ProcessAsync (14 handlerów LLM jobs: Mechanisms / Beck / Positive / Consultation / PatientInformation / Conceptualization* / AnalyzePatterns / ResourceMap / Summary / Abc / Annotations / Problems / ExtractTranscript) — po handler.ExecuteAsync OK → FlushBlobCleanupQueueAsync (drenuje + kasuje). Catch → DiscardPending().
  2. TranscriptPostProcessingService (pseudonimizacja) — konwersja na explicit SaveDeferredDeleteAsync. Po udanym ExecuteUpdate (affected > 0) → manual delete oldBlob. affected == 0 (concurrent row change) → delete NEW blob + oldBlob zostaje.

Zysk

  • ~60 buggy callsitów (SessionsController, PatientsController, writers, background services) staje się safe bez zmian w kodzie.
  • Każdy nowy SaveAsync jest safe-by-default — class-of-bug nie da się przypadkiem wprowadzić ponownie.
  • Pozostaje explicit SaveDeferredDeleteAsync dla precyzyjnej kontroli przy ExecuteUpdate.

Defense-in-depth: logging JsonException w GetPatientConsultation

Druga część SCRUM-1621 — GetPatientConsultation wcześniej silent-catch JsonException i zwracał empty. Korupcja bloba mogła być niewidoczna godzinami. Teraz loguje: PatientId + content length + BlobId + JSON error position (line/col). Nie loguje treści (PII/Medical, RODO art. 9). UI dostaje safe defaults jak wcześniej (brak crashy).


5. 🔢 Numeracja sesji w PDF zgodna z UI (SCRUM-1649)

Zgłoszenie Martyny. Numer sesji widoczny w aplikacji (np. “Sesja #15”) nie zgadzał się z numerem drukowanym na PDF/portalu pacjenta (np. “#25”) — różnica brała się stąd, że handlery analiz liczyły wszystkie sesje pacjenta łącznie z Cancelled i NoShow, podczas gdy UI poprawnie te statusy wykluczało.

Jedno źródło prawdy

Wprowadziliśmy kanoniczny helper SessionNumberingQuery.WhereCountableForNumbering() — extension method IQueryable<Session> filtrujący StatusId != Cancelled && StatusId != NoShow. Używany teraz w:

  • SessionQueryService — lista sesji w UI (refaktor bez zmiany zachowania)
  • AbcHandler, AbcV2Handler — PDF Analizy ABC
  • CognitiveRestructuringHandler — PDF Restrukturyzacji poznawczej
  • CognitiveDistortionsService — wykresy zaburzeń poznawczych

Następna zmiana definicji “co liczyć do numeracji” — jedno miejsce w kodzie.


6. 🌍 Waluta dynamiczna w mailach, dashboardzie i przypomnieniach

Audyt walutowy po wykryciu, że terapeuta Jacek Wiaderny (Currency=EUR) wysyłał pacjentom maile i SMS-y z hardcoded " zł", a pacjenci widzieli kwoty w PLN na karcie “Nadchodząca sesja” w dashboardzie. Cztery niezależne fixy:

6.1 booking.confirmed + session.reminder — placeholder {currency}

Default szablon booking.confirmed (PL/EN email) renderował cenę jako gołą liczbę {price} bez waluty. NotificationOrchestrator w 3 metodach (SendBookingConfirmationAsync patient + therapist, SendSessionReminderAsync) dostał {currency} z therapistProfile.Currency (fallback "PLN"). NotificationTypeCatalog formalnie deklaruje price/currency/serviceName w Placeholders[]. Seedery PL+EN zaktualizowane ({price}{price} {currency}). Custom override’y terapeutów (np. Jacek) zachowują się jak przedtem — seeder ich nie nadpisuje.

6.2 Hardcoded PLN/zł usunięte z 4 miejsc UI

  • UpgradePromptModal — fallback 'zł'getCurrencySymbol(currency).
  • patients.cancelSession.creditWillBeAdded — usunięto " PLN" z 7 lokali. W SK/CA/LT było " EUR", w RU " руб.", w UK " грн" — wszystkie hardcoded mimo waluty terapeuty. {{amount}} już zawiera walutę z formatPrice().
  • payment.payAll — analogicznie.
  • Usunięto martwe klucze publicProfile.perSession i publicProfile.sessionPrice (×7 lokali) — nigdy nieużywane w kodzie.

6.3 Dashboard pacjenta — sesje API zwracają walutę

Frontend (PatientUpcomingSessions, PatientSessionPlan, PatientRecentSessions, PatientPaymentsSection) używał formatPrice(session.price, session.currency ?? 'PLN'), ale 3 endpointy backendu nie zwracały Currency. Naprawione:

  • GET /api/patient/sessions
  • GET /api/patient/sessions/today-plan
  • GET /api/therapist/patients/{id}/dashboard/sessions (podgląd terapeuty)
  • GET /api/sessions/patient/{id}/today-plan (podgląd terapeuty)

Wszystkie z fallback "PLN" gdy Currency jest null.

6.4 finance.reminder — hardcoded " zł"

TherapistFinanceController.BuildReminderContentAsync dopisywał " zł" do {unpaidAmount} i każdej pozycji {detailsList}. Waluta czytana teraz z therapistProfile.Currency (fallback "PLN").


7. ⏱️ Granularność slotów wraca do 60 + ukryj wolne terminy przy 5/10 min

Dwa powiązane bugi po wprowadzeniu konfigurowalnej granularności (release 27 maja).

7.1 PublicProfileSettings nadpisywał slotGranularityMinutes na 60

GET /api/therapist/profile/public nie zwracało slotGranularityMinutes. Frontend dostawał undefined, loadProfile ustawiał 60 jako fallback, a auto-save po 5s wysyłał PUT z pełnym profilem → backend nadpisywał DB. Granularność jest edytowana wyłącznie w TherapistAvailability, więc wycinamy to pole z payloadu PUT (zarówno autoSave jak i ręczny save).

7.2 AddSessionModal — “Wolne terminy” widoczne przy 5/10 min

Dla granularności 5 i 10 min sekcja “Wolne terminy na wybrany dzień” była nadal widoczna obok custom time pickera — kompletny szum wizualny. Dodano warunek gran > 10 (analogicznie do SessionForm.tsx).


8. 📧 EmailProbe — czyta email.smtp.timeoutSeconds z DB

Banner SystemStatusBanner na DEV stale pokazywał “Powiadomienia e-mail [down]” mimo że realna wysyłka maili działała. Powód: EmailProbe miał hardcoded TimeoutMs=10000 w HealthCheckOptions, co dawało TaskCanceledException przy probowaniu wn03.webd.pl:465 z Azure Container Apps West Europe — SMTP handshake cross-internet do polskiego hostingu nie mieścił się w 10s. Tymczasem realna wysyłka w EmailService używała email.smtp.timeoutSeconds=60s z DB i działała.

Iteracja 1: zmiana w C# default

Pierwszy commit (1797b4cb) zmienił:

  • EmailProbe preferuje email.smtp.timeoutSeconds z AppConfiguration (single source of truth z EmailService).
  • Fallback HealthCheckOptions.Email.TimeoutMs 10000 → 45000ms.
  • DegradedLatencyMs 5000 → 22500.

Iteracja 2: appsettings.json nadpisywał default

Pierwszy fix był częściowo nieskuteczny na DEVappsettings.json ma sekcję HealthChecks.Email z TimeoutMs=10000, która override’uje C# default przez IOptions binding. DependencyHealthBackgroundService.TickAsync ustawiał outer CancellationToken na probeOpts.TimeoutMs + 1000 = 11s → ucinał probe zanim wewnętrzny CTS z DB (60s) miał szansę zadziałać.

Drugi commit (c7c743f3) zaktualizował appsettings.json:

  • HealthChecks.Email.TimeoutMs 10000 → 45000
  • HealthChecks.Email.DegradedLatencyMs 5000 → 22500

PROD już chodził (latency 611ms < 11s, więc deadline nie ucinał). DEV potrzebował tej zmiany żeby probe miał szansę dokończyć handshake przez SslOnConnect do wn03.webd.pl.


9. 📱 MAUI — porządki w aplikacji mobilnej

Trzy niezależne fixy w aplikacji mobilnej dla terapeutów (wersje 1.7.15 / 1.7.16 / 1.7.17).

9.1 Connectivity gate blokował upload na iOS (1.7.17)

iOS Connectivity.NetworkAccess zwraca ConstrainedInternet / Unknown przy VPN i Low Data Mode — upload nie startował mimo działającej sieci. Usunięto gate; upload próbuje naturalnie i failuje w try/catch z czytelnym komunikatem.

9.2 Banner “degraded” przy samym email down (1.7.16)

Krytyczne dla nagrywania to DB, Blob, OpenAI. Email down nie wpływa na zapis sesji ani transkrypcję, ale powodował false alarm “Niektóre usługi działają w trybie ograniczonym” w MAUI. Teraz email down → banner się nie pokazuje.

9.3 Usunięcie obsługi pokoju video (1.7.15)

Aplikacja mobilna nie obsługuje już pokoju video — telefon służy tylko do nagrywania. Usunięto banner z linkiem do pokoju, komendę OpenMeetingInBrowser oraz pola MeetingUrl / MeetingLink / VideoInviteToken z SessionDto. Zachowano MeetingType, Location, IsOnlineSession do logiki nagrywania (rozpoznanie typu sesji).


10. 🌱 Seed pacjentów testowych w transakcji (SCRUM-1626)

Zgłoszenie Martyny. Endpoint seedera pacjentów testowych mógł zostawić bazę w połowicznym stanie, jeśli któryś z INSERT-ów padł w trakcie (np. naruszenie unique constraint na email). Naprawione przez owinięcie całości w IDbContextTransaction z explicit CommitAsync / RollbackAsync. Częściowy seed nie zatruje już bazy testowej.


11. 📚 ADR-0029 — dokumentacja decyzji architektonicznej

Architektoniczna decyzja udokumentowana po SCRUM-1621. ADR-0029 opisuje:

  • Kontekst incydentu Gosi (2026-05-26).
  • Audyt 60 callsitów BlobTextService.SaveAsync.
  • Decyzję o BlobCleanupQueue + SaveChangesInterceptor.
  • Edge cases: ExecuteUpdate handling, orphan blobs.
  • Powiązania z ADR-0018 (Always Encrypted) i ADR-0025.
  • Instrukcja dla agentów AI: kiedy używać SaveAsync vs SaveDeferredDeleteAsync.

12. 🛠️ Code review SCRUM-1643/1644 — audit + logging + UX + i18n

Po code review na Mapie zasobów i Konceptualizacji pozytywnej Phase 2b znaleziono i naprawiono:

Backend (compliance + observability)

  • [AuditPatientAccess] dodane do 11 write endpointów positive/* + resource-map/*. Poprzednio audyt obejmował tylko GET-y — write’y były nieaudytowane (naruszenie invariantu z CLAUDE.md: zmiany danych pacjenta → DataAccessAudit).
  • catch { } w handlerach (PositiveConceptualization, ResourceMap) zastąpione logger.LogWarning ze stack trace + sessionId + patientId. Bez tego ślepa diagnoza gdy któraś sesja zwraca błąd przy czytaniu adaptiveAbc.
  • try/catch wokół JsonSerializer.Deserialize w obu handlerach — przy niepoprawnym JSON od AI rzuca InvalidOperationException z friendly message zamiast technicznego stack trace.

Frontend (UX bugs)

  • saveEdit() guard if(saving) w obu komponentach (Positive + ResourceMap) — szybki double-click nie wyśle 2 PUT-ów.
  • callAction() guard if(busyAction !== null) — kliknięcie “Approve” → szybko “Publish” wyśle dopiero po zakończeniu pierwszej (Publish wymaga approve w DB, więc race condition mógł zwrócić 400).
  • useLlmJob useEffect + useRef lastHandledJobIdRef — toast “wygenerowane” nie pojawi się 2× przy StrictMode / re-renderze.

i18n

Dodane 60 brakujących kluczy (positiveConceptualization Phase 2b + cały resourceMap + therapyInfo.tabs.resourceMap) w 6 lokalach: SK, CA, RU, UK, LT, FR (PL/EN już miały). Natywne tłumaczenia, nie automatyczne (ważne dla terapeutycznej terminologii).

Świadomy odkład

Pseudonimizacja imienia pacjenta przekazywanego do LLM context — istnieje też w Beck handler (pre-existing pattern). Odkładamy na osobny ticket bo dotyczy 4 handlerów łącznie i to nie jest regresja.


13. 🔄 Infrastruktura — sync wersji MAUI

Cztery automatyczne commity chore: sync MobileVersion.Latest (1.7.15 → 1.7.18) z workflow synchronizującym wersję mobilnej aplikacji wstrzykiwaną do bannera “Dostępna nowsza wersja” w SystemStatusBanner. Brak manualnej akcji.


✅ QA Checklist

ObszarCo przetestowaćSpodziewane
Mapa zasobówWłącz localStorage.admin_preview_key = 'PromptyAI', wygeneruj mapę z 2-3 adaptive ABCs4 kolumny kart z badge strength, edit mode dodaje/usuwa zasoby
Mapa zasobów — PDF/resource-map/pdf po approvePDF z zasobami w 4 kategoriach
Asystent PraktykiKlik floating button (w kontekście pacjenta i poza nim)Pełny prawy panel otwiera się od razu
Asystent PraktykiKlik “Kopiuj” przy odpowiedzi AIToast “Skopiowano”, treść w schowku
Auto-stop nagrywaniaWłącz nagrywanie >2hAlert z opcją “Przedłuż +1h”, po 3 min bez reakcji → auto-stop + zapis + transkrypcja
SaveAsync safe-by-defaultWymuś błąd w SaveChanges po edycji wywiadu pacjentaStary blob zostaje, DB ref valid, brak “wywiad znika”
GetPatientConsultation loggingSztucznie skoruptuj blobApp Insights pokazuje LogWarning z PatientId+BlobId+pozycja błędu, UI nie crashuje
Numeracja sesji PDFPacjent z 10 sesjami Completed + 5 Cancelled, wygeneruj PDF ABCNumer w PDF = numer w UI (tylko Completed liczone)
Waluta (booking)Terapeuta Currency=EUR, rezerwacja sesjiEmail “Cena: 130.00 EUR” zamiast samej liczby
Waluta (cancelSession)Terapeuta Currency=EUR, anulacja sesji z creditemModal “creditWillBeAdded” pokazuje EUR bez duplikatu waluty
Waluta (dashboard pacjenta)Pacjent terapeuty Currency=EUR, karta “Nadchodząca sesja”Badge ceny w EUR (nie zł)
Waluta (finance.reminder)Wyślij przypomnienie pacjentowi terapeuty Currency=EUREmail/SMS z kwotami EUR, nie zł
Granularność slotówZapisz public profile, sprawdź slotGranularityMinutes w DBWartość z TherapistAvailability nie zostaje nadpisana na 60
Granularność slotówUstaw 5 lub 10 min, otwórz “Dodaj sesję”Sekcja “Wolne terminy” ukryta, tylko custom time picker
EmailProbeDEV banner SystemStatusBannerBrak “Powiadomienia e-mail [down]” (timeoutSeconds=60s z DB, fallback 45s z appsettings.json)
MAUI iOS uploadiOS z aktywnym VPN lub Low Data Mode, nagraj sesję, wyślijUpload startuje (brak Connectivity gate), failuje z czytelnym komunikatem jeśli sieć padła
MAUI banner degradedWymuś email down (PROD/DEV)Banner w MAUI nie zapala się (tylko DB/Blob/OpenAI mają znaczenie)
MAUI pokój videoOtwórz sesję w MAUIBrak bannera z linkiem do pokoju video; nagrywanie działa
Seed pacjentów testowychWywołaj seeder z błędnym emailem (unique constraint)Transakcja roll-back, baza nie zostaje w połowicznym stanie

Artykuł przygotowany przez zespół Therapy Support

Program feedbackowy · Dołącz teraz

Odzyskaj czas dla siebie
i swoich pacjentów

Jesteś terapeutą / terapeutką CBT?
Sprawdź, jak platforma wspiera Twoją codzienną pracę.
Podsumowania sesji, które porządkują materiał kliniczny. Administracja, która nie przeszkadza.