Strona i system zapisów na unconference D-Day w Hakierspejsie w Łodzi.
- Kiedy: sobota 8 sierpnia 2026, 14:00–22:00
- Gdzie: Hakierspejs, Zielona 30 LU3, Łódź
- Wstęp: darmowy, zapisy obowiązkowe (20 miejsc + 20 na liście rezerwowej)
- Zapisy startują: niedziela 26 lipca 2026, 15:00 czasu polskiego
Powyższe daty to wartości domyślne — wszystkie są konfigurowalne przez zmienne środowiskowe, patrz Zmiana terminu wydarzenia.
Dwie binarki Go (bez CGO), wspólny pakiet z datami i konfiguracją terminu:
| Komponent | Ścieżka | Opis |
|---|---|---|
| Serwer WWW | . (main.go, server.go, templates.go) |
landing + rejestracja; index.html, privacy.html i style.css są go:embed-owane w binarce |
| Bot Matrix | cmd/bot, internal/matrixbot |
nasłuchuje !start, wysyła DM z prywatnym linkiem (rejestracja albo panel) |
| Bramka czasowa i daty | internal/regwindow |
jedyne źródło prawdy dla terminów; generuje polskie opisy dat |
| Baza | internal/store |
SQLite przez modernc.org/sqlite (czysty Go) |
| Style | style.css |
jedyny arkusz stylów; sekcje body.landing / body.page / body.privacy |
Trasy serwera WWW:
| Trasa | Opis |
|---|---|
GET / |
landing (index.html); tylko dokładnie /, żadnego przeglądania katalogu |
GET /register?t=… |
formularz zapisu dla ważnego tokenu |
POST /register |
zapis (miejscowość + e-mail); tożsamość wyłącznie z tokenu |
GET /api/count |
JSON: obłożenie miejsc + wszystkie daty i ich opisy (patrz niżej) |
GET /api/registered?h=@user:hs |
wewnętrzne, dla bota; wymaga Authorization: Bearer $INTERNAL_TOKEN, bez tokenu 404 |
GET /api/registrations?since=<id> |
wewnętrzne, dla bota: zgłoszenia o id > since do ogłoszeń na kanale; wymaga Authorization: Bearer $INTERNAL_TOKEN, bez tokenu 404, bez e-maila i miejscowości |
GET /panel?t=… |
panel uczestnika dla ważnego magic linku: status + przycisk „Wycofaj udział" |
POST /panel |
wycofanie udziału (tożsamość wyłącznie z tokenu) |
GET /admin?t=… |
panel admina: podgląd wszystkich zgłoszeń; wymaga ADMIN_TOKEN, bez tokenu 404 |
GET /privacy |
polityka prywatności (privacy.html) |
GET /style.css |
wspólny arkusz stylów wszystkich stron (style.css) |
GET /healthz |
health check (używany też przez dday -healthcheck) |
- Użytkownik pisze
!startdo bota (DM albo pokój z allowlisty).!registerdziała nadal jako alias — komendą oficjalną (tą z landing page) jest!start. - Bot sprawdza bramkę czasową i — jeśli ma
INTERNAL_TOKEN— pyta/api/registered, czy ten handle już się zapisał. - Bot generuje token:
{handle, issued}zaszyfrowane kluczem age (odbiorcą jest klucz publiczny serwera), podpisane HMAC-em (TOKEN_SECRET), całość base64. - Bot wysyła w DM link
REGISTER_URL?t=<token>. GET /register?t=…odszyfrowuje i weryfikuje token: zły podpis → „Nieprawidłowy link", starszy niż 48 h (lub wystawiony w przyszłości ponad 5 min zapasu na zegar) → „Link wygasł".- Formularz zbiera miejscowość i e-mail;
POSTzapisuje rekord w SQLite i przydziela numer uczestnika (AUTOINCREMENT). - Numery 1–20 to miejsca potwierdzone, 21–40 lista rezerwowa, powyżej — brak miejsc.
- Link jest jednorazowy: po zapisie ten sam link pokazuje potwierdzenie, a próba
ponownego
POSTkończy się stroną „Już zapisany", bez drugiego rekordu.
Kolejne !start od kogoś, kto jest już zapisany (/api/registered), nie jest odmową —
bot wysyła w DM magic link do panelu (PANEL_URL?t=<token>), a na kanale zostawia
tylko publiczny nudge „sprawdź prywatne wiadomości" (numeru uczestnika nie ujawnia publicznie).
- Token panelu ma ten sam mechanizm co rejestracyjny (age + HMAC, TTL 48 h), ale niesie
inny kind (
panelvsreg), objęty HMAC-em. Dzięki temu link rejestracyjny nie otworzy panelu, a magic link do panelu nie pozwoli się zapisać. - W panelu widać nick, numer uczestnika i status (uczestnik / lista rezerwowa, pozycja). Jedyną akcją jest „Wycofaj udział" — usuwa rekord i zwalnia miejsce (osoba z rezerwy awansuje).
- Po wycofaniu można zapisać się ponownie:
!start→ bot widzi, że nie jesteś zapisany → link rejestracyjny (o ile są miejsca). - Gdy
PANEL_URLnie da się ustalić, bot degraduje się do dawnej publicznej odpowiedzi „jesteś już zapisany", zamiast wysyłać zepsuty link.
GET /admin to widok tylko do odczytu dla organizatora: podsumowanie obłożenia
(uczestnicy X/20, lista rezerwowa Y/20, łącznie) oraz tabela wszystkich zgłoszeń —
numer uczestnika, status (uczestnik / rezerwa #poz), nick, handle Matrix,
miejscowość, e-mail i data zapisu (strefa Europe/Warsaw).
Dostęp chroni ADMIN_TOKEN. Token można podać na dwa sposoby:
https://dday.hs-ldz.pl/admin?t=<ADMIN_TOKEN>
curl -H "Authorization: Bearer <ADMIN_TOKEN>" https://dday.hs-ldz.pl/admin
- Token bierze się z pliku
.env—make upgeneruje go raz i potem reużywa (grep ADMIN_TOKEN .env). - Pusty
ADMIN_TOKENwyłącza endpoint całkowicie (404), tak samo jakINTERNAL_TOKENwyłącza/api/registered. Zły lub brakujący token → 401. Porównanie jest constant-time. - URL zawiera sekret — nie wklejaj go na czat, do issue ani w screenshot.
Odpowiedź ma
Cache-Control: no-store, a globalneReferrer-Policy: no-referrerpilnuje, żeby token nie wyciekł nagłówkiem Referer. Kto ma token, ten widzi dane osobowe wszystkich zapisanych. - Panel nic nie modyfikuje: wycofanie udziału robi sam uczestnik w
/panel, a awaryjne usunięcie danych to CLIdday -delete <handle>(patrz sekcja RODO). - Status liczony jest z rangi (pozycji wśród aktualnych wierszy), więc po wycofaniu się uczestnika awans z listy rezerwowej widać od razu.
Rejestracja kończy się po stronie WWW, więc bot nie dowiaduje się o niej sam — cyklicznie
odpytuje wewnętrzny endpoint GET /api/registrations?since=<ostatnie ogłoszone id>
(bearer INTERNAL_TOKEN) i ogłasza to, czego jeszcze nie ogłosił, na kanale macierzystym:
🎉 alice dołącza do D-Day — uczestnik #5
🎉 bob zapisał(a) się na D-Day — lista rezerwowa, pozycja #2
- Bez danych osobowych. Ogłoszenie zawiera wyłącznie handle Matrix (jako mention
przez
matrix.to) i numer uczestnika albo pozycję na liście rezerwowej. E-mail i miejscowość nie wychodzą poza serwer — endpoint/api/registrationsw ogóle ich nie zwraca, a typ, którym posługuje się bot, nie ma na nie pól. - Bez powtórek po restarcie. Ostatnie ogłoszone
idjest zapisywane w tym samym pliku co cache DM (DM_CACHE_PATH, format{"dms":{…},"lastAnnounced":N}; stary, płaski format wczytuje się nadal, zlastAnnounced=0). - Pierwszy start bez stanu nie odtwarza historii: bot ustawia
lastAnnouncedna aktualne maksymalneidi ogłasza dopiero kolejne zapisy. - Wyłączenie: puste
ANNOUNCE_ROOM(iMATRIX_ROOM) albo brakINTERNAL_TOKEN.
Landing pobiera stan z /api/count (licznik zapisanych, pasek, lista rezerwowa,
flaga open i daty). Bez API (np. GitHub Pages) strona degraduje się do wartości
wpisanych na sztywno w index.html.
Serwer WWW:
| Zmienna | Domyślnie | Opis |
|---|---|---|
PORT |
3329 |
port nasłuchu |
STATIC_DIR |
(puste) | katalog z index.html/privacy.html/style.css zamiast wersji wbudowanej (dev) |
DB_PATH |
./dday.db |
plik bazy SQLite |
AGE_KEY |
config/dday_ed25519 |
ścieżka klucza prywatnego age |
AGE_KEY_DATA |
(puste) | ten sam klucz przekazany base64 (ma pierwszeństwo; używane w kontenerze) |
TOKEN_SECRET |
(puste) | wspólny sekret HMAC dla tokenów; musi być identyczny u bota |
INTERNAL_TOKEN |
(puste) | bearer chroniący /api/registered i /api/registrations; puste = endpointy wyłączone (404) |
ADMIN_TOKEN |
(puste) | token chroniący /admin; puste = panel admina wyłączony (404) |
REGISTRATION_OPEN |
(puste) | 1/true/yes wymusza otwarte zapisy; inaczej decyduje bramka czasowa |
REGISTRATION_OPEN_AT |
2026-07-26 15:00 |
moment otwarcia zapisów |
EVENT_START_AT |
2026-08-08 14:00 |
początek wydarzenia |
EVENT_END_AT |
2026-08-08 22:00 |
koniec wydarzenia |
Bot Matrix (cmd/bot, konfiguracja zwykle w matrix.env):
| Zmienna | Domyślnie | Opis |
|---|---|---|
MATRIX_HOMESERVER |
wymagane | np. https://matrix.org |
MATRIX_USER |
wymagane | np. @ddaybot:matrix.org |
MATRIX_PASSWORD |
wymagane | hasło bota |
MATRIX_ROOM |
(puste) | pokój używany przez skrypty make matrix-hello / matrix-send |
REGISTER_URL |
https://dday.hs-ldz.pl/ |
baza linku rejestracyjnego (jej origin służy też do /api/registered) |
PANEL_URL |
origin(REGISTER_URL) + /panel |
baza magic linku do panelu uczestnika |
AGE_PUB |
config/dday_ed25519.pub |
klucz publiczny age, którym bot szyfruje tokeny |
AGE_PUB_DATA |
(puste) | ten sam klucz base64 (ma pierwszeństwo) |
TOKEN_SECRET |
(puste) | jak wyżej — musi zgadzać się z serwerem |
INTERNAL_TOKEN |
(puste) | włącza pytanie /api/registered przed wydaniem linku oraz ogłaszanie nowych zapisów (/api/registrations) |
ANNOUNCE_ROOM |
MATRIX_ROOM |
pokój, w którym bot ogłasza nowe zapisy; puste = ogłaszanie wyłączone |
ANNOUNCE_INTERVAL |
30s |
jak często bot odpytuje /api/registrations (format time.ParseDuration) |
ALLOWED_ROOMS |
(puste) | lista room id po przecinku; puste = bot reaguje wszędzie |
DM_CACHE_PATH |
(puste) | plik stanu bota (cache handle → room id + lastAnnounced), przeżywa restart |
REGISTRATION_OPEN, REGISTRATION_OPEN_AT |
jak wyżej | bot używa tej samej bramki i tego samego opisu daty |
Formaty dat (*_AT): RFC3339 (2026-07-26T15:00:00+02:00) albo
2006-01-02 15:04 / 2006-01-02 15:04:05 / 2006-01-02 interpretowane w strefie
Europe/Warsaw. Wartość niepoprawna → wpis w logu i użycie domyślnej (bez crasha).
make keys # jednorazowo: para kluczy age do config/ (gitignored)
make run # serwer WWW na :3329 (index.html z binarki)
make dev # to samo, ale index.html czytany z dysku (live edit)
make bot # bot Matrix, konfiguracja z ./matrix.env
make check # walidacja matrix.env
make test # go test -race ./...Sekrety trzymamy poza repo: matrix.env (wzór w matrix.env.example), .env
generowany przez make up i katalog config/ — wszystkie są w .gitignore.
Podgląd z niestandardowymi datami:
REGISTRATION_OPEN_AT="2026-09-01 12:00" \
EVENT_START_AT="2026-09-05 10:00" EVENT_END_AT="2026-09-05 18:00" make runmake up # generuje .env i odpala docker compose up -d --build
make logs # docker compose logs -f
make down # zatrzymanie stackumake up sprawdza obecność kluczy age i matrix.env, a następnie zapisuje .env
z: AGE_KEY_DATA, AGE_PUB_DATA (klucze base64 — plik 0600 byłby nieczytelny dla
użytkownika nonroot w obrazie distroless), INTERNAL_TOKEN, TOKEN_SECRET i
ADMIN_TOKEN (losowane raz i potem reużywane) oraz REGISTRATION_OPEN
(domyślnie 1).
docker-compose.yml wystawia serwis dday przez Traefika na
dday.hs-ldz.pl (entrypoint websecure, certresolver myresolver) i uruchamia
serwis bot. Dane trwałe: wolumen dday-data (/data/dday.db) i bot-data
(cache DM-ów bota). Zmienne REGISTRATION_OPEN_AT / EVENT_START_AT /
EVENT_END_AT są przepuszczane do obu serwisów — ustaw je w .env, jeśli
zmieniasz termin.
Terminy pochodzą wyłącznie z internal/regwindow: serwer używa ich na stronach
„zapisy nieotwarte", bot w odpowiedzi „jeszcze nie wystartowały", a landing
pobiera je z /api/count (openAt, eventStartAt, eventEndAt + gotowe polskie
teksty openText, openHowto, openShort, openShortTime, eventText,
eventShort, eventShortTime, eventBadge). Polskie odmiany („niedziela”,
„lipca”, „w niedzielę”) generuje sam pakiet — nic nie synchronizuje się ręcznie.
Żeby przesunąć termin, ustaw w .env (deployment) albo w środowisku procesu:
REGISTRATION_OPEN_AT="2026-09-01 12:00"
EVENT_START_AT="2026-09-05 10:00"
EVENT_END_AT="2026-09-05 18:00"Restart kontenerów i tyle — landing, formularz i bot mówią to samo.
Ręcznie trzeba poprawić tylko rzeczy, których nie da się wyliczyć w runtime:
index.html:<meta name="description">i<meta property="og:description">— statyczne, bo crawlery nie wykonują naszego JS.index.html: literały fallbackowe (REG_OPEN,EVENT, teksty kafelków) — używane wyłącznie, gdy strona jest serwowana bez API (np. GitHub Pages). Warto je odświeżyć przy zmianie terminu, choć produkcja bierze daty z API.- Nagłówek tego README.
privacy.html— pole „Ostatnia aktualizacja”, jeśli zmieniacie samą politykę.
Na żądanie uczestnika (kontakt: kontakt@hakierspejs.pl) usuwamy jego rekord
z bazy zapisów. Służy do tego wbudowany tryb CLI binarki, wskazujący uczestnika
po identyfikatorze Matrix (matrix_handle):
# w kontenerze (usługa dday używa /data/dday.db z wolumenu):
docker compose run --rm dday -delete @user:homeserver
# lub bezpośrednio przy uruchomionej binarce (DB_PATH wskazuje bazę):
DB_PATH=./dday.db dday -delete @user:homeserverKomenda wypisuje wynik i kończy się kodem 0, gdy rekord został usunięty, albo
1, gdy takiego zapisu nie było (lub wystąpił błąd). Uwaga: numer uczestnika to
AUTOINCREMENT — po usunięciu nie jest odtwarzany i nie zostaje przydzielony
ponownie. To celowe i akceptowalne.
make test # go test -race ./...
make fmt vet # gofmt -w . && go vet ./...Pokrycie: pełny przepływ rejestracji (token → formularz → zapis → duplikat →
lista rezerwowa → komplet), TTL i manipulacja tokenem, bramka czasowa i parsowanie
*_AT, generowanie polskich opisów dat, pola /api/count, nagłówki
bezpieczeństwa, tryb -delete, ładowanie kluczy oraz logika bota (allowlista
pokojów, cache DM-ów, odpowiedzi przed otwarciem zapisów).
CI (GitHub Actions, .github/workflows) uruchamia gofmt -l, go vet,
go build, go test -race oraz build obrazu Dockera.