Przejdź do głównej zawartości

Architektura

Trzy warstwy​

  1. Frontend (przeglądarka) — cały interfejs, wszystkie obliczenia (zdrowie finansowe, FIRE, projekcje, insighty), powiadomienia in-app, szyfrowanie end-to-end danych oraz tryb lokalny (localStorage). Działa bez serwera.
  2. Managed backend — Supabase — baza (gospodarstwa, subskrypcje), Auth, Row Level Security i funkcje Postgres (logika dostępu wykonywana w bazie).
  3. Własny kod serwerowy — funkcje serverless (route'y /api/* w Next.js) potrzebne tam, gdzie wchodzi sekret lub granica zaufania — przede wszystkim płatności (webhook, checkout) oraz wysyłka e-mail / web push.

Do czego potrzebny jest serwer​

  • Webhook płatności — weryfikacja podpisu i zapis statusu subskrypcji kluczem service-role (nie może trafić do przeglądarki).
  • Tworzenie checkoutu — wywołanie API dostawcy tajnym kluczem.
  • Powiadomienia e-mail / web push — wymagają harmonogramu i kluczy.

Czego serwer NIE wymaga​

Przechowywania planu, logowania, obliczeń finansowych, insightów, powiadomień in-app ani szyfrowania — to obsługuje frontend + Supabase (RLS). Klucz szyfrujący powstaje na urządzeniu i nigdy nie trafia na serwer.

Wydajność​

  • Podział kodu. Ekrany dla osób niezalogowanych (strona startowa, beta, dokumenty prawne), kreator importu oraz słowniki niemiecki i francuski są osobnymi paczkami. Zalogowany użytkownik ich nie pobiera, dopóki z nich nie skorzysta. Strona startowa nadal jest renderowana na serwerze, więc wyszukiwarki widzą jej treść.
  • Język. Polski i angielski są w paczce startowej. Po wybraniu niemieckiego słownik doładowuje się w tle; na tę chwilę interfejs pokazuje angielski.
  • Kursy w tle. Gdy karta lub aplikacja jest w tle, odświeżanie kursów (kryptowaluty, ETF, akcje) jest wstrzymane. Po powrocie, jeśli minął pełny okres odświeżania, kursy pobierają się od razu.
  • Analiza. Wynik kondycji, zwroty (XIRR, TWR), FIRE i oś czasu liczą się raz na zmianę danych, a nie przy każdym przełączeniu zakładki.
  • Pulpit. Wynik zdrowia, trend 12 miesięcy, zdanie „co się zmieniło" i stan rytuału liczą się raz na zmianę danych — tak samo karta głównego celu i panel boczny. Przełączanie zakładek czy otwieranie paneli niczego nie przelicza.
  • Portfel. Na komputerze z modułem „Szczegóły inwestycji" montowana jest tylko tabela, na telefonie tylko karty (wcześniej oba, jedno ukryte). Wiersze odświeżają się tylko przy zmianie własnej pozycji. Projekcja każdej pozycji liczy się raz na zmianę danych lub horyzontu.
  • Pulpit. Na telefonie montowana jest tylko aktywna zakładka i kompaktowy wykres struktury; na komputerze wszystkie sekcje, wykres z legendą i wykres przychodów — bez ukrytych kopii.
  • Wybór wariantu (telefon / komputer) zna szerokość okna już przy pierwszym rysowaniu ekranu, więc na komputerze nie mignie na chwilę wersja mobilna.
  • Paczka startowa. Biblioteka Supabase (konto w chmurze), dane trybu demo, słowniki EN/DE/FR i kod Pulpitu doładowują się dopiero, gdy są potrzebne — strona startowa i aplikacja w trybie lokalnym ładują mniej kodu. Prawy panel z podsumowaniami (od 1280 px) na węższych ekranach w ogóle się nie liczy ani nie renderuje.
  • Zapis stanu. Zmiany trafiają do pamięci przeglądarki z krótkim opóźnieniem (seria zmian = jeden zapis), a przy zamknięciu lub schowaniu karty od razu. Pola tekstowe planu (nazwa planu, kategorie, inne dochody w FIRE) zapisują się po wyjściu z pola.
  • Nawigacja bez rozmycia tła (rozmycie liczyło się przy każdej klatce przewijania); pasek stanu działa tylko na komputerze.
  • Znak GrapeVest. Gradienty logo są zdefiniowane raz na stronę, więc logo wygląda tak samo na serwerze i w przeglądarce (bez ponownego renderowania przy starcie strony).

PWA​

Manifest + service worker umożliwiają instalację na telefonie i pracę offline w trybie lokalnym. To także fundament pod przyszłe web push.

Aktualizacje: po nowym wdrożeniu nowy service worker czeka w stanie waiting, a aplikacja pokazuje toast „Nowa wersja — Odśwież". Kliknięcie wysyła do SW SKIP_WAITING, a po controllerchange strona przeładowuje się na nową wersję. Dzięki temu użytkownik nie zostaje na starym cache bez sygnału (a jednocześnie nie przerywamy mu pracy bez pytania).

Trzy zabezpieczenia pilnują, żeby karta otwarta w trakcie wdrożenia nie utknęła na starym kodzie (version skew):

  • Sprawdzanie przy powrocie do karty. Rejestracja SW odpytuje o aktualizację nie tylko przy starcie, ale też przy każdym powrocie fokusu do karty (z przepustnicą), więc toast „Nowa wersja" pojawia się także w sesjach, które przeżyły wdrożenie — również po przywróceniu karty z pamięci (bfcache).
  • Payloady nawigacji RSC zawsze z sieci. Service worker nie cache'uje odpowiedzi routera (nagłówek RSC / parametr _rsc) — stary payload wskazywałby chunki JS poprzedniego wdrożenia i „przypinał" starą wersję.
  • Nagłówek X-App-Build. Serwer zwraca hash aktualnie wdrożonego builda. Gdy import pliku się nie powiedzie, aplikacja porównuje go z buildem wkompilowanym w działającą kartę i — przy rozjeździe — zamiast mylącego błędu prosi wprost o odświeżenie strony.

Bezpieczny margines u góry (notch / pasek stanu). Cały układ używa viewport-fit: cover, więc treść sięga krawędzi ekranu — górny odstęp od paska stanu (zegar, sieć, bateria, notch / Dynamic Island) bierzemy z env(safe-area-inset-top). Na części iPhone'ów WebKit potrafi zwrócić tę wartość jako 0 (i to zarówno w Safari, jak i w PWA z ekranu głównego), przez co nagłówek i pływające przyciski wchodziły pod pasek stanu. Dlatego górny inset ma gwarantowaną dolną granicę: zmienna CSS --safe-area-top = max(env(safe-area-inset-top), 1.5rem). Na zdrowych urządzeniach to zwykłe „nic nie zmieniaj", a gdy env skłamie 0 — układ i tak trzyma bezpieczny odstęp. Używamy jej wszędzie tam, gdzie liczy się górna krawędź (powłoka aplikacji, ekrany pełnoekranowe, przyciski ⚙️ / zaproszenia, panele wysuwane).

Przewijanie na mobile. Jedynym scrollerem jest kontener .app-scroll (position: fixed; inset: 0) — nie okno (body { overflow: hidden }). Dwie konsekwencje:

  • Brak poziomego scrolla. .app-scroll przycina w poziomie przez overflow-x: hidden (nie clip — to drugie iOS < 16 ignoruje i wtedy z overflow-y: auto wraca poziomy pasek).
  • Nowa strona zawsze od góry. Domyślne „scroll to top" Next.js celuje w okno, więc tu nie działa — komponent ScrollToTopOnNav zeruje scroll samego kontenera przy zmianie ścieżki (window.scrollTo był próbowany i pogarszał sprawę). Zmiana tylko parametrów zapytania (/?saved=…) nie przewija.