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:
AdminReleaseNotesControllerzPOST /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 audienceaits-react-release-notes; API waliduje JWT przeciw JWKS GitHuba i sprawdzarepository == Aithentica/AITS-React. Fallback: rola Administrator (do manualnych smoke-testów) - Broadcast:
NotificationServicerozsyłaUserNotificationtypurelease_notesdo wszystkich aktywnych terapeutów i administratorów — DB + SignalR (live update bez F5) - Frontend:
ReleaseNotesModalfetchuje markdown z/release-notes/{slug}.md, parsuje frontmatter (date,title,blogUrl), renderuje treść przezreact-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
mainzmieniającyclient/public/release-notes/**automatycznie wywołuje endpoint. Manualnyworkflow_dispatchz wyboremdev/prodjako 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-notesjest 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)) iComment_AE(nvarchar(1000)) jako Always Encrypted; backfill kopiujący wartości z plain do AE działa stopniowo - Faza 1 follow-up —
SqlParameter.Sizeexplicite 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:
DROPstarych plain kolumn,sp_rename _AE → docelowe. KolumnyQuotedTextiCommentto teraz AE Randomized - Subtelność EF Core 8:
AppDbContextModelSnapshot.csnie 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 przezCbtAnnotationService.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, sesja21271— bloblastModified+14 min poCreatedAtId 3393, sesja21275— blob +23 min poCreatedAt- 5 takich sesji odzyskanych ręcznie 14.05 przez
Failed → UploadingSQL flip
Co naprawiono:
ProcessStuckUploadsAsyncfilter rozszerzony oStatus = Failedz dokładnymErrorMessage"Plik audio nie został przesłany w wyznaczonym czasie."+BatchJobId = null+ProcessingCompletedAtz ostatnich 2h (limit chroni przed wskrzeszaniem stałych failures bez bloba)- Atomic claim
(Uploading → Processing) OR (Failed → Processing)z wyzerowaniemErrorMessageiProcessingCompletedAt LogInformation "SCRUM-1515 resurrect"gdy late-arrival wskakuje do pipeline (do monitoringu w Azure App Insights)NeedsReuploadSignalR ograniczony doStatus = Uploading— Failed-resurrect candidates dostali ten event już wcześniej, powtarzanie spamowałoby klientyCleanupOrphanedTranscriptionsAsync uploadingCutoff10 → 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.csdefensywna (IF NOT EXISTS,Up+Down) TherapistProfileController:GET+PUT /api/therapist/profile/sticky-note(auth:IsTherapistOrAdmin)
Frontend (Dashboard.tsx):
useEffectprzy mount: pobiera z backendu. Jeśli backend pusty +localStoragema dane → migracja jednorazowa do backendu + czyszczenielocalStorage. Jeśli backend ma dane → ignorujelocalStorage, używa backendu (źródło prawdy)- Save z 2s debounce +
onBlur— teraz idziePUTzamiastlocalStorage.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}zMobileVersionwappsettings.json - MAUI:
IVersionCheckServicewBootSplashViewModelpo health-check porównujeAppInfo.Current.VersionStringzlatesti 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
storeUrlto placeholderid0000000000, dialog na iOS jest pomijany (żeby nie otwierać martwego linku) - Workflow
maui-sync-version.yml: trigger na push zmieniającymaui/AITS.Mobile/AITS.Mobile.csproj, czytaApplicationDisplayVersioni auto-commituje bumpMobileVersion.{Android,Ios}.Latestwserver/AITS.Api/appsettings.json. Po auto-commicie explicite triggeruje deploy API - Bump 1.7.8/28 → 1.7.9/29 — drobne fixy z
MyUploadsperf
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) wSubscriptionType - 6 wierszy EUR (
starter/standard/professional×monthly/yearly) sklonowanych z PLN z nadpisanymStripePriceId - Unique index na
(Code, DurationDays, Currency)— wsparcie pod kolejne waluty bez kolizji - Migracja idempotentna, defensywna,
featureskopiowane z PLN
Stripe:
- 6 nowych Price (EUR) na istniejącym produkcie
prod_TXlr7rBrxH8jTx - Kwoty: Starter
59/590, Standard89/890, Professional119/1190EUR (miesięczne/roczne) - Price IDs w Key Vault
aits-kv-{prod,dev-new,sandbox-new}jakoStripe--PriceIds--{Plan}{Period}EUR
Backend:
SubscriptionPlanInfoma terazCurrencyGetAvailablePlansAsynczwraca 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-DElocale) - Fallback do PLN gdy backend jeszcze nie zwraca currency
/pricingpokazuje 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
- PL:
- 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-4mię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: dodanoAnalysisApprovalController,LlmJobsControllerdorelated_code; dodanoAnalysisApprovalService,AnalysisHandlerRegistry, 5Handlers/,LlmJobBackgroundServicenotifications: dodanoPatientNotificationsController(wyciągnięty w SCRUM-1338 zPatientAnalysisPublicationController)architecture.md: nowe routy w tabeli URL→context (/api/llm-jobs,/api/patients/*/analyses,/api/patient/notification-preferences)billing-payments+subscriptions: usunięto nieistniejąceSubscriptionInvoiceService.csiSubscriptionInvoicePdfService.cszrelated_code(logika przeniesiona inline do kontrolera); zaktualizowano invariant numeracji + sekcje cross-contextlast_verifiedzaktualizowane do2026-05-14we 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,storeUrlto 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-versionworkflow: po auto-commicie wersji workflow explicite triggeruje deploy API — wcześniej auto-commit nie odpalał deploy’a (commit odgithub-actions[bot]nie matchował filtra trigger’u)
QA Checklist
| Co testuje | Jak | Status |
|---|---|---|
| Release notes w dzwoneczku | Po 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-notes | GitHub Actions → announce-release-notes.yml po push main → 200 OK | ✅ |
| Auth OIDC | API 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 resurrect | Sesja MAUI z dużym plikiem na słabym 4G → po Failed blob dociera +15 min → transkrypcja się uruchamia | ✅ |
| Cleanup cutoff 20 min | Nowe Uploading rekordy bez bloba → markowane Failed dopiero po 20 min | ✅ |
| Sticky-note migracja localStorage→DB | Pierwszy login po deploy z notatką w localStorage → PUT /sticky-note w Network → localStorage wyczyszczony | ✅ |
| Sticky-note sync między urządzeniami | Edycja na desktop → restart aplikacji na laptopie → notatka widoczna | ✅ |
| MAUI version check | Apka 1.7.8 + latest=1.7.9 → dialog “Aktualizuj/Później” | ✅ |
| MAUI iOS guard | iOS + storeUrl=id0000000000 → dialog NIE pokazuje się | ✅ |
| Auto-sync workflow MAUI | Bump .csproj → auto-commit + deploy API | ✅ |
| EUR — checkout NL terapeuty | Holenderski terapeuta → /pricing z EUR toggle → Stripe checkout w EUR | ✅ |
| Currency switcher na pricing | PL → PLN default; EN/DE/NL → EUR default; toggle persistuje w localStorage | ✅ |
| Postcode NL | Profil terapeuty z taxRegisteredCountry=NL → 1234 AB akceptowane | ✅ |
| Postcode PL | taxRegisteredCountry=PL → wymagany format XX-XXX | ✅ |
| SCRUM-1510 dual-source read | Edycja podsumowania → F5 → dane nadal widoczne | ✅ |
| Dashboard układ | Lista sesji bez scrollbara, “TEN TYDZIEŃ” bez button-stylu, NavBar mniejsze odstępy | ✅ |
| Tag Online/Office | Karta sesji 100 min Online → “Online” w osobnej linii pod “100 min” | ✅ |
| PatientProfile spójne guziki | Pokój Online + Kopiuj link mają ten sam wariant co w zakładce sesji | ✅ |
| LoginPage — powód blokady | Konto bez aktywnej subskrypcji → wiadomość “Konto bez aktywnej subskrypcji” (nie “Błędne dane”) | ✅ |
| audit-ddd walidator | node scripts/verify-ddd-docs.mjs → 0 errors | ✅ |
Artykuł przygotowany przez zespół Therapy Support