[13]
METROPOLIA GZM — GŁOSOWANIE OBYWATELSKIE NA MAPIE 3D
▓ TIER B · ROK 2026 · STATUS: DZIAŁA · JĘZYKI: TYPESCRIPT / PYTHON / SQL
Dane geoprzestrzenne i mapa 3D w przeglądarce, z głosowaniem odpornym na wyścigi
[FIG. 1] MISJA
Pracuję z danymi geoprzestrzennymi i buduję na nich interfejsy mapowe w 3D (MapLibre GL + Three.js) z realną interakcją, nie dekoracyjną animacją. Tak powstała aplikacja mapowa budżetu obywatelskiego Górnośląsko-Zagłębiowskiej Metropolii, projekt z hackathonu BLOXHUB nagrodzony wyróżnieniem, zbudowany w czteroosobowym zespole: trzon postawił Mikołaj Paweł Sapek (mikolajpawelsapek.eu), a obok mnie pracowali Maciej Sachse (maciejsachse.com) i Adam Bączek. Interaktywna mapa 3D pokazuje projekty z 14 miast, granice dzielnic, trasy rowerowe i POI, a mieszkaniec przegląda projekty, głosuje, dodaje propozycje ze zdjęciami i modelami 3D. Dane płyną z otwartych źródeł (GZM CKAN, Gliwice Open Data, GUGiK, OSM) i synchronizują się codziennie cronem — ETL piszę na czystej bibliotece standardowej, idempotentnie, bez ani jednej zależności pip, bo zależało mi na zerowym bagażu. Głosowanie projektuję jak system współbieżny: jeden głos na osobę, ochrona przed wyścigami i walidacja po stronie serwera, a pod spodem 38 migracji SQL z RLS, triggerami i hardeningiem, łącznie z formalnym audytem bezpieczeństwa z findingami P0–P2. Logowanie działa przez PLZ: skan kodu QR w aplikacji mobilnej, z własnoręcznie zbudowanym mostem do Supabase Auth, bo PLZ nie oferuje OAuth. W zespole pracuję z dyscypliną: CLAUDE.md jest konstytucją projektu, a jego reguły cytujemy w komentarzach kodu, całość domyka CI/CD w GitHub Actions z testami e2e w Playwrighcie.
[FIG. 2] ARCHITEKTURA
najedź na blok, by zobaczyć opis
[FIG. 3] WYZWANIA
[+][CH-01]
PLZ nie ma OAuth ani OIDC — logowanie to kod QR skanowany w aplikacji mobilnej, a callback wraca z własnym xsessionId, nie identyfikatorem naszej sesji. Trójstronny handshake przeglądarka ↔ PLZ ↔ Supabase trzeba było zbudować ręcznie, łącznie z pułapką typów tokenów: generateLink dla niepotwierdzonego konta zwraca token typu signup, który frontend odrzuca błędem 403 — rozwiązanie to potwierdzenie e-maila przez Admin API i wygenerowanie linku po raz drugi, już jako magiclink. Osobną klasą problemu był replay: callback PLZ potrafi przyjść dwukrotnie, a naiwny zapis tokena ponownie „uzbrajał” skonsumowaną sesję. Warunkowy UPDATE po status='pending' czyni operację atomową — drugi callback trafia w zero wierszy, a sessionId nigdy nie podróżuje w URL-u, tylko w cookie HttpOnly.
[+][CH-02]
Otwarte dane rządowe walczyły z nami na każdym poziomie stosu. WAF przed CKAN-em resetował połączenia z runnera GitHub Actions z pythonowym User-Agentem, a Supabase równocześnie odrzucał klucz serwerowy, gdy żądanie wyglądało „przeglądarkowo” — dwa systemy stawiały sprzeczne wymagania na ten sam nagłówek, więc sync dobiera User-Agent per cel. Dalej: część CSV przychodzi w Windows-1250 (dekodowanie jako UTF-8 niszczy każdy polski diakrytyk — stąd fallback strict-UTF-8-then-cp1250), Gliwice mają własny schemat kolumn różny od standardu GZM, zbiór zabytków Rudy Śląskiej ma odwrócone osie współrzędnych, a duplikaty kluczy wywracały upsert błędem PG 21000 — deduplikacja przed zapisem zachowuje ostatnie wystąpienie. Całość jest idempotentna i działa codziennie z retry i backoffem, na czystej bibliotece standardowej Pythona.
[+][CH-03]
Najgroźniejszy finding audytu siedział w niuansie modelu ról Postgresa: guard chroniący katalog PostGIS sprawdzał session_user, ale PostgREST łączy się jako rola authenticator i robi SET ROLE — session_user zostaje authenticator, więc strażnik nigdy nie blokował i anonim mógł uszkodzić katalog SRID, wyłączając całe PostGIS. Do tego RLS nie chroni pojedynczych kolumn: właściciel projektu mógł podmieniać pola systemowe jak vote_count czy status, bo polityki sprawdzały tylko created_by. Migracja naprawcza przeszła na triggery SECURITY INVOKER używające current_user, REVOKE praw zapisu i wymuszanie pól systemowych dla ról publicznego API — a wcześniej inny trigger, chroniący ręcznie rozrzucone współrzędne projektów, okazał się „cichą regułą”, która nigdy się nie odpalała, i został przepisany z flagi na porównanie geometryczne.
[FIG. 4] WARSTWA AI
W runtime nie działa żaden model — to klasyczny stack geodanych, bez warstwy LLM. AI pracowało przy budowie: CLAUDE.md pełnił rolę konstytucji projektu, której reguły są cytowane w komentarzach kodu (wzorzec ref+state, cancelled flag, zakaz podwójnych castów), spec migracji bezpieczeństwa był pisany wprost dla agenta z kryteriami akceptacji, a audyt kodu wylistował findingi P0–P2 z linkami plik:linia i werdyktami.
[FIG. 2A] MIASTO O KAŻDEJ GODZINIE
DÓŁ = PLAŻA (SZCZYT 13:00) · ŚRODEK = CENTRUM (SZCZYT 20:00)
GRACEFUL DEGRADATION — KLIKNIJ, BY ODCIĄĆ ŹRÓDŁO
→ /api/health: heatmapa: real · eventy: real · miejsca: real — ten sam kontrakt Pydantic w obu trybach
[FIG. 5] GALERIA





