Rozwój Produktu

Release Notes — 14 maja 2026

Release Notes — 14 maja 2026

Bardzo intensywny dzień — 32 commity od 03:21 do 22:45. Centralna zmiana to release notes w dzwoneczku z auto-broadcastem (ironicznie: właśnie ten release, który czytasz, jest pierwszym, który aktywuje nową belkę “Co nowego” w aplikacji). Drugi duży blok to dokończenie szyfrowania AE dla adnotacji CBT (SCRUM-1504) i resurrect-fix dla transkrypcji z wolnych łączy (SCRUM-1515, zgłoszone z PROD). Na deser — sticky-note przeniesiona z localStorage do bazy, MAUI version check i fundamenty pod wielowalutowość, którą Jacek Wiaderny dokończy 16 maja.

💚 Podziękowania: Bohdan (większość ticketów), Bartek (SCRUM-1510 read fix), Jacek W. (podwaliny EUR), oraz kontakt@posithink.pl za zgłoszenie SCRUM-1515 z konkretnymi Id sesji — bez tego nie wykrylibyśmy race’u 13–23 min.


1. 🔔 Release notes w dzwoneczku + auto-broadcast po deployu

Pierwszy raz wypuszczamy mechanizm informowania o releaseach w samej aplikacji. Wcześniej terapeuci dowiadywali się o zmianach z bloga albo od nas bezpośrednio — teraz po każdym mergu do main ikona dzwoneczka w panelu zapala się z nową kategorią Sparkles (“Co nowego”), a kliknięcie otwiera modal z 3–5 najważniejszymi zmianami i linkiem “Czytaj pełny artykuł”.

  • Backend: AdminReleaseNotesController z POST /api/admin/release-notes/announce. Idempotentny per data (Link = release-notes:{slug}) — kolejne uruchomienia tej samej daty są no-opem
  • Auth bez sekretu: GitHub OIDC — workflow .github/workflows/announce-release-notes.yml żąda krótkiego ID tokena dla audience aits-react-release-notes; API waliduje JWT przeciw JWKS GitHuba i sprawdza repository == Aithentica/AITS-React. Fallback: rola Administrator (do manualnych smoke-testów)
  • Broadcast: NotificationService rozsyła UserNotification typu release_notes do wszystkich aktywnych terapeutów i administratorów — DB + SignalR (live update bez F5)
  • Frontend: ReleaseNotesModal fetchuje markdown z /release-notes/{slug}.md, parsuje frontmatter (date, title, blogUrl), renderuje treść przez react-markdown. Kliknięcie w dzwoneczku nie nawiguje — otwiera modal
  • Treść TL;DR w repo AITS-React/client/public/release-notes/{YYYY-MM-DD}.md — 3–5 punktów + link do pełnego artykułu na blogu (na który właśnie patrzysz)
  • i18n: 5 nowych kluczy releaseNotes.* w 8 językach (pl/en/sk/ca/ru/uk/lt/fr)
  • Workflow trigger: push do main zmieniający client/public/release-notes/** automatycznie wywołuje endpoint. Manualny workflow_dispatch z wyborem dev/prod jako fallback

Praca Bohdana. Skill /release-notes dostał Step 6 — po wygenerowaniu artykułu na bloga skill zapisuje też TL;DR do AITS-React.

Uwaga retrospektywna: ten artykuł jest postem korekcyjnym — pierwotnie, 14 maja, modal w aplikacji działał, ale przycisk “Czytaj pełny artykuł” prowadził do 404. Tworząc dzwoneczek skupiliśmy się na infrastrukturze i TL;DR, ale zapomnieliśmy o pełnym artykule blogowym — co ironicznie udowadnia, dlaczego sam Step 6 skilla /release-notes jest potrzebny.

2. 🔒 SCRUM-1504 — Always Encrypted dla adnotacji CBT (Faza 1 + Faza 2)

Druga warstwa porządkowania danych pacjentów zgodnie z ADR-0018. Tabela SessionTranscriptionAnnotation (adnotacje CBT do transkrypcji — myśl, cytat, komentarz terapeuty) przechowywała QuotedText i Comment jako plain nvarchar. Po tej zmianie obie kolumny są AE Randomized (CEK_Randomized, AEAD_AES_256_CBC_HMAC_SHA_256).

  • Faza 1 (expand) — dodane kolumny QuotedText_AE (nvarchar(2000)) i Comment_AE (nvarchar(1000)) jako Always Encrypted; backfill kopiujący wartości z plain do AE działa stopniowo
  • Faza 1 follow-upSqlParameter.Size explicite ustawiony podczas backfillu (bez tego sterownik wysyłał nvarchar(MAX) co przy dużych zbiorach było wolne)
  • Faza 2 (contract) — po backfillu 757 wierszy na DEV: DROP starych plain kolumn, sp_rename _AE → docelowe. Kolumny QuotedText i Comment to teraz AE Randomized
  • Subtelność EF Core 8: AppDbContextModelSnapshot.cs nie wymagał zmiany — EF nie śledzi AE w snapshocie, logiczny typ (nvarchar(2000) IsRequired) bez zmian
  • Diagnostyka: jeśli między deploy Phase 1 a Phase 2 powstał nowy wiersz z wartością plain i _AE = NULL, jego plain wartość zostaje utracona — adnotacje są regeneralne przez CbtAnnotationService.GenerateAnnotationsAsync (akceptowalne)
  • Tech debt linkowane: docs/ddd-inconsistencies-report.md → SCRUM-1514 (przegląd pozostałych plain text kolumn z danymi terapeutycznymi)

Praca Bohdana. Tickety: SCRUM-1504 (Faza 1+2), SCRUM-1514 (follow-up tech debt).

3. 💾 SCRUM-1515 — resurrect Failed transcriptions przy wolnym łączu

Klasyczny race wykryty z PROD-a. Klient MAUI używa exponential backoff retry dla uploadu audio (1+2+4+8+16+32+64+128s ≈ 4 min 15s przy 8 retry). Dla plików 100 MB+ na słabym 4G/5G blob dociera 13–23 minuty po CreatedAt. Tymczasem CleanupOrphanedTranscriptionsAsync markowała rekord jako Failed po 10 minutach, a ProcessStuckUploadsAsync szukał tylko Status = Uploading. Efekt: blob siedział w storage bez właściciela, transkrypcja nigdy się nie uruchamiała.

Dowody PROD (zgłoszenie kontakt@posithink.pl, 13.05.2026):

  • Id 3356, sesja 21271 — blob lastModified +14 min po CreatedAt
  • Id 3393, sesja 21275 — blob +23 min po CreatedAt
  • 5 takich sesji odzyskanych ręcznie 14.05 przez Failed → Uploading SQL flip

Co naprawiono:

  • ProcessStuckUploadsAsync filter rozszerzony o Status = Failed z dokładnym ErrorMessage "Plik audio nie został przesłany w wyznaczonym czasie." + BatchJobId = null + ProcessingCompletedAt z ostatnich 2h (limit chroni przed wskrzeszaniem stałych failures bez bloba)
  • Atomic claim (Uploading → Processing) OR (Failed → Processing) z wyzerowaniem ErrorMessage i ProcessingCompletedAt
  • LogInformation "SCRUM-1515 resurrect" gdy late-arrival wskakuje do pipeline (do monitoringu w Azure App Insights)
  • NeedsReupload SignalR ograniczony do Status = Uploading — Failed-resurrect candidates dostali ten event już wcześniej, powtarzanie spamowałoby klienty
  • CleanupOrphanedTranscriptionsAsync uploadingCutoff 10 → 20 min — szansa na dotarcie z naprawdę wolnego łącza

Pozostałe gaps po stronie MAUI (queue-before-upload atomicity, TryReconcileAsync na pierwszy fail, persistent ClientUploadId) zostały wydzielone do osobnych ticketów: SCRUM-1516 i SCRUM-1517 (zaadresowane już w release 16.05).

Praca Bohdana.

4. 📝 Sticky-note terapeuty w bazie (migracja z localStorage)

Dashboard ma żółtą karteczkę (“sticky-note”) obok kalendarza, gdzie terapeuta zapisuje robocze notatki na dany dzień. Wcześniej trzymaliśmy ją wyłącznie w localStorage przeglądarki — przez co nie dało się policzyć ilu userów z funkcji korzysta, nie syncowała się między urządzeniami i ginęła przy wyczyszczeniu cache.

Backend:

  • TherapistProfile.StickyNote (nvarchar(4000)) jako Always Encrypted (RANDOMIZED, CEK_Randomized) — zgodnie z ADR-0018 (kontekst pacjentów: treść notatki potencjalnie zawiera dane wrażliwe)
  • Migracja 20260514150000_AddStickyNoteToTherapistProfile.cs defensywna (IF NOT EXISTS, Up + Down)
  • TherapistProfileController: GET + PUT /api/therapist/profile/sticky-note (auth: IsTherapistOrAdmin)

Frontend (Dashboard.tsx):

  • useEffect przy mount: pobiera z backendu. Jeśli backend pusty + localStorage ma dane → migracja jednorazowa do backendu + czyszczenie localStorage. Jeśli backend ma dane → ignoruje localStorage, używa backendu (źródło prawdy)
  • Save z 2s debounce + onBlur — teraz idzie PUT zamiast localStorage.setItem
  • stickyNoteSyncedRef: nie wysyłamy PUT jeśli wartość się nie zmieniła (oszczędność zapisów)

Po deployu: SELECT COUNT(*) FROM TherapistProfile WHERE StickyNote IS NOT NULL da liczbę aktywnych użytkowników funkcji — pierwsza metryka jakiej nie mieliśmy.

Praca Bohdana.

5. 📱 MAUI version check + auto-sync workflow

Aplikacja mobilna sprawdza teraz przy starcie, czy jest aktualna. Jeśli nie — pokazuje dismissable alert (“Aktualizuj” → otwiera Store, “Później” → kontynuuj).

  • Backend: GET /api/mobile/version (AllowAnonymous) zwraca {android, ios}.{latest, minSupported, storeUrl} z MobileVersion w appsettings.json
  • MAUI: IVersionCheckService w BootSplashViewModel po health-check porównuje AppInfo.Current.VersionString z latest i decyduje czy pokazać dialog. Każdy błąd = silent skip — wersja nigdy nie blokuje startu
  • iOS guard: dopóki apka nie jest publicznie w App Store i storeUrl to placeholder id0000000000, dialog na iOS jest pomijany (żeby nie otwierać martwego linku)
  • Workflow maui-sync-version.yml: trigger na push zmieniający maui/AITS.Mobile/AITS.Mobile.csproj, czyta ApplicationDisplayVersion i auto-commituje bump MobileVersion.{Android,Ios}.Latest w server/AITS.Api/appsettings.json. Po auto-commicie explicite triggeruje deploy API
  • Bump 1.7.8/28 → 1.7.9/29 — drobne fixy z MyUploads perf

Praca Bohdana.

6. 💱 Wielowalutowość — fundamenty pod EUR (sprint Jacka W.)

Pierwsza odsłona wielowalutowości — wsparcie EUR dla subskrypcji + walidacja kodu pocztowego per kraj. Pełna wielowalutowość dla cennika sesji terapeuty (PLN/EUR/USD/GBP/AUD/RUB/UAH) wjedzie 16.05; tu kładziemy fundamenty dla subskrypcji.

Skąd potrzeba: holenderski terapeuta nie mógł kupić rocznego abonamentu — checkout był zamknięty w PLN, a walidacja postcode wymuszała format XX-XXX.

DB:

  • Nowa kolumna Currency (ISO 4217) w SubscriptionType
  • 6 wierszy EUR (starter/standard/professional × monthly/yearly) sklonowanych z PLN z nadpisanym StripePriceId
  • Unique index na (Code, DurationDays, Currency) — wsparcie pod kolejne waluty bez kolizji
  • Migracja idempotentna, defensywna, features kopiowane z PLN

Stripe:

  • 6 nowych Price (EUR) na istniejącym produkcie prod_TXlr7rBrxH8jTx
  • Kwoty: Starter 59/590, Standard 89/890, Professional 119/1190 EUR (miesięczne/roczne)
  • Price IDs w Key Vault aits-kv-{prod,dev-new,sandbox-new} jako Stripe--PriceIds--{Plan}{Period}EUR

Backend:

  • SubscriptionPlanInfo ma teraz Currency
  • GetAvailablePlansAsync zwraca wszystkie aktywne plany (PLN + EUR), frontend filtruje client-side

Frontend (PricingPage):

  • Toggle PLN/EUR w nagłówku, persisted w localStorage
  • Auto-detect po i18n.language (pl → PLN, inne → EUR)
  • Filtrowanie planów + formatowanie ceny per waluta (pl-PL / de-DE locale)
  • Fallback do PLN gdy backend jeszcze nie zwraca currency
  • /pricing pokazuje wszystkie plany Start/Practice/Pro (wcześniej był limit)
  • Currency switcher finalnie zaprezentowany jako kursywowy link w rzędzie trust badges (obok “Zabezpieczone przez Stripe”) — mniej krzykliwy niż toggle, zachowuje gęstość strony pricing

Frontend (TherapistProfileManagement):

  • Walidacja kodu pocztowego per taxRegisteredCountry:
    • PL: XX-XXX
    • NL: 1234 AB
    • DE: XXXXX
    • inne: luźna walidacja 2-12 znaków
  • Naprawia osobny bug Jacka — holenderski code zostaje zaakceptowany

i18n: 4 nowe klucze × 8 lokalizacji.

Praca Jacka Wiadernego — 6 commitów w jeden dzień przygotowuje grunt pod release 16.05 (pełna wielowalutowość dla terapeutów).

7. 🎨 Dashboard polish — TEN TYDZIEŃ, układ, NavBar

Drugi sprint kosmetyki Dashboardu (po podstawowej polishu z 13.05). Cel: spójność wizualna z portalem pacjenta, mniej janku przy scrollowaniu, czytelniejsza hierarchia.

  • Nagłówek: data trzyma się tytułu “Panel Główny” w jednej linii — wcześniej skakała pod tytuł na średnich szerokościach
  • NavBar: mniejsze odstępy nad tytułem strony — tytuł bliżej górnej belki, mniej “powietrza” w pierwszej rzucie
  • Lista sesji: usunięty wewnętrzny scrollbar — lista rozwija się pionowo razem ze stroną, spójny spacing z karteczką sticky-note obok
  • Sekcja “TEN TYDZIEŃ”: usunięte stylowanie button/badge — to jest nagłówek sekcji, nie kontrolka klikalna
  • QuickRecordButton: wypełnia szerokość kolumny — wcześniej był wąskim guzikiem w środku
  • Gridy: ujednolicony gap gap-6 → gap-4 między dwoma gridami (nagłówek + treść)
  • Padding-top: mniejszy padding nad tytułem — bliżej NavBaru
  • Symetria: odstępy pionowe nad i pod elementami sticky-note + akcji teraz lustrzane
  • Tag “Online/Office” na karcie sesji: przeniesiony do osobnej linii pod “100 min” — wcześniej rozjeżdżał listę przy długich nazwach pacjentów

Karty pacjenta:

  • PatientProfile: guziki “Pokój Online” i “Kopiuj link” spójne wizualnie z zakładką sesji (ten sam wariant, ta sama ikona) — wcześniej na profilu pacjenta były innym wariantem, co dawało wrażenie dwóch różnych funkcji

Praca Bohdana.

8. 🔄 SCRUM-1510 (Bartek) — dual-source read dla session-summary/conceptualization

Po migracji ADR-0017 (SCRUM-1320) zapisy podsumowania sesji i konceptualizacji szły do _AiPart (z AI) oraz _TherapistPart (po edycji terapeuty), ale GET endpointy nadal czytały z legacy SessionSummaryJson / ConceptualizationJson — kolumny, których już nikt nie aktualizuje.

Efekt dla użytkownika: po odświeżeniu strony ocena sesji znikała, mimo że dane były w bazie (w _AiPart).

Fix: priorytetowy read TherapistPart → AiPart → legacy (legacy jako fallback dla sesji, gdzie AE backfill nie skopiował danych).

Praca Bartka.

9. 🩹 audit-ddd 2026-05-14 — naprawa 5 Critical findings

Cykliczny audyt dokumentacji DDD (docs/contexts/*.md). 11 bounded contextów porównanych z aktualnym stanem kodu przez auto-walidator + 11 równoległych agentów Explore. Raport: docs/ddd-audit-2026-05-14.md.

Naprawione Critical (5):

  • ai-llm: dodano AnalysisApprovalController, LlmJobsController do related_code; dodano AnalysisApprovalService, AnalysisHandlerRegistry, 5 Handlers/, LlmJobBackgroundService
  • notifications: dodano PatientNotificationsController (wyciągnięty w SCRUM-1338 z PatientAnalysisPublicationController)
  • architecture.md: nowe routy w tabeli URL→context (/api/llm-jobs, /api/patients/*/analyses, /api/patient/notification-preferences)
  • billing-payments + subscriptions: usunięto nieistniejące SubscriptionInvoiceService.cs i SubscriptionInvoicePdfService.cs z related_code (logika przeniesiona inline do kontrolera); zaktualizowano invariant numeracji + sekcje cross-context
  • last_verified zaktualizowane do 2026-05-14 we wszystkich dotkniętych plikach

Walidator scripts/verify-ddd-docs.mjs: 0 errors (przed: 7).

Pozostawione na później: 8 Medium + 4 Low findings → osobne tickety M1–M8 i L1–L4 w raporcie. Critical C3 (PatientsController context leakage) wymaga refaktoru kodu — osobny ticket.

Praca Bohdana.

10. 🩹 SCRUM-1503 — ADR-0025 audit HandleCheckoutSessionCompletedAsync

Pierwszy z serii audytów ADR-0025 (atomic claim pattern dla webhook handlers Stripe — wielokrotne webhooki dla tej samej sesji checkoutu mogły powodować silent last-write-wins). HandleCheckoutSessionCompletedAsync dostał audit + plan zabezpieczeń — pełny fix dla 9 webhook handlers wjechał już 12.05 (wcześniejszy release), ten ticket to formalny audit i dokumentacja decyzji.

Praca Bohdana.

11. 🐛 Bug fixes

  • LoginPage — prawdziwy powód blokady konta: gdy konto nie ma aktywnej subskrypcji, logowanie kończyło się komunikatem o “błędnych poświadczeniach”. Teraz pokazujemy konkretny powód (Konto bez aktywnej subskrypcji) — żeby terapeuta wiedział co zrobić zamiast zgadywać czy źle wpisał hasło
  • Temp iOS storeUrl → App Store search: zanim apka jest publicznie w App Store, storeUrl to wynik searcha (fallback link) — guard z punktu 5 i tak nie pokaże dialogu, ale jeśli ktoś otworzy ręcznie, zobaczy listę zamiast 404
  • maui-sync-version workflow: po auto-commicie wersji workflow explicite triggeruje deploy API — wcześniej auto-commit nie odpalał deploy’a (commit od github-actions[bot] nie matchował filtra trigger’u)

QA Checklist

Co testujeJakStatus
Release notes w dzwoneczkuPo deploy main: dzwoneczek terapeuty → ikona Sparkles → modal z 2026-05-14.md
ReleaseNotesModal — blogUrl”Czytaj pełny artykuł” otwiera https://aitherapy.support/blog/release-notes-14-maja-2026
Workflow announce-release-notesGitHub Actions → announce-release-notes.yml po push main → 200 OK
Auth OIDCAPI loguje repository=Aithentica/AITS-React + waliduje JWT — nie wymaga sekretu
SCRUM-1504 Faza 2 (AE adnotacji CBT)SELECT QuotedText FROM SessionTranscriptionAnnotation TOP 10 przez aplikację → odszyfrowane
SCRUM-1515 resurrectSesja MAUI z dużym plikiem na słabym 4G → po Failed blob dociera +15 min → transkrypcja się uruchamia
Cleanup cutoff 20 minNowe Uploading rekordy bez bloba → markowane Failed dopiero po 20 min
Sticky-note migracja localStorage→DBPierwszy login po deploy z notatką w localStoragePUT /sticky-note w Network → localStorage wyczyszczony
Sticky-note sync między urządzeniamiEdycja na desktop → restart aplikacji na laptopie → notatka widoczna
MAUI version checkApka 1.7.8 + latest=1.7.9 → dialog “Aktualizuj/Później”
MAUI iOS guardiOS + storeUrl=id0000000000 → dialog NIE pokazuje się
Auto-sync workflow MAUIBump .csproj → auto-commit + deploy API
EUR — checkout NL terapeutyHolenderski terapeuta → /pricing z EUR toggle → Stripe checkout w EUR
Currency switcher na pricingPL → PLN default; EN/DE/NL → EUR default; toggle persistuje w localStorage
Postcode NLProfil terapeuty z taxRegisteredCountry=NL1234 AB akceptowane
Postcode PLtaxRegisteredCountry=PL → wymagany format XX-XXX
SCRUM-1510 dual-source readEdycja podsumowania → F5 → dane nadal widoczne
Dashboard układLista sesji bez scrollbara, “TEN TYDZIEŃ” bez button-stylu, NavBar mniejsze odstępy
Tag Online/OfficeKarta sesji 100 min Online → “Online” w osobnej linii pod “100 min”
PatientProfile spójne guzikiPokój Online + Kopiuj link mają ten sam wariant co w zakładce sesji
LoginPage — powód blokadyKonto bez aktywnej subskrypcji → wiadomość “Konto bez aktywnej subskrypcji” (nie “Błędne dane”)
audit-ddd walidatornode scripts/verify-ddd-docs.mjs → 0 errors

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.