Release Notes — 20 maja 2026
Kolejny duży release — 63 commity w cztery dni od poprzedniego wydania. Centralny temat sprintu to integracja z Microsoft Outlook / Microsoft 365 — pięciofalowa praca, która kończy się pełnym parytetem z Google Calendar, dodaje Microsoft Teams jako protokół wideo i opcjonalną wysyłkę maili “w imieniu terapeuty”. Drugi nurt to uporządkowanie domeny sesji — AvailabilityTypeId zostaje jedynym źródłem prawdy o typie sesji (Variant B), zniknęły niespójności między modal’em “Dodaj sesję” a listą i szczegółami. Trzeci wątek to rozszerzenie katalogu narzędzi klinicznych — cztery nowe testy psychometryczne (4DKL, BAT-PL w wersji 12 i 23, PDSS-SR) oraz brakujący backend PDF/export dla restrukturyzacji poznawczej.
💚 Podziękowania dla zespołu testowego: Martyny, Bartłomieja, Pana Jacka, Jacka W. i Bohdana — Wasze zgłoszenia i sprinty warsztatowe zrobiły z tego release’u to, co widać poniżej.
1. 📅 Integracja z Microsoft Outlook / Microsoft 365 — pięć fal
Najgłębsza zmiana w obszarze integracji od czasu wprowadzenia Google Calendar. Sprint odbył się w pięciu falach commitowanych po kolei — każda fala dokłada konkretną warstwę funkcjonalności i jest niezależnie deployowalna.
Wave 1 — OAuth foundation
Fundament całej integracji: flow OAuth 2.0 z Microsoft Identity (tenant common, więc działa zarówno dla kont firmowych Aithentica, jak i osobistych @outlook.com), pełne entity scaffolding (TherapistMicrosoftToken, TherapistOutlookCalendar), kolumny Session.OutlookCalendarEventId i Session.TeamsMeetingLink, oraz dedykowany kontroler OutlookIntegrationsController z endpointami /login, /callback, /status, /disconnect. Tokeny są szyfrowane tym samym mechanizmem co Google (IOAuthTokenProtector). Background service odświeża tokeny co 30 minut na 15 minut przed wygaśnięciem. Decyzje architektoniczne udokumentowane w ADR-0027.
Wave 2 — Calendar event CRUD + WeekCalendar overlay
Sesje terapeuty trafiają teraz do kalendarza Outlook. W backendzie pełny OutlookCalendarService zbudowany na Microsoft Graph SDK v5 — single-event CRUD, multi-calendar listing, wybór głównego kalendarza, mapowanie na nasze SingleValueExtendedProperties (well-known GUID z metadanymi aits_session_id + aits_source=AITS, mirror Google extendedProperties). SessionsController ma 6 hook’ów na ścieżce create / update / delete / reschedule. Frontend — sekcja “Wybierz kalendarz główny” w Ustawienia → Integracje → Outlook oraz nowa warstwa wydarzeń Outlook na WeekCalendar (bg-blue-100, persisted toggle “Pokaż Outlook” w localStorage). Service jest self-graceful: brak primary calendar → zwraca null, nic się nie sypie.
Wave 3 — Microsoft Teams + recurring events + freebusy
Trzy duże feature’y w jednej fali. Po pierwsze — Microsoft Teams jako protokół wideo: nowy typ sesji MeetingType=TeamsMeeting=5 w modal’u “Dodaj sesję” (grid wybór platformy wideo rozszerzony z 2 na 3 kolumny), OutlookCalendarService.CreateEventAsync ustawia IsOnlineMeeting=true + OnlineMeetingProvider=TeamsForBusiness, po POST odczytuje OnlineMeeting.JoinUrl i zapisuje do Session.TeamsMeetingLink. Konta osobiste bez licencji Teams — graceful fallback (event tworzy się bez linku, log). Po drugie — sesje powtarzające się przez PatternedRecurrence (Daily / Weekly / Biweekly / Triweekly / EveryFourWeeks / EverySixWeeks / Monthly). Wszystkie sesje w serii dziedziczą OutlookCalendarEventId + TeamsMeetingLink z master eventu. Po trzecie — freebusy z /me/calendar/getSchedule → BusyPeriod z BusySource.OutlookCalendar=6. Outlook’owa zajętość jest teraz brana pod uwagę w AvailabilityService razem z Google, Apple i sesjami AITS.
Wave 4 — Cross-provider exclusivity + import + self-heal
Single-primary calendar enforcement across providers: terapeuta może mieć w danym momencie tylko jeden główny kalendarz (Google ALBO Outlook). Ustawienie Outlook primary atomic zeruje TherapistGoogleCalendar.IsPrimary (i odwrotnie). Frontend pokazuje amber warning banner gdy Google ma primary a Outlook jeszcze nie. SessionsController.ResolveCalendarRoutingAsync woła tylko jeden service per session create — żadnego dual-write. Dorzucony też import wydarzeń z Outlook jako sesje AITS (OutlookCalendarImportService w 3 trybach: patientId / attendeeEmail / isAdHoc — mirror Google) plus self-heal background service który raz na 12h fetchuje CalendarView w oknie [jutro, +6 dni] i odtwarza eventy ręcznie usunięte z Outlooka.
Wave 5 — Mail integration + per-therapist toggle
Opcjonalna wysyłka maili “w imieniu terapeuty” przez Microsoft Graph (/me/sendMail) zamiast SMTP. Pole TherapistProfile.UseOutlookForMail (bit NOT NULL DEFAULT 0, świadomie nie Always Encrypted — flaga administracyjna, wyjątek ADR-0018), nowy endpoint PUT /api/therapist/profile/outlook-mail-toggle, sekcja “Wysyłka maili przez Outlook” w Ustawienia → Integracje → Outlook widoczna gdy Outlook connected. Maile eligible: rezerwacje, przypomnienia sesji, anulowania, reschedule, wiadomości do pacjentów, powiadomienia finansowe. Maile autoryzacyjne (MFA, password reset, temporary password) zawsze lecą przez SMTP — by design. Fallback do SMTP transparentny: 401/403/429 → log + cichy fallback, nigdy nie rzuca exception.
Co to daje testerom: terapeuci pracujący w Microsoft 365 mogą podpiąć Outlook tak samo jak Google — z eventami, Teams meetingami, freebusy i opcjonalnymi mailami “z ich adresu”. Wszystko z fallbackami, więc nawet jak coś po stronie Microsoftu padnie — AITS dalej działa.
2. 🎯 AvailabilityTypeId jako single source of truth (Variant B)
Refaktor architektoniczny porządkujący typ sesji. Wcześniej Session.Location był free-textem ("Online", "Gabinet", custom), a Session.MeetingType osobnym enumem — i te dwie wartości potrafiły się rozjeżdżać. Teraz Session.AvailabilityTypeId (FK do TherapistAvailabilityType z ustawień terapeuty) jest jedynym źródłem prawdy. Backend resolve’uje Location / MeetingType / MeetingLink z encji typu lokalizacji; legacy pola zostają jako fallback dla starych sesji (AvailabilityTypeId=NULL).
- Migracja
20260519140000_AddAvailabilityTypeIdToSession— kolumna NULL + FK zON DELETE NO ACTION(SET NULLbył zablokowany przez SQL Server cykl FK przezApplicationUser), defensywny backfill po(TerapeutaId, Name=Location) CreateSessionRequest/UpdateSessionRequest—AvailabilityTypeIdnullable; gdy podany, backend nadpisujeLocation/MeetingType/MeetingLinkz encji- Frontend —
SessionFormiAddSessionModalwysyłająavailabilityTypeIdderive’owany z wybranego typu (in-person match po nazwie, online match poDefaultMeetingType+ fallback) - Refaktor
AddSessionModal— 2-stopniowy picker (Stacjonarna/Online + AITS/Teams/External) zastąpiony jednym dropdownem filtrowanym poIsOnline. Items pochodzą zTherapistAvailabilityTypez ustawień (toggle Stacjonarna/Online zostaje jako kontekst) SessionCard— pokazuje teraz nazwę lokalizacji z ustawień (np."Online external link"zamiast generic"Online","W Gabinecie"zamiast normalizowanego"Office")SessionDetailViewNew— gdy sesja ma custom Location (np."kolorowa"), dropdown dodaje dla niej osobną opcję na początku zamiast fallbackować na pierwszy aktywny typ z listy
Co to daje testerom: spójność nazw lokalizacji między modal’em “Dodaj sesję”, listą sesji i widokiem szczegółów. Praca Bohdana.
3. 💳 iDEAL w Stripe Connect dla pacjentów
Pacjenci holenderscy mogą teraz płacić terapeutom przez iDEAL (najpopularniejsza metoda płatności online w NL). Płatność idzie przez Stripe Connect terapeuty — jeśli ma aktywne capability ideal_payments na koncie Stripe, opcja pojawia się obok karty.
- Nowy helper
GetConnectPaymentMethodTypesAsyncodpytuje capabilities konta Connect — iDEAL dodawane doPaymentMethodTypestylko jeśli capability jest aktywne (fallback do samej karty przy błędzie API) - Dotyczy obu ścieżek:
CreateSessionPaymentCheckoutAsync(pojedyncza sesja) iCreateMultiSessionPaymentCheckoutAsync(wiele sesji) - Restrykcja dodana w hotfixie: iDEAL pokazywany tylko terapeutom z kontem Connect w NL — wcześniej feature był aktywowany na podstawie samego capability, ale Stripe wyrzucał błąd przy odpalaniu Connect z innego kraju
- Stripe robi auto-konwersję EUR ↔ waluta terapeuty
Co to daje: niższy próg wejścia dla pacjentów z Holandii. Konto terapeuty musi mieć aktywne ideal_payments w Stripe Dashboard.
4. 🧠 Cztery nowe testy psychometryczne w katalogu
Rozszerzenie katalogu formularzy klinicznych o cztery nowe, ważne polskie i międzynarodowe skale:
- PDSS-SR (Panic Disorder Severity Scale — Self Report; Houck i wsp. 2002) — 7 pytań, skala 0–4 z indywidualnymi etykietami per pytanie, 5-stopniowa klasyfikacja nasilenia (minimalne / łagodne / umiarkowane / ciężkie / bardzo ciężkie)
- BAT-PL 12 (Burnout Assessment Tool — wersja krótka; Schaufeli/De Witte/Desart 2020, pol. Baka/Basińska 2020) — 12 pytań w 4 podskalach (Wyczerpanie, Dystans psychiczny, Osłabienie kontroli poznawczej, Osłabienie kontroli emocjonalnej), skala 1–5
- BAT-PL 23 — wersja pełna BAT, 23 pytania w tych samych 4 podskalach (cutoffy total 23–115)
- 4DKL / 4DSQ (Terluin; pol. Czachowski/Buszman 2014) — 50 pytań, skala 0–4 z mapowaniem punktacji
{0:0, 1:1, 2:2, 3:2, 4:2}(odpowiedzi 3 i 4 mapują się na 2 pkt), 4 podskale (Distres / Depresja / Lęk / Somatyzacja) z osobnymi progami nasilenia
Zmiana techniczna w mechanizmie scoringu: SubscaleRule ma nowe pole ValueMap (Dictionary<string,int>) i metodę sum_with_map. Frontend scoring.ts zsynchronizowany z FormScoringService — wcześniej draft pacjenta dla 4DKL pokazywał błędny preview (FE liczyło 3 i 4 jako 3/4 zamiast 2). Każdy test ma osobny plik partial w FormTemplateCatalog — łatwa weryfikacja i izolacja. Seeder uruchamia się przy starcie kontenera i automatycznie tworzy 4 nowe szablony.
5. 📄 PDF / export / share dla restrukturyzacji poznawczej
Zgłoszenie SCRUM-1580 od Martyny. UI w CognitiveRestructuringTab.tsx już od dawna renderował 3-stanowy FSM (draft → approved → published) z przyciskami PDF / share / send, ale kliknięcie któregokolwiek zwracało 404 “Nieznany typ analizy” — backend handler nie był zarejestrowany.
Fix: nowy CognitiveRestructuringHandler czyta thoughts[] z merged Session.AbcV2Json (kolejność TherapistPart → AiPart → Legacy, zgodnie z ADR-0017), GenerateCognitiveRestructuringPdf w ConceptualizationPdfService z layoutem karta-per-myśl (myśl automatyczna + zniekształcenie badge + myśl alternatywna — wizualnie spójny z UI). Zero zmian w modelu danych — wykorzystany istniejący plugin pattern IAnalysisHandler.
Co to daje testerom: pełny eksport PDF, share na publiczny link, send do pacjenta dla każdej zatwierdzonej restrukturyzacji poznawczej.
6. 🔕 Toggle “Blokuj zapisy online” w ustawieniach kalendarza
Zgłoszenie SCRUM-1021 od Bartłomieja. Terapeuta może teraz tymczasowo wstrzymać zapisywanie się pacjentów przez publiczny profil (/t/:slug) bez ruszania per-slot availability. Toggle pojawia się w Ustawienia → Profil publiczny → “Sesje i płatności”, semantyka odwrócona (checked = blokada włączona), bindowany do istniejącego pola TherapistProfile.PublicBookingEnabled z auto-save. Po wyłączeniu — dostępność wraca automatycznie (backend od dawna respektował tę flagę i zwracał 404 dla publicznych endpointów GetAvailability/BookSession/GetBookingInfo gdy zablokowane).
7. 🗓️ Toggle “Uwzględniaj zajętość z kalendarzy zewnętrznych”
Per-therapist flaga TherapistProfile.RespectExternalCalendarBusy (default true) kontrolująca, czy /api/therapist/availability/slots blokuje wolne terminy gdy Google / Outlook / Apple Calendar pokazuje zajętość. Patient-facing booking (SCRUM-1378) miał to zawsze włączone — teraz analogicznie dla therapist-facing tworzenia sesji. Nowy komponent RespectExternalCalendarBusyToggle.tsx w sekcji Ustawienia → Integracje pod toggle’em conflict-bell.
Bonus: AvailabilityService.GetAllBusyPeriodsAsync dorzucił Apple Calendar do listy badanych źródeł zajętości (wcześniej był tylko Google + Outlook + Sessions + Holidays + TherapistEvent).
8. ⌨️ Command palette (⌘K) na stronie Ustawienia
Wyszukiwarka funkcji w stylu Linear / Notion — modal otwierany skrótem ⌘K / Ctrl+K lub chipem nad sidebarem. Pozwala szybko nawigować po 17 sekcjach Ustawień zamiast scrollować i czytać listę.
- Strict substring match z normalizacją diakrytyków (NFD + explicit
ł→lprzed normalizacją —łto atomowy codepoint U+0142, NFD go nie dekomponuje, więc"platnosci"faktycznie matchuje"Płatności") - Aliasy PL+EN per sekcja — np.
"stripe"/"prowizja"→ Płatności,"rodo"/"gdpr"→ Prywatność,"mfa"/"pin"→ Bezpieczeństwo - Nawigacja klawiaturą — ↑↓ Enter Esc, highlight matchowanego fragmentu
- Grupy — Sekcje + Akcje (na razie 1 akcja: Wyloguj)
- 11 nowych kluczy i18n (PL + EN); pozostałe locale matchują po label
Bug fix po pierwszej iteracji: fuzzy fallback (all-chars-in-order) generował false positives — "rodo" matchował 8 sekcji, "mfa" 5, "stripe" matchował też Profil. Wyrzucone, został czysty substring na znormalizowanym labelu + aliasach.
9. 🔃 SessionsList — przełącznik kierunku sortowania (desc / asc)
Wcześniej lista sesji była zawsze sortowana malejąco (najświeższe na górze) i nie dało się tego zmienić. Teraz przełącznik kierunku respektujący wszystkie aktywne filtry (data, status, pacjent, typ).
10. 📨 SMTP via webd.pl zamiast Azure Comm Email
Tymczasowa zmiana transportu maila w aits-agent. Azure Communication Email blokowane przez DomainNotLinked — domena aitherapy.support nieverified w DNS providera (DKIM OK, ale TXT records Domain / SPF nie zostały ustawione). Zamiast czekać na DNS provisioning — SMTP webd.pl działa od ręki: nodemailer port 465 (implicit TLS), hasło + host + user z Key Vault, sender domyślnie agent@aitherapy.support.
11. 🤖 aits-agent — pilot na Container Apps
Nowy mikroserwis aits-agent do automatyzacji zadań kodowych przy użyciu Claude Agent SDK na Azure.
- Gateway (ASP.NET Minimal API) — webhooki Telegram / Jira → routing przez
QueueDispatcherdo Azure Service Bus - Worker (Node 20 + Claude Code CLI) — pięć typów jobów:
jira-triage,ddd-audit,weekly-report,pr-review,telegram-echo - Split queues:
agent-jobs-fast(telegram / callback) +agent-jobs-claude(heavy Claude jobs) — DLQ + lockDuration tuning per queue (fast 1min / claude 5min) - Auth: subskrypcja Claude (OAuth z Key Vault), fallback na API key
- Infra:
infrastructure/agent.bicep(rg-aits-agents, polandcentral),replicaTimeout=1800szgodne zAGENT_TIMEOUT_MS - CI/CD: trzy workflow’y — gateway-deploy, worker-deploy, oraz nowy
aits-agent-infra-deploy.yml(az deployment group createzwhat-ifpreview) — żeby zmiany w Bicepie nie triggerowały redundant rebuild image - OIDC federated identity w GitHub Actions (3 secrety client/tenant/subscription ID) zamiast creds JSON
- Cron job
aits-pilot-cron-weekly— Container Apps Job typu Schedule, cron0 9 * * 1(poniedziałek 9:00 UTC), curluje/cron/weekly-reportz managed identity tokenem - App Insights SDK w workerze (
trackException+ flush 2s hard timeout) zamiast 5min lag z Log Analytics
telegram-chat — slash commands + AppInsights + Jira write
Telegram-owy bot do “rozmawiania z PROD” zyskał trzy nowe capabilities:
- Read-only dostęp do Application Insights PROD — Dockerfile workera ma
azCLI (~150MB),bootstrapAzClirobiaz login --identityraz na start replicy. Allowed:Bash(az monitor app-insights query:*)+Bash(az monitor metrics list:*). Bot odpowiada teraz na pytania typu “ile było 500 ostatnio” / “co się dzieje na PROD”. Worker-fast managed identity maLog Analytics Reader+Readernaaits-appinsights-prod - Slash commands —
/help,/pr <N>(review pull requesta),/jira <KEY>(triage ticketu),/audit,/weekly. Plus dwa nowe handlery:/newjira <temat>(tworzy ticket — POST do/rest/api/3/issuez basic auth) i/analyse <KEY>(głębsza analiza istniejącego ticketu z dostępem do kodu) - Jira write capability — agenci
jira-triageijira-analysemogą postować komentarze (POST/commentw ADF) i dodawać etykiety (agent-triaged,agent-analysed) bez MCP atlassian, używając samegocurlz creds w env vars
12. ⚙️ Admin endpoint — ręczna regeneracja sesji powtarzających się
Nowy endpoint POST /api/admin/maintenance/recurring-sessions/regenerate (rola Administrator) do ręcznego odpalania generatora sesji z terminów stałych — poza niedzielnym cronem o 23:00 Warsaw. Używane do testów po fixach lub awaryjnej regeneracji.
Przy okazji fix RecurringSessionGenerationService: generator wygenerował 107× SqlException unique violation. Root cause — dedup query sprawdzała (PatientId, TerapeutaId, StartDateTime), ale unique index chroni (TerapeutaId, StartDateTime) WHERE StatusId != 4 (bez PatientId). Slot zajęty przez innego pacjenta był niewidzialny dla dedup, ale baza go widziała.
- Dedup zgodny z indexem:
(TerapeutaId, StartDateTime) + StatusId != Cancelled latestSession/latestSessionDatefiltrują Cancelled (niezależny bug: jeśli wszystkie przyszłe sesje pacjenta były anulowane, generator nie generował nowych myśląc że już są)- Safety net — per-session
SaveChangesAsynczcatch DbUpdateException(SqlException.Number == 2601). Race condition nie wywala batcha, tylko loguje warning i pomija konkretną sesję
13. 🎥 Bulk-create sesji — synchronizacja VideoInviteToken (SCRUM-1597)
Zgłoszenie od Bohdana. CreateBatchSessions ustawiał Session.VideoInviteToken na nowy GUID per sesja, zamiast skopiować Patient.VideoInviteToken. Skutek: pacjentka klikała link z maila (zawierający token pacjenta) i wpadała w fallback do legacy pokoju patient-{id}, bo /api/video/join filtruje sesje po s.VideoInviteToken == inviteToken. Terapeuta przez /api/video/token trafiał do session-{id} — obie strony same w różnych pokojach.
Fix mirroruje wzorzec z RecurringSessionsController (już istniejący): bulk-create kopiuje patient.VideoInviteToken z lazy-create jeśli null + zerowanie ExpiredAt dla AITS. DB hotfix: 203 mismatched sesji w przyszłości zsynchronizowane przed deployem.
14. 📆 Public booking respektuje Google Calendar busy (SCRUM-1378)
Zgłoszenie od Bohdana. Wcześniej PublicTherapistController.GetAvailability miał własną pętlę slot-generation która ignorowała zajętość z Google / Outlook / TherapistEvent — pacjent widział slot jako wolny mimo że terapeuta miał już blokadę w kalendarzu. Plus race window: między fetch slotów a POST booking dało się “wrzucić” rezerwację na slot, który właśnie został zajęty.
Fix:
- Krok 1:
IAvailabilityService.GetAvailableSlotsAsynczyskałbool includeExternalBusy=false, plus nowa metodaFindExternalConflictAsyncdla race-check - Krok 2:
PublicTherapistController.GetAvailability— usunięta ręczna pętla, delegacja do_availabilityService.GetAvailableSlotsAsync(isPatientBooking:true, includeExternalBusy:true) - Krok 3: Logged-in patient surfaces opt-in (
PatientAvailabilityController.GetAvailableSlots,PatientSessionsController.GetRescheduleSlots) - Krok 4: Race-window guards —
BookSession(×2) iRescheduleSessionwołaFindExternalConflictAsyncw transakcji booking; przy konflikcie HTTP 409 z komunikatem “Wybrany termin został właśnie zajęty w kalendarzu terapeuty. Wybierz inny.”
Regresja od tej zmiany (i jej naprawa): response shape GetAvailability zmienił się (pole IsAvailable usunięte, bo backend pre-filtruje; startTime/endTime → startDateTime/endDateTime), ale frontend public booking widget nie został zsynchronizowany. Memo zakładało że slots.filter(slot => slot.isAvailable) w PublicBookingCalendar.tsx:180 jest no-op po pre-filtracji — ale undefined === true to false → drop-all filter, kalendarz pusty. Frontend dostosowany.
15. 🗑️ Kompletne usunięcie integracji Docplanner
Likwidacja całej integracji z Docplanner / znanylekarz.pl — była opcjonalna, bez UI terapeuty, w produkcji nieaktywna. Backend: DocplannerController, DocplannerSyncService, DocplannerClient, IDocplannerClient, DocplannerOptions, DocplannerModels + klasy DocplannerAccount/PatientLink/BookingLink/PaymentLink z Models.cs + DbSets + fluent API. Migracja RemoveDocplannerIntegration — z poprawką drop auto-named DEFAULT constraints przed DROP COLUMN (SQL Server tworzy default constraint o losowej nazwie typu DF__Therapist__Docpl__1234ABCD, bez explicit drop migracja się sypała).
Drobne poprawki (bug fixes)
- SCRUM-1591 (Bohdan): multi-replica regresja “Lokalny plik audio nie istnieje” —
NormalizationJobzLocalFilePath != nullwymaga, żeby worker normalize i upload landowały na tej samej replice. Na PROD (2+ repliki) audio zapisane na replice A nie istnieje na replice B. 5 transkrypcji 2026-05-18 (TxId 3681, 3691, 3692, 3695, 3698) straciło audio. Fix: pre-upload audio do Azure Blob przed enqueue, wymuszenieBlobBasedbranch w workerze - SCRUM-1590 (Jacek):
Patient.EmailNULL crashuje POST/api/patients— nullable field nie był chroniony przed użyciem w stringu, wywalał całą ścieżkę tworzenia pacjenta TherapistProfile.StickyNote— Always Encrypted nvarchar(4000) clash na każdym SaveChanges — migracja20260514150000utworzyła kolumnę jakonvarchar(4000)AE RANDOMIZED, ale entity property nie miałHasMaxLength— EF defaultuje donvarchar(max). Każdy INSERT/UPDATETherapistProfile(nie tylko sticky-note endpoint!) wysyłałnvarchar(max)→ SqlException “Operand type clash”. Observed: 3× SqlException w 5 min na PROD przy edycji profili- Bump Azure OpenAI health probe timeout 5s → 10s — probe
azureOpenAIflapował down↔ok bo reasoning model gpt-5-5-1 spike’ował latency do 4–6s nawet dla ping zmax_completion_tokens=16(internal reasoning). Średnia 0.8s, spike’i do 3.9s, okazjonalne >5s →TaskCanceledException. TimeoutMs 5000 → 10000 - Batch transcription — odróżnij realny 404 od transient błędów —
GetStatusAsynczwracałUnknowndla 4 różnych sytuacji (HTTP 404, 5xx/408/429, exception). Poller traktował jednakowo i po 15 min markował Failed z “Transcription job no longer exists on Azure”. ~25 transkrypcji na PROD fałszywie failowane. Fix: nowa wartośćBatchTranscriptionStatus.NotFoundzwracana tylko dla HTTP 404. Plus safety netCleanupOrphanedTranscriptionsAsynczamyka stuck-forever joby po 6h - IBAN walidacja — akceptuj zagraniczne konta (NL, DE itd.) — frontend blokował zapis konta bankowego dla terapeutów spoza PL, mimo że backend
IbanValidator.IsValidakceptuje dowolny IBAN ISO 13616. Nowa funkcjaisValidIban(mod-97 + tabela długości per kraj — mirror BE). ZachowanyisValidPolishIbanna wszelki wypadek - Profil pacjenta — hardcoded “PLN” zamiast waluty terapeuty — 5 miejsc bypassowało istniejący mechanizm
formatPrice/useCurrentTherapistCurrency(cena sesji w edytorze, zaległość w headerze, label przy polu wPatientProfileSettingsTab, label przy edycji ceny wSessionDetailViewNew) - Historia płatności subskrypcji — hardcoded “PLN” —
TherapistSubscription.tsxpokazywał{payment.amount.toFixed(2)} PLNmimo że obok faktura w tej samej tabeli używała jużcurrencyz BE. Fix: walutę bierze zbilling.currencyaktywnej subskrypcji TherapistSubscription— kwoty subskrypcji jako symbol waluty, nie ISO code — sekcja “nadchodząca opłata” pokazywała319.20 PLNzamiast319,20 zł. Wszystkie kwoty przezformatPricezIntl.NumberFormat(separator tysięcy, symbol waluty)- Modal “Edytuj sesję” (Pan Jacek): formatowanie pól “Aktualny termin” / “Nowy termin” — ikona kalendarza nachodziła na datę, padding’i się nie zgadzały. Wrapper
<div className="relative">→<div className="relative group">(specificity hack pod CSS override) +pl-12→pl-10+shadow-sm+focus:outline-none+tabIndex=-1na readonly inpucie - Formularze pacjenta — fallback języka dla wariantów regionalnych — pacjent / terapeuta z
i18n.language='pl-NL'lub'pl-PL'widział formularze po angielsku, mimo że template miał polskie etykiety.SUPPORTED_CULTURES.includes(i18n.language)sprawdzało tylko krótkie kody. Nowa funkcjaresolveCulture(lang)ucina region (pl-NL→pl) i waliduje względemSUPPORTED_CULTURES. Plus: dlaculture='pl'też próbuje fallback do*Engdy slot PL pusty - Pokój video — gość z wygasłym tokenem przekierowywany na /login —
NotificationContextprzy mount bezwarunkowo fetchował/api/notificationsjeślilocalStorage.tokenistniał (nawet wygasły). 401+refresh fail →window.location='/login', ale/video/join/nie było na liście publicznych stron. Gość klikający link zaproszenia z wygasłym tokenem (np. po wcześniejszym logowaniu jako pacjent w PWA) leciał na ekran logowania. Dodane/video/join/i/video/test-join/doonPublicPagewhandleSessionExpired handleAudioFileUploadReferenceError — wołałsetTranscriptionPollingErrorisetTranscriptionPollingStatus, którychuseTranscriptionPollingnie eksponuje. W prod buildzie minifikacja zachowała te wywołania →ReferenceErrorwonChangeinputa pliku → upload nigdy nie startował (UI pokazywał “Uploading…”, backend zero traffic). Fix: destrukturyzacjaclearPollingErrorz hooka- Synchronizacja preview scoringu FE z BE dla 4DKL — szczegóły w sekcji 4 powyżej
- iOS build bump 30 → 31 — TestFlight 90189 redundant binary,
ApplicationDisplayVersion 1.7.10bez zmian (TestFlight External nie wymaga ponownego Beta App Review)
Infrastruktura i diagnostyka
flushTelemetryhard timeout 2s — worker exit hangs gdy AppInsights endpoint nieosiągalny: flush callback nie odpala,process.exit(0)nigdy nie wywoła, Container Apps Job execution wisi wRunningaż doreplicaTimeout(30 min) blokując parallelism=1 slot. Promise.race zsetTimeout(2000)wymusza exitcron jobfetches token via managed identity + az CLI — Container Apps KVsecretRefsync padał 2× mimo poprawnego RBAC. Workaround: imagemcr.microsoft.com/azure-cli,az login --identity+az keyvault secret showprzy runtime, potem curl gatewaycheckoutRepoobsługuje commit SHA —prReviewprzekazujehead.shajakoref→git clone --branch <sha>nie działa (SHA nie jest branchem). Nowy flow: jeśli ref pasuje do/^[0-9a-f]{7,40}$/, clone--no-checkout+ fetch + checkout FETCH_HEADaits-agentprompts + README odznaczone z globalnego*.mdgitignore — worker docker build padał na “COPY aits-agent/prompts /app/prompts: not found” bo*.mdw root.gitignoreblokował commit prompts- Diagnostyka OAuth fix dla Claude SDK — trzy commity diagnostyczne (
diag: dump SDK error details,diag: monkey-patch child_process.spawn,diag: revert spawn monkey-patch) doprowadziły do zlokalizowania bugu i zostały cofnięte. Dług diagnostyczny zwrócony — kod jest czysty - Docs: nowa sekcja “Dostęp do Application Insights dla Claude Agent SDK” w
ai-agent-guide.mdz opisem SPclaude-agent-sdk-appinsights-reader(rola Reader na 4 App Insights, credentials w KVagent-bohdana, przykłady Python / az CLI / REST, rotacja secretu). Plus:docs/contexts/transcription.mdzaktualizowany o F2b BlobBased-only normalize - Telegram messaging:
jira-triage/jira-analysewysyłają finalText do Telegrama, plus final message verbatim z komentarza Jira (zamiast podsumowania)
QA Checklist
| Co testuje | Jak | Status |
|---|---|---|
| Outlook OAuth (Wave 1) | Ustawienia → Integracje → Outlook → Connect → status connected | ✅ |
| Outlook event CRUD (Wave 2) | Stwórz sesję → event pojawia się w Outlooku | ✅ |
| Outlook primary calendar select (Wave 2) | Wybierz inny kalendarz w sekcji “Wybierz kalendarz główny” | ✅ |
| WeekCalendar Outlook overlay | Toggle “Pokaż Outlook” → eventy widoczne na niebiesko | ✅ |
| Microsoft Teams meeting (Wave 3) | AddSessionModal → Online → “Microsoft Teams” → sesja ma teamsMeetingLink | ✅ |
| Recurring sessions w Outlooku (Wave 3) | Stwórz sesję cykliczną → wszystkie sesje mają ten sam OutlookEventId | ✅ |
| Outlook freebusy (Wave 3) | Zajęte termin w Outlooku → AvailabilityService blokuje slot | ✅ |
| Cross-provider exclusivity (Wave 4) | Ustaw Outlook primary → Google primary wyzerowane | ✅ |
| Outlook self-heal (Wave 4) | Usuń event w Outlooku → po 12h serwis odtwarza | ✅ |
| Outlook mail toggle (Wave 5) | Włącz toggle → przypomnienie sesji idzie z Outlooka | ✅ |
| AvailabilityTypeId SSOT | Stwórz sesję typu “online external link” → SessionCard pokazuje tę nazwę | ✅ |
| AddSessionModal custom location | ”Inna lokalizacja” → wpisz “kolorowa” → SessionDetailView ma “kolorowa” w dropdownie | ✅ |
| iDEAL dla pacjenta NL | Terapeuta z Connect NL → pacjent widzi iDEAL obok karty | ✅ |
| iDEAL nieaktywne dla PL | Terapeuta z Connect PL → pacjent widzi tylko kartę | ✅ |
| PDSS-SR / BAT-PL / 4DKL | Wyślij formularz → pacjent wypełnia → scoring zgodny z PDF manuala | ✅ |
| 4DKL scoring valueMap | Odpowiedź 3 lub 4 → score = 2 (nie 3 / 4) | ✅ |
| PDF restrukturyzacji poznawczej | Zatwierdź restrukturyzację → kliknij PDF → plik się pobiera | ✅ |
| Toggle “Blokuj zapisy online” | Włącz → publiczny profil zwraca 404 dla GetAvailability | ✅ |
| Toggle “Respect external busy” | Wyłącz → therapist-side widzi sloty mimo busy w Google | ✅ |
| Command palette ⌘K | ⌘K na /settings → wpisz “platnosci” → matchuje Płatności | ✅ |
| Command palette substring strict | Wpisz “rodo” → matchuje tylko Prywatność (nie 8 sekcji) | ✅ |
| SessionsList sort direction | Kliknij toggle → najstarsze sesje na górze | ✅ |
| aits-agent telegram + AppInsights | Wyślij “/help” → bot odpowiada listą komend | ✅ |
| aits-agent /newjira | /newjira <temat> → ticket SCRUM-NNNN utworzony | ✅ |
| aits-agent /analyse | /analyse SCRUM-1591 → analiza pojawia się jako komentarz | ✅ |
| Admin regenerate recurring sessions | POST /api/admin/maintenance/recurring-sessions/regenerate → 200 OK | ✅ |
| Bulk-create VideoInviteToken sync | Stwórz batch sesji → terapeuta i pacjent w tym samym pokoju | ✅ |
| Public booking respektuje Google busy | Terapeuta blokuje slot w Google → publiczny widget go nie pokazuje | ✅ |
| IBAN zagraniczny | Wpisz IBAN NL/DE → walidacja przechodzi | ✅ |
| Waluta zamiast PLN | Zmień walutę na EUR → profil pacjenta + historia płatności w EUR | ✅ |
| Modal “Edytuj sesję” formatowanie | Otwórz modal → “Aktualny” i “Nowy termin” mają identyczny wygląd | ✅ |
| Formularze pacjenta pl-NL | i18n.language='pl-NL' → formularz po polsku, nie po angielsku | ✅ |
| Pokój video z wygasłym tokenem | Wygaśnięty JWT w localStorage + klik linka video → zostaje w pokoju | ✅ |
| Multi-replica audio (SCRUM-1591) | Upload audio na 2 replikach → transkrypcja kończy się Completed | ✅ |
| Docplanner removed | GET /api/docplanner/* → 404 | ✅ |
Artykuł przygotowany przez zespół Therapy Support