background

Case Study: Anatomia systemu szytego na miarę. Techniczny Deep-Dive w architekturę transformacji HARMO [1/2]

Gdy „Cloud-First” nie jest jedyną słuszną drogą

Współczesna inżynieria oprogramowania zdaje się podążać w jednym kierunku: chmura, architektura mikroserwisowa, aplikacje typu SPA (Single Page Application) i podejście mobile-first. To standardy, które w większości przypadków sprawdzają się doskonale. Jednak w GOTOMA SOFTWARE HOUSE wierzymy, że rola architekta systemowego nie polega na bezrefleksyjnym podążaniu za trendami, ale na doborze narzędzi, które najlepiej rozwiązują unikalne problemy biznesowe i operacyjne klienta.

Projekt realizowany dla firmy HARMO jest tego doskonałym przykładem. Odsłaniamy karty i pokazujemy techniczną stronę tego przedsięwzięcia.

Opowiemy o tym, dlaczego w 2024 roku zdecydowaliśmy się napisać aplikację desktopową w technologii WPF zamiast modnego Reacta. Wyjaśnimy, jak zbudowaliśmy nowoczesny pipeline CI/CD dla izolowanego środowiska on-premise. Przeanalizujemy bolesny proces migracji danych z systemu legacy i pokażemy, jak algorytmy synchronizacji radzą sobie z pracą offline.

To tekst dla inżynierów, napisany przez inżynierów. Bez marketingowego pudrowania rzeczywistości – tylko kod, architektura i lekcje wyciągnięte z pola walki.

Architektura hybrydowa – W poszukiwaniu „Flow” dewelopera

Podstawowym założeniem projektowym było zrozumienie specyfiki pracy użytkownika końcowego. W przypadku HARMO byli to programiści i inżynierowie DevOps. Ich praca charakteryzuje się wysokim tempem, częstym przełączaniem kontekstu (Context Switching) i koniecznością precyzyjnego rejestrowania czasu pracy (Time Tracking) z dokładnością co do minuty.

Desktop vs Web: Dlaczego .NET/WPF wygrał z przeglądarką?

Pierwszym dylematem architektonicznym był wybór platformy dla aplikacji klienckiej. Naturalnym odruchem w dzisiejszych czasach jest budowa aplikacji webowej (SPA) lub wykorzystanie wrapperów typu Electron. Jednak po głębokiej analizie wymagań niefunkcjonalnych, zdecydowaliśmy się na natywną aplikację desktopową w technologii .NET/WPF.

Dlaczego odrzuciliśmy podejście webowe (PWA/SPA)? Deweloperzy w HARMO pracują na wielu narzędziach jednocześnie (IDE, terminale, kontenery, przeglądarka z dokumentacją). Kolejna karta w przeglądarce to ryzyko. Ryzyko przypadkowego zamknięcia, ryzyko „uśpienia” procesu przez mechanizmy oszczędzania energii przeglądarki (co jest zabójcze dla precyzji timerów) oraz trudność w obsłudze skrótów globalnych.

Technologia WPF (Windows Presentation Foundation) pozwoliła nam na osiągnięcie kilku kluczowych celów:

  • Lekki footprint: W przeciwieństwie do Electrona, który uruchamia osobną instancję Chromium dla każdej aplikacji, natywny .NET współdzieli zasoby runtime’u. Zależało nam, aby aplikacja do śledzenia czasu była „niewidzialna” dla systemu – zużywała minimalną ilość RAM i CPU, nie wpływając na kompilację kodu czy działanie maszyn wirtualnych dewelopera.
  • Głęboka integracja z OS: Potrzebowaliśmy dostępu do funkcji systemowych niskiego poziomu. Obsługa Tray Icon (ikony w zasobniku systemowym) pozwala na szybki podgląd stanu timera bez otwierania okna. Globalne skróty klawiszowe (np. CTRL+ALT+S do start/stop) umożliwiają sterowanie aplikacją będąc wewnątrz IDE, co drastycznie redukuje opór przed logowaniem czasu.
  • Targetowanie platformy: W pierwszej fazie projektu świadomie ograniczyliśmy się do systemu Windows, który stanowił dominujące środowisko pracy w HARMO. Pozwoliło to na szybsze dowiezienie wartości (Time-to-Market) i uproszczenie testów. Architektura UI została jednak odseparowana od logiki biznesowej (wzorzec MVVM), co zostawia otwartą furtkę do migracji na rozwiązania cross-platformowe (np. Avalonia UI lub MAUI) w przyszłości, bez konieczności przepisywania warstwy Core.

Komunikacja: REST dla danych, WebSocket dla zdarzeń

Backend systemu musiał obsłużyć dwa rodzaje ruchu: standardowe operacje na danych oraz komunikację w czasie rzeczywistym. Zastosowaliśmy podejście hybrydowe:

  • REST API: Obsługuje 90% ruchu. Operacje CRUD na projektach, zadaniach, pobieranie konfiguracji czy historii logów. REST jest stateless, łatwo skalowalny, cache’owalny i prosty w debugowaniu. To solidny fundament.
  • WebSocket: Tutaj dzieje się magia synchronizacji. Kiedy w zespole projektowym pracuje kilka osób, informacja o tym, kto aktualnie pracuje nad danym zadaniem, czy zadanie zostało zablokowane, lub czy PM zmienił priorytet, musi być dostarczona natychmiast. Polling (cykliczne odpytywanie serwera) byłby nieefektywny i generowałby zbędny ruch. WebSocket utrzymuje stałe połączenie, pozwalając na wysyłanie zdarzeń (Events) typu TimerStarted, TaskLocked czy NotificationReceived z opóźnieniem rzędu milisekund.

Offline-First i Strategia Synchronizacji

Największym wyzwaniem w aplikacjach desktopowych jest obsługa braku sieci. Deweloper jadący pociągiem lub pracujący przy niestabilnym VPN nie może stracić możliwości logowania czasu.

Zaimplementowaliśmy strategię Offline-First. Aplikacja desktopowa posiada lokalny storage (lekka baza danych SQLite lub zserializowany plik), który pełni rolę bufora:

  • Buforowanie: Wszystkie rozpoczęte i zakończone interwały czasu, notatki oraz zmiany statusów są najpierw zapisywane lokalnie.
  • Batched Sync: Po wykryciu powrotu do sieci (co obsługuje dedykowany serwis ConnectivityManager), aplikacja nie wysyła setek pojedynczych zapytań. Zamiast tego formuje „paczkę” (Batch) ze wszystkimi zmianami i wysyła ją w jednym lub kilku requestach.
  • Rozwiązywanie Konfliktów: Co się stanie, jeśli deweloper edytował zadanie offline, a w międzyczasie PM je zamknął? Backend stosuje ustaloną politykę (Policy-Based Resolution). W większości przypadków wygrywa ostatnia zmiana (Last Write Wins), ale dla krytycznych anomalii system tworzy osobny log „Do weryfikacji”, nie nadpisując danych bezpowrotnie.

Intelligence on the Edge: Gdzie żyje „mózg”?

Ciekawym aspektem jest podział logiki biznesowej między klienta (Desktop) a serwer. Zastosowaliśmy model mieszany. Thick Client (częściowo): Logika odpowiedzialna za natychmiastowe podpowiedzi (np. „ostatnio używane zadania”, „projekty przypisane do mnie”) działa lokalnie. Dzięki temu interfejs jest ultra-responsywny, nawet przy słabym łączu. Server-Side Intelligence: Bardziej zaawansowane algorytmy, takie jak model scoringu podpowiadający zadania na podstawie analizy pracy całego zespołu czy wykrywanie anomalii w czasie pracy, są liczone na backendzie. Serwer cyklicznie przesyła do klienta zaktualizowane wagi i sugestie. Pozwala to nam na iteracyjne ulepszanie algorytmów bez konieczności wymuszania aktualizacji aplikacji u wszystkich użytkowników.

DevOps w twierdzy. Infrastruktura On-Premises

Decyzja o wdrożeniu systemu w modelu On-Premises (na własnej infrastrukturze klienta) była podyktowana względami bezpieczeństwa i polityką firmy HARMO. W erze, gdzie „git push” zazwyczaj kończy się wdrożeniem na AWS czy Azure, musieliśmy zaprojektować nowoczesny proces CI/CD dla środowiska izolowanego.

Konteneryzacja: Docker w środowisku „Legacy”

Mimo że infrastruktura klienta opierała się na klasycznych maszynach wirtualnych, nie chcieliśmy rezygnować z dobrodziejstw konteneryzacji. Backend (napisany w .NET Core), baza pomocnicza oraz serwisy towarzyszące (np. Redis do cache’owania) zostały spakowane w kontenery Docker.

Na serwerach produkcyjnych klienta wykorzystaliśmy Docker Compose. Dlaczego nie Kubernetes (K8s)? Przy skali projektu (kilkudziesięciu użytkowników, kilka kontenerów) i ograniczeniach zasobowych, K8s byłby przysłowiowym strzelaniem z armaty do wróbla. Docker Compose zapewnił nam wystarczającą orkiestrację, łatwość zarządzania konfiguracją (YAML) i restart policies, przy minimalnym narzucie operacyjnym dla działu IT klienta.

Hybrydowy pipeline CI/CD

Największym wyzwaniem było dostarczenie kodu z naszego repozytorium (GitLab w chmurze) na serwery klienta, które są schowane za VPN i firewallem. Zbudowaliśmy pipeline dwuetapowy:

  • Build & Test (Cloud): Kompilacja kodu, uruchomienie testów jednostkowych i integracyjnych oraz budowa obrazów Dockerowych odbywa się w chmurze, na naszych runnerach CI.
  • Delivery (On-Prem Tunneling): Artefakty (obrazy kontenerów, pliki instalacyjne .msi) są przesyłane do „strefy zrzutu” (dedykowane repozytorium/registry) wewnątrz infrastruktury klienta. Dostęp jest realizowany przez bezpieczny tunel VPN lub w modelu „Pull”, gdzie wewnętrzny agent klienta cyklicznie sprawdza dostępność nowych, podpisanych cyfrowo paczek.

Dystrybucja aplikacji desktopowej

Aktualizacja aplikacji zainstalowanej na komputerach pracowników to często koszmar administratorów. Aby tego uniknąć, zaimplementowaliśmy mechanizm Auto-Update. Aplikacja desktopowa przy starcie (oraz cyklicznie w tle) odpytuje wewnętrzny serwer (po prostym protokole HTTP lub przez zasób SMB) o plik manifestu wersji. Jeśli wykryje nowszą wersję, pobiera ją w tle. Wdrożyliśmy obsługę tzw. Breaking Changes. Jeśli zmiana w API backendu jest niekompatybilna wstecznie, aplikacja wymusza aktualizację przed pozwoleniem użytkownikowi na dalszą pracę. To drastycznie redukuje ilość błędów wynikających z niespójności wersji.

Tech stack backendowy i baza danych

Sercem systemu jest MS SQL Server. Wybór relacyjnej bazy danych od Microsoftu był podyktowany jej doskonałą wydajnością przy złożonych zapytaniach analitycznych. Dane o czasie pracy mają strukturę hierarchiczną i silnie powiązaną (Użytkownik -> Projekt -> Zadanie -> Log Czasu -> Tagi -> Statusy rozliczeniowe). Bazy NoSQL, choć świetne do zapisu dużej ilości zdarzeń, mogłyby polec przy generowaniu skomplikowanych raportów przekrojowych (Joiny).

Tabela logów czasu – najbardziej obciążona w systemie – została zoptymalizowana pod kątem odczytu (Read-Heavy). Zastosowaliśmy:

  • Indeksy złożone (Composite Indexes): Na kolumnach najczęściej używanych w filtrowaniu (UserId, ProjectId, Date, BillingType).
  • Partycjonowanie: Dane historyczne są logicznie dzielone (np. po latach), co przyspiesza operacje na danych bieżących.
  • Optymalizacje ORM: Choć używamy Entity Framework Core dla wygody programistycznej, krytyczne ścieżki (tzw. hot paths) obsługujące raportowanie wykorzystują czysty SQL lub Dapper, aby ominąć narzut generowany przez śledzenie zmian w EF.

W pierwszej części technicznego deep-dive’u udowadniamy, że w starciu ze specyficznymi wymaganiami wydajnościowymi i security, natywny WPF i strategia Offline-First deklasują podejście webowe – a już teraz zapraszamy na drugą odsłonę, w której zejdziemy jeszcze głębiej w kod i twarde dane z wdrożeni.

Jeśli chcesz dowiedzieć się więcej, zapraszamy do kontaktu z naszymi specjalistami.

Blog

Czytaj więcej