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 Gosi — BlobTextService.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— uruchamiaResourceMapLlmJobHandler(JobTyperesource-map-generate)GET /resource-map— odczyt aktualnej mapyPUT /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.copiedjuż 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 udanymSaveChanges, lock-protected.BlobCleanupSaveChangesInterceptor— EF CoreSaveChangesInterceptor, drenuje kolejkę wSavedChangesAsync, discarduje wSaveChangesFailedAsync. Symetryczne wsparcie sync (SavedChanges/SaveChangesFailed).
Zmiana zachowania
BlobTextService.SaveAsynczamiast_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.
ExecuteUpdateonly) → kolejka discarded → stary blob jako orphan (akceptowalne; lepsze niż data loss).
Edge case: ścieżki ExecuteUpdate (interceptor nie odpala)
LlmJobBackgroundService.ProcessAsync(14 handlerów LLM jobs: Mechanisms / Beck / Positive / Consultation / PatientInformation / Conceptualization* / AnalyzePatterns / ResourceMap / Summary / Abc / Annotations / Problems / ExtractTranscript) — pohandler.ExecuteAsyncOK →FlushBlobCleanupQueueAsync(drenuje + kasuje). Catch →DiscardPending().TranscriptPostProcessingService(pseudonimizacja) — konwersja na explicitSaveDeferredDeleteAsync. Po udanymExecuteUpdate(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
SaveAsyncjest safe-by-default — class-of-bug nie da się przypadkiem wprowadzić ponownie. - Pozostaje explicit
SaveDeferredDeleteAsyncdla precyzyjnej kontroli przyExecuteUpdate.
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 ABCCognitiveRestructuringHandler— PDF Restrukturyzacji poznawczejCognitiveDistortionsService— 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ę zformatPrice().payment.payAll— analogicznie.- Usunięto martwe klucze
publicProfile.perSessionipublicProfile.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/sessionsGET /api/patient/sessions/today-planGET /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ł:
EmailProbepreferujeemail.smtp.timeoutSecondszAppConfiguration(single source of truth zEmailService).- Fallback
HealthCheckOptions.Email.TimeoutMs10000 → 45000ms. DegradedLatencyMs5000 → 22500.
Iteracja 2: appsettings.json nadpisywał default
Pierwszy fix był częściowo nieskuteczny na DEV — appsettings.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.TimeoutMs10000 → 45000HealthChecks.Email.DegradedLatencyMs5000 → 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ć
SaveAsyncvsSaveDeferredDeleteAsync.
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ówpositive/*+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ąpionelogger.LogWarningze stack trace +sessionId+patientId. Bez tego ślepa diagnoza gdy któraś sesja zwraca błąd przy czytaniuadaptiveAbc.try/catchwokółJsonSerializer.Deserializew obu handlerach — przy niepoprawnym JSON od AI rzucaInvalidOperationExceptionz friendly message zamiast technicznego stack trace.
Frontend (UX bugs)
saveEdit()guardif(saving)w obu komponentach (Positive + ResourceMap) — szybki double-click nie wyśle 2 PUT-ów.callAction()guardif(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).useLlmJobuseEffect +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
| Obszar | Co przetestować | Spodziewane |
|---|---|---|
| Mapa zasobów | Włącz localStorage.admin_preview_key = 'PromptyAI', wygeneruj mapę z 2-3 adaptive ABCs | 4 kolumny kart z badge strength, edit mode dodaje/usuwa zasoby |
| Mapa zasobów — PDF | /resource-map/pdf po approve | PDF z zasobami w 4 kategoriach |
| Asystent Praktyki | Klik floating button (w kontekście pacjenta i poza nim) | Pełny prawy panel otwiera się od razu |
| Asystent Praktyki | Klik “Kopiuj” przy odpowiedzi AI | Toast “Skopiowano”, treść w schowku |
| Auto-stop nagrywania | Włącz nagrywanie >2h | Alert z opcją “Przedłuż +1h”, po 3 min bez reakcji → auto-stop + zapis + transkrypcja |
| SaveAsync safe-by-default | Wymuś błąd w SaveChanges po edycji wywiadu pacjenta | Stary blob zostaje, DB ref valid, brak “wywiad znika” |
| GetPatientConsultation logging | Sztucznie skoruptuj blob | App Insights pokazuje LogWarning z PatientId+BlobId+pozycja błędu, UI nie crashuje |
| Numeracja sesji PDF | Pacjent z 10 sesjami Completed + 5 Cancelled, wygeneruj PDF ABC | Numer w PDF = numer w UI (tylko Completed liczone) |
| Waluta (booking) | Terapeuta Currency=EUR, rezerwacja sesji | Email “Cena: 130.00 EUR” zamiast samej liczby |
| Waluta (cancelSession) | Terapeuta Currency=EUR, anulacja sesji z creditem | Modal “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=EUR | Email/SMS z kwotami EUR, nie zł |
| Granularność slotów | Zapisz public profile, sprawdź slotGranularityMinutes w DB | Wartość z TherapistAvailability nie zostaje nadpisana na 60 |
| Granularność slotów | Ustaw 5 lub 10 min, otwórz “Dodaj sesję” | Sekcja “Wolne terminy” ukryta, tylko custom time picker |
| EmailProbe | DEV banner SystemStatusBanner | Brak “Powiadomienia e-mail [down]” (timeoutSeconds=60s z DB, fallback 45s z appsettings.json) |
| MAUI iOS upload | iOS z aktywnym VPN lub Low Data Mode, nagraj sesję, wyślij | Upload startuje (brak Connectivity gate), failuje z czytelnym komunikatem jeśli sieć padła |
| MAUI banner degraded | Wymuś email down (PROD/DEV) | Banner w MAUI nie zapala się (tylko DB/Blob/OpenAI mają znaczenie) |
| MAUI pokój video | Otwórz sesję w MAUI | Brak bannera z linkiem do pokoju video; nagrywanie działa |
| Seed pacjentów testowych | Wywołaj seeder z błędnym emailem (unique constraint) | Transakcja roll-back, baza nie zostaje w połowicznym stanie |
Artykuł przygotowany przez zespół Therapy Support