Konkurs fotograficzny — dokumentacja aplikacji

wersja 1.24.4
Brak wyników dla podanej frazy.

Architektura

Całość działa w chmurze Cloudflare, bez własnego serwera. Cztery warstwy: urządzenie → frontend → backend → dane (+ usługi zewnętrzne). Kliknij dowolny kafelek, by zobaczyć opis i ścieżkę pliku.

1 · Urządzenie / przeglądarka (PWA)

↓ ładuje

2 · Frontend — public/ (vanilla JS, moduły ES)

↓ fetch / SSE (HTTPS)

3 · Backend — Pages Functions functions/api/ (+ lib/)

↓ czyta / zapisuje

4 · Dane (Cloudflare)

5 · Usługi zewnętrzne · Wydanie

wskazówka

Kliknij kafelek diagramu

Po lewej cztery warstwy: urządzenie → frontend → backend → dane. Tu pojawi się opis i ścieżka pliku.

  1. Start: index.html → app.js → GET /api/me (cookie JWT).
  2. Brak sesji: e‑mail → kod PIN (Resend) → verify-pin ustawia JWT (7 dni).
  3. Bramka zgody na regulamin → accept-regulamin (KV).
  4. Zakładki: Moje zdjęcia · Moje głosy · Wyniki · (Ustawienia – admin).
  5. Faza (KV) decyduje, co wolno: zgłaszanie → głosowanie → wyniki.

1. Cel i skala

20uczestników (max) + admin
4kategorie (definiuje owner)
3zdjęcia / kategorię
16 MBna zdjęcie (6–20)
~240zdjęć (teoretyczny szczyt)
Zakres i zasady
Domena

Nowa domena zarejestrowana w Cloudflare: czwarta-b.com. Aplikacja działa na subdomenie foto-konkurs.czwarta-b.com. Domena czwarta-b.com jest stroną www klasy IVb, a zarazem hubem grupującym dostęp do usług i aplikacji.

2. Role, uprawnienia i etapy

Administrator

Admin = jeden z uczestników (też wgrywa i głosuje, też nie na własne). Dodatkowo:

Uwaga — fair play

Admin też startuje, więc NIE widzi cudzych ocen w trakcie — tylko postęp (kto już zagłosował, liczby). Pełne rankingi pokazują się wszystkim dopiero po odsłonięciu.

Role i uprawnienia

Wszystkie trzy role wgrywają zdjęcia i głosują; różnią się dostępem do funkcji administracyjnych:

Rolawidzi ustawieniadodaje kategoriedane testowe
Właściciel (owner)taktaktak
Administrator (admin)taknienie
Uczestnik (user)nienienie

Trzy role — od najszerszych uprawnień do najwęższych:

Każda rola ma niezależny przełącznik zgłaszania zdjęć (w panelu „zdjęcia +/−"; w bazie odwrotne pole voter_only). Domyślnie jest włączony — osoba zgłasza własne zdjęcia i głosuje. Po wyłączeniu osoba tylko głosuje, nie zgłasza zdjęć; przydaje się dla zaproszonych gości albo szerszego grona oceniającego.

W skrócie: rola mówi, co ktoś może robić w konkursie, a przełącznik zgłaszania zdjęć — czy sam staje do rywalizacji ze swoimi zdjęciami. Jedno z drugim nie jest powiązane, więc można je łączyć dowolnie: nawet administratorowi (czy właścicielowi) można wyłączyć zgłaszanie zdjęć — wtedy prowadzi konkurs, ale sam w nim nie startuje, jedynie ocenia prace innych. W ustawieniach odpowiadają temu dwa niezależne przełączniki: jeden dla roli, drugi dla zgłaszania zdjęć.

Etapy (uruchamia admin)
  1. Zgłoszenia (upload) — uczestnicy wgrywają zdjęcia; dostępne „Zapytaj Claude".
  2. Głosowanie (voting) — admin zamyka zgłoszenia, otwiera układanie TOP3; można modyfikować oceny.
  3. Wyniki (results) — admin kończy ocenę; progresywne odsłanianie wyników + wyróżnienia komisji.

Progresywne odsłanianie wyników — szczegóły w sekcji 14.

3. Stos technologiczny

Stos
ElementRola
Pagesfrontend statyczny pod foto-konkurs.czwarta-b.com
Pages Functionsbackend serverless w /functions — bez osobnego Workera
D1dane (uczestnicy, zdjęcia, głosy, recenzje AI) — env.DB
R2pliki zdjęć: oryginały + lekkie kopie — env.BUCKET
KVfaza, terminy, parametry, akceptacje regulaminu, dzienny limit maili, tryb testowy, ulubione komisji, liczniki dławienia
Resendmaile: PIN + potwierdzenia/przypomnienia + zaproszenie
Anthropic APIfunkcja „Zapytaj Claude" (Opus 4.8) — patrz sekcja 9
Uwaga — e-mail (Resend)

Domena czwarta-b.com weryfikowana w Resend → wysyłka z dowolnego adresu (no-reply@ dla automatów, admin@/konkurs@ dla korespondencji), bez zakładania skrzynek. Dostawca maili — Resend (wdrożone). Moduł email.js jest provider-agnostyczny (aplikacja woła tylko sendEmail), więc podmiana dostawcy = zmiana tego modułu + sekrety, bez zmian w logice. Strażnik dziennego limitu maili w KV. SES rozważany na przyszłość (własna domena), ale niezaimplementowany (placeholder zwraca provider_not_implemented:ses). Odbiór poczty (skrzynka admin@) — osobno, niższy priorytet (Cloudflare Email Routing → Gmail).

4. Logowanie — PIN na e-mail (OTP) + długa sesja

Logowanie
  1. E-mail → Functions sprawdza allowlist (users). Brak → odmowa.
  2. PIN losowany kryptograficznie (CSPRNG), przechowywany jako hash z solą w D1 (~10 min), PIN wychodzi mailem przez Resend.
  3. Wpisany PIN → weryfikacja (hash, niewygasły, limit prób, jednorazowość) → sesja (JWT w cookie HttpOnly, ~7 dni).
  4. Sesja 7 dni → PIN mniej więcej raz w tygodniu (skrócone, bo cichych wejść nie logujemy).

PIN ponownie tylko gdy: sesja wygaśnie, wylogowanie, czyszczenie ciasteczek, nowe urządzenie. WhatsApp jako kanał — odrzucony.

Ochrona przed nadużyciem (wdrożone): prośba o kod — maks. 10 kodów/h na e-mail i 30 żądań/h na IP; weryfikacja — 5 prób na kod + twardy licznik 20 nietrafień/h na e-mail (domyka brute-force). Liczniki w KV, przy awarii KV fail-open (nie blokuje logowania). Podgląd PIN na ekranie (tryb dev) tylko z localhost/LAN.

SSO — wspólne logowanie z portalem czwarta-b

Konkurs (foto-konkurs.czwarta-b.com) i portal czwarta-b.com dzielą jedną sesję. Zalogowanie w jednej aplikacji uznaje druga; wylogowanie kończy sesję po obu stronach.

Warstwa SSO: lib/session.js (wspólne podpisywanie/weryfikacja/kasowanie ciasteczka).

5. Model danych (D1 / SQLite) — szkic

Schemat
CREATE TABLE users (
  id INTEGER PRIMARY KEY, name TEXT NOT NULL,
  email TEXT NOT NULL UNIQUE,
  gender TEXT,                          -- poprawna odmiana / zwroty AI
  is_admin INTEGER NOT NULL DEFAULT 0,
  is_owner INTEGER NOT NULL DEFAULT 0,  -- chroniony superadmin
  active   INTEGER NOT NULL DEFAULT 1,  -- dezaktywacja zamiast usuwania
  voter_only INTEGER NOT NULL DEFAULT 0,-- „tylko głosuje": nie wgrywa zdjęć
  ai_sessions_used INTEGER NOT NULL DEFAULT 0,  -- licznik sesji AI
  invited_at TEXT);                     -- znacznik wysłania zaproszenia

CREATE TABLE categories (id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT);  -- definiuje owner (obecnie 4)

CREATE TABLE photos (
  id INTEGER PRIMARY KEY,
  owner_id    INTEGER NOT NULL REFERENCES users(id),
  category_id INTEGER NOT NULL REFERENCES categories(id),
  r2_key_original TEXT NOT NULL,        -- oryginał (nietknięty)
  r2_key_display  TEXT,                 -- lekka kopia do siatki / AI (~1500 px)
  title TEXT, filename TEXT, bytes INTEGER,
  taken_at TEXT,                        -- data wykonania z EXIF (jeśli jest)
  created_at TEXT DEFAULT (datetime('now')));

CREATE TABLE votes (
  voter_id    INTEGER NOT NULL REFERENCES users(id),
  category_id INTEGER NOT NULL REFERENCES categories(id),
  photo_id    INTEGER NOT NULL REFERENCES photos(id),
  rank   INTEGER NOT NULL,              -- 1..3
  points INTEGER NOT NULL,              -- 5-3-1
  PRIMARY KEY (voter_id, category_id, rank),
  UNIQUE (voter_id, category_id, photo_id));

CREATE TABLE ai_reviews (               -- cache „Zapytaj Claude" — per zdjęcie
  photo_id INTEGER PRIMARY KEY REFERENCES photos(id),
  review_text TEXT NOT NULL,
  titles_json TEXT,                     -- 3 tytuły + wskazany najlepszy
  followups_json TEXT,                  -- pytanie z 5 chipów + odpowiedź
  created_at TEXT DEFAULT (datetime('now')));

Pozostałe tabele (pełny schemat w schema.sql): favorites (ulubione per głosujący), ai_usage (tokeny Anthropic), jury_awards (3 wyróżnienia: pos, photo_id, degree, reason), login_codes (kody PIN — OTP), events (log zdarzeń, retencja 60 dni).

PIN-y w D1 (login_codes, mocna spójność); sesja jako JWT w cookie. Faza/terminy/parametry/akceptacje/limit maili — w KV.

6. Endpointy (Pages Functions)

Endpointy
EndpointDziałanie
POST /api/auth/request-pin{email} → allowlist → PIN (CSPRNG), hash do D1, Resend; dławienie per IP/e-mail
POST /api/auth/verify-pin{email, pin} → cookie sesji (długa); limit prób + twardy licznik nietrafień
POST /api/auth/logoutczyści ciasteczko sesji we wszystkich wariantach (host-only + Domain=.czwarta-b.com + stare session) — wspólne wylogowanie (SSO)
GET /api/mebieżący użytkownik + faza + terminy + czy wymagana akceptacja regulaminu; przy sesji SSO spoza konkursu → 401 { ssoEmail } (front pokazuje komunikat)
POST /api/accept-regulaminzapis akceptacji aktualnej wersji regulaminu
GET /api/photosmoje zdjęcia + lista kategorii (z opisami) + limity/liczniki
POST /api/photos/uploadoryginał + lekka kopia + miniatura, limit 3/kategorię, faza upload; blokada dla „tylko głosy"
DELETE /api/photos/:idusunięcie zdjęcia (właściciel; admin — moderacja) — faza upload
PATCH /api/photos/:idzmiana tytułu / kategorii własnego zdjęcia — faza upload
GET /api/img/:idstrumień zdjęcia z R2 (wymaga sesji); ?t=1 — miniatura (awaryjnie lekka kopia); ?v=<token> — wersja pliku do cache (z tokenem immutable, bez — no-cache + ETag/304); chroni przed starym obrazem po odzyskaniu numeru zdjęcia (SQLite reużywa id)
GET /api/download/:idpobranie oryginału na dysk — tylko właściciel
GET /api/voteprzegląd: faza + wszystkie kategorie (kandydaci, mój TOP, ulubione)
GET /api/vote/:categorykandydaci kategorii bez własnych, stała losowa kolejność + mój TOP/ulubione
POST /api/vote/:category{top:[…], favorites:[…]} → punkty 5-3-1 → zapis (nadpisuje, transakcyjnie), faza voting
GET /api/ai/:idrecenzja z cache (jeśli jest) + stan budżetu AI
POST /api/ai/:idrecenzja strumieniowana (SSE, Opus 4.8) albo 1 dodatkowe pytanie; faza upload, własne zdjęcie
GET /api/resultsranking per kategoria + wyróżnienia + statystyki zapowiedzi (bez rankingu zbiorczego) — pełne dane w fazie results
GET /api/my-awardsnagrody zalogowanego (dyplomy) — faza results; ?photo=ID = podgląd cudzego dyplomu (owner/admin/DEV, do testów)
...admin/*uczestnicy, kategorie (owner), terminy, etapy, parametry, postęp, galeria wyboru wyróżnień, wyróżnienia, komunikacja, log, dane testowe

Fazy upload → voting → results sterowane flagą w KV.

7. Punktacja i wyniki

Punktacja
Uwaga — mała liczba głosujących

Przy ~20 osobach, dobrowolnym głosowaniu i braku głosów na własne, remisy będą częste, a wyniki czułe na pojedynczy głos. Suma punktów faworyzuje kategorie z większą frekwencją głosowania — świadoma decyzja przy konkursie towarzyskim.

8. Interfejs, głosowanie i obsługa zdjęć

„all devices first"
Mechanizm głosowania TOP3

W kategorii może być 20+ (nawet ~100) zdjęć → przeciąganie z dużej puli jest niepraktyczne. Dlatego:

Obsługa zdjęć (oryginał nietknięty)
HEIC / miniatury — rozstrzygnięte

Kopie (lekka + miniatura, a dla HEIC także pełna kopia JPEG) robi przeglądarka (canvas), bez Cloudflare Images. Aplikacja przechowuje surowy HEIC (Apple nie zamienia go na JPEG przed wysłaniem), dlatego dekodowanie HEIC wymaga przeglądarki Apple (Safari), a oglądającym bez HEIC (np. Windows) służy pełna kopia JPEG. Miniatury starszych zdjęć dorabiają się w tle po zalogowaniu (przeglądarka właściciela).

Pozostałe funkcje

9. Funkcja AI — „Zapytaj Claude"

AI

Cel: autor dostaje rzeczową, ludzką recenzję własnego zdjęcia + propozycje tytułów i może „poradzić się" AI przed decyzją o zgłoszeniu.

AI — cache i budżet
AI — log zdarzeń
AI — prompt (verbatim, co do słowa)

Tekst promptu zapisany dokładnie jak ustalono — nie parafrazujemy go ani nie zastępujemy własnym opisem:

Jesteś recenzentem amatorskich fotografii grupy przyjaciół z tej samej klasy maturalnej. Bądź rzeczowy i miły, okaż empatię. Zwracaj się bezpośrednio. Nie stosuj sformułowań typowych dla AI, w tym przesadnego chwalenia autora zdjęcia.

Opisz załączone zdjęcie w około ${reviewWords} słowach — to długość samego opisu, dąż do niej; 3 tytuły i 5 pytań dodajesz osobno, poza tym limitem słów. Pisz pełnymi zdaniami, nie urywaj zdania w połowie. Oceń jego walory artystyczne, pokazane emocje i warsztat twórcy. Skup się na pozytywnym przekazie. Liczbę uwag krytycznych dopasuj do jakości zdjęcia: im zdjęcie lepsze, tym mniej rzeczy do poprawy — nie szukaj wad na siłę; przy bardzo dobrym zdjęciu zasugeruj wysoką ocenę i ogranicz uwagi krytyczne do minimum. Przekaż sugestie.

Zaproponuj 3 tytuły dla tego zdjęcia, wskaż najlepiej dopasowany.

Jeśli autor chce, odpowiesz na jedno dodatkowe pytanie. Zaproponuj pięć pytań, które AUTOR mógłby zadać TOBIE o to zdjęcie i na które potrafisz odpowiedzieć — sformułowane z perspektywy autora proszącego o radę lub wyjaśnienie (np. „Jak mógłbym…?", „Jakie ustawienia…?", „Jak obrobić…?"). NIE pytaj autora o jego intencje ani decyzje. Po jednym z obszarów, w kolejności: 1) przekaz i wymowa zdjęcia (jak je wzmocnić), 2) ujęcie (kompozycja, kadr, światło), 3) ustawienia aparatu, 4) obróbka w postprodukcji, 5) lekkie, z przymrużeniem oka.

Implementacja obok promptu (bez zmiany słów): płeć / kategoria / tytuł przekazywane w wiadomości jako dane (źródło prawdy); zdjęcie jako kopia ~1500 px; z 5 chipów autor wybiera jedno pytanie.

10. Skala vs darmowe limity Cloudflare

Przeliczone na ~240 zdjęć
ZasóbZużycie (max)Darmowy próg
R2 (magazyn)oryginały do 16 MB × 240 ≈ do ~3,8 GB; kopie — ułamek tego10 GB
D1 (dane)głosy ~setki + ulubione + recenzje5 GB, mln operacji/dzień
Functionskilka–kilkanaście tys. żądań100 tys./dzień
Resend (maile)kilkadziesiąt–setka~100/dzień, 3 000/mies.
Uwaga — koszty niezerowe

Infrastruktura CF mieści się w darmowych progach (R2 z dużym zapasem — zmniejszanie do kopii robimy dla szybkości galerii, nie dla limitu). Płatne: domena czwarta-b.com (~kilkadziesiąt zł/rok) i tokeny Claude (zmienny koszt, ograniczany budżetem sesji AI).

11. Plan pracy (etapy)

Etapy realizacji
  1. Fundament / scaffold — katalog + git (osobne, prywatne repo), Cloudflare Pages, wrangler.toml (D1/R2/KV), sekrety (Resend, Anthropic), schema.sql + seed, flaga fazy, domena.
  2. UX / prototyp — klikalna makieta HTML w PREVIEW (logowanie, upload, „Zapytaj Claude", głosowanie TOP3, wyniki, panel admina). Zatwierdzenie przed kodem.
  3. Autentykacja — OTP PIN (Resend), allowlist, długa sesja (JWT), anty-brute-force.
  4. Ładowanie zdjęć — upload R2 (oryginał + kopia), HEIC→wyświetlana, lightbox, limit 3/kategorię, „Moje zgłoszenia", postęp.
  5. „Zapytaj Claude" — Opus 4.8 streaming, budżet sesji + licznik, cache per zdjęcie, kopiuj/udostępnij.
  6. Ocena — galeria per kategoria (bez własnych, anonimowo, losowa), ulubione → TOP3 (tap + drag), modyfikacja głosów.
  7. Komunikacja — maile potwierdzające + przypomnienia (admin).
  8. Panel admina — uczestnicy, kategorie, postęp (bez cudzych ocen), usuwanie zdjęć, etapy, wyróżnienia komisji, odsłanianie.
  9. Wyniki — podium per kategoria + wyróżnienia, progresywne odsłanianie. (Z pierwotnego planu nie powstał ranking zbiorczy ani eksport/link read-only.)
  10. Testy + wdrożenie — smoke (logowanie, upload, AI, głos, wynik), deploy, seed realnych osób, próbny przebieg.

12. Struktura projektu i git

Repozytorium

Osobno od Rambulo — celowo. Rambulo = Vite + Express + SQLite na Railway. Konkurs = serverless w całości na Cloudflare, lekka technologia.

konkurs-foto/
  SPEC.md  README.md  LOG-ZDARZEN.md     # specyfikacja (źródło prawdy), opis repo, typy zdarzeń logu
  dokumenty/                           # źródła .md dokumentów ze stopki (→ public/*.html)
  PREVIEW/                             # katalog do prezentacji makiet na żywo (prototypy UX, nieserwowane)
  public/                              # frontend (vanilla JS, lekko)
  functions/api/...                    # Pages Functions (login, photos, vote, ai, results, admin)
  schema.sql                           # tabele D1 + seed
  scripts/                             # stamp-version, tag-release
  _WYDANIE.COMMAND  _WDROZENIE.COMMAND # launchery wydania (dwuklik) + _STATUS / _SERWER
  wrangler.toml  .dev.vars  .gitignore  README.md

Dokumenty tekstowe (.md) — gdzie i po co

Wszystkie pliki .md to źródła (edytujemy je, nie rendery). Trzy z nich zasilają dokumenty widoczne w stopce aplikacji; po zmianie treści źródła ręcznie przenosimy ją do odpowiadającego pliku public/*.html (nie ma konwertera md→html — polecenie „zsynchronizuj …"). Numer wersji, tabela „Rozmiary plików" i drzewo plików trafiają do obu plików automatycznie przy wydaniu.

Dokumenty ze stopki (źródło → render serwowany):

Dokumenty projektowe w katalogu głównym (nie serwowane w aplikacji):

Struktura style.css (sekcje)

Spis sekcji arkusza stylów — generowany automatycznie z banerów „SECTION:" w public/style.css (kolejność = kolejność w pliku; npm run cssmap, także przy wydaniu). Nie edytuj poniższej listy ręcznie.

1. Podstawy — zmienne, reset, paski przewijania, body
2. Układ i nawigacja — wrap, zakładki, nagłówek, panel, stopka
3. Modale wspólne — QR, potwierdzenie, bramka zgody
4. Diagram faz (Ustawienia)
5. Głosowanie — TOP, toast, filtry, pula miniatur
6. Wyniki — pulpit, podium, medale, lightbox wyników
7. Lightbox (podgląd zdjęć)
8. Logowanie
9. Moje zdjęcia — liczniki, pasek etapu, kategorie, lista zdjęć
10. Ustawienia (admin) — uczestnicy, tryb testowy, log zdarzeń
11. Wyróżnienia komisji — sloty, medale/stopnie
12. Aktywność AI (panel admina)
13. Kategorie (Ustawienia)
14. Modal pomocy
15. Modal „O aplikacji"
16. Modal „Zapytaj Claude" — recenzja, tytuły, loader, chipy
17. Mobile (≤560px)
18. Przełącznik (pigułka)
19. Filtr uczestników
20. Treść maili (Ustawienia) — akordeon, podgląd, wysyłka
21. Baner instalacji PWA
22. Dyplomy i odznaka zakładki

Wydanie i wdrożenie — dwa skrypty (model jak w Rambulo)

Zamiast osobnych skryptów COMMIT / RELEASE / DEPLOY / PUSH są dwa launchery .command (dwuklik w Finderze):

Bramką „nie wdrażaj tej samej wersji dwa razy" jest tag git-WERSJA (tworzony w postdeploy) — przy powtórnym wdrożeniu tej samej wersji predeploy przerywa i odsyła do _WYDANIE. W odróżnieniu od Rambulo nie ma tu maszyny stanów .deploy-state, testów E2E, mirrora GitLab ani purge Cloudflare — konkurs wdraża bezpośrednio przez wrangler. Pomocniczo: _STATUS.COMMAND (podgląd stanu) i _SERWER.COMMAND (serwer lokalny).

Testy i piaskownica testowa (E2E na życzenie)

npm test — testy jednostkowe, bez sieci i bazy (szybkie, wpięte w wydanie): scripts/filesizes.test.mjs + scripts/imgver.test.mjs (token wersji obrazka imgVer — sedno ochrony przed pokazaniem starego zdjęcia po odzyskaniu numeru).

Cięższy scenariusz end‑to‑end (obrazy, głosowanie, wyniki, dyplomy, panel admina) uruchamiamy na życzenie w izolowanej piaskownicy — świadomie poza npm test i poza wydaniem, bo zasiewa i mutuje dane. Nigdy nie ruszamy żywego serwera dev ani danych produkcyjnych.

  1. Osobny worktree (własna kopia, nie pod żywym serwerem): git worktree add -b test-sandbox ../konkurs-foto-sandbox.
  2. Zależności: npm --prefix ../konkurs-foto-sandbox install.
  3. .dev.vars w piaskownicy: JWT_SECRET, DEV=1, MAIL_PROVIDER/MAIL_FROM/MAIL_DAILY_CAP oraz AUTH_SECRET — bez niego serwer używa ciasteczka session, które na localhost bywa przesłaniane przez stare cbsession z innych lokalnych aplikacji (ciasteczka ignorują port) i logowanie w przeglądarce nie działa.
  4. Baza (własny persist): wrangler d1 execute konkurs --local --persist-to ../konkurs-foto-sandbox/.wrangler/state --file schema.sql, potem wstaw usera i kategorię.
  5. Serwer na INNYM porcie: npm --prefix ../konkurs-foto-sandbox run dev -- --port 8790 --persist-to ../konkurs-foto-sandbox/.wrangler/state.
  6. Logowanie: w DEV PIN pokazuje się na ekranie (auto‑wpisany). Dane demonstracyjne: tryb testowy (POST /api/admin/test-mode {on:true}, rola owner) → 10 osób × 3 zdjęcia SVG; etap: POST /api/admin/phase {phase:"upload|voting|results"}.
  7. Co przejść: wstaw/usuń/podgląd po odzyskaniu numeru (miniatura + lightbox pokazują bieżące zdjęcie), dodanie do TOP3, dodanie do wyróżnień, karty wyników, dyplom (/dyplom.html?photo=ID).

Pułapki: konto @test.test blokuje seed (testDataExists uznaje je za dane testowe — na czas seedu zmień e‑mail konta na nietestowy; sesja działa po numerze konta, nie po e‑mailu) · sprzątanie: POST /api/admin/test-mode {on:false} lub DELETE (purge) · obrazy loading="lazy" poza ekranem mają chwilowo naturalWidth=0 — to nie błąd. Po testach: zatrzymaj serwer i git worktree remove --force ../konkurs-foto-sandbox.

13. Decyzje

Podjęte ✅

Cloudflare + Resend + Anthropic domena czwarta-b.com → foto-konkurs.* 20 + admin 4 kategorie (definiuje owner) 3 zdjęcia / kategorię · do 16 MB JPG/HEIC/WEBP/PNG TOP3 · 5-3-1 · pełny · dobrowolny remisy: więcej wyższych miejsc → wcześniej zgłoszone → id (bez ex aequo) brak głosów na własne PIN na e-mail (kryptograficzny) · długa sesja dławienie logowania per IP/e-mail + fail-open zapis głosów transakcyjny admin NIE widzi cudzych ocen komisja: wyróżnienia wpisuje admin usuwanie uczestnika → dezaktywacja all devices first (3/2/1) oryginał nietknięty + lekkie kopie głosowanie: tap z puli + drag w TOP3 + ulubione „Zapytaj Claude" (Opus 4.8, budżet sesji) anonimowość · maile · „Moje zgłoszenia" dezaktywacja uczestnika → zdjęcia nieaktywne (FK), oddane głosy zostają budżet AI = kategorie×3 + bufor role: owner / admin / uczestnik · tryb: tylko głosuje „tylko głosuje" — bez zgłaszania kategorie i dane testowe tylko owner punktacja w jednym pliku scoring.js adres aplikacji w mailu zaproszenia wyniki: odsłanianie od najniższego miejsca maile: moduł wymienny + strażnik limitu owner/superadmin nadaje admina konkurs jako PWA

14. Ekrany, nawigacja i język wizualny

Język wizualny
Nawigacja
Ekran logowania
  1. Pole e-mail + „Wyślij kod".
  2. Pole PIN (6 cyfr) + „Zaloguj"; komunikat „Wysłaliśmy kod na <e-mail>".

Komunikaty ludzkie: e-mail spoza listy → „Nie znaleźliśmy tego adresu — sprawdź pisownię albo napisz do organizatora"; zły/wygasły PIN → „PIN niepoprawny lub wygasł (pozostały próby: N)". Ochrona (limit prób, hash, TTL, jednorazowość) zostaje.

Ekran zgód (jednorazowy)
Zakładka 1 „Moje zdjęcia"
Przycisk AI (3 stany)

Wyzwalacz: przycisk tekstowy (bez ikony) — ustalone (rezygnacja z gwiazdki Claude).

Zakładka 2 „Głosowanie"
Zakładka 3 „Wyniki"
Ekran wyników (progresywne odsłanianie)

U góry przyciski tylko dla admina: Wyróżnienia, Kategoria 1, …, ostatnia kategoria. Naciśnięcie → dana sekcja odsłania się ładnie, od najniższego miejsca podium w górę. „Wyróżnienia" pokazują uznaniowe wyróżnienia komisji.

Zakładka 4 „Ustawienia" (panel admina)
Filtr listy uczestników (sekcja „Uczestnicy")
Sekcja „Protokół" — protokół z głosowania dla Komisji
Sekcja „Piaskownica" (pracownia testowa)
Podgląd i zapis dyplomu (dyplom.html)
plikkrójużycie na dyplomielatinlatin-ext
cinzel-v26-700-*Cinzel 700nagłówek, „MIEJSCE", kategoria, nazwisko laureata15,2 kB8,3 kB
cormorant-garamond-v21-700italic-*Cormorant Garamond italic 700tytuł zdjęcia, uzasadnienie Komisji, podziękowanie, nazwisko bez faksymile (gałąź zapasowa)22,4 kB19,2 kB
montserrat-v31-500-*Montserrat 500dolna belka (adres, data)18,7 kB32,3 kB
montserrat-v31-600-*Montserrat 600etykiety „OTRZYMUJE", „ZA ZDJĘCIE"18,7 kB33,2 kB
Jak dodać lub wymienić krój dyplomu
Dyplom uczestnictwa i sekcja „Podziękowania"
Ekran „O aplikacji" (modal-akordeon ze stopki)
Komunikaty błędów (zasada)
Animacje interfejsu — parametr „Tempo animacji"

Większość przejść treści steruje parametr admina „Tempo animacji" (0–200%, sekcja Parametry): 100% = wartości bazowe, mniej = szybciej, 0 = bez animacji, więcej = wolniej. Parametr trafia do każdego uczestnika na starcie (/api/me), ustawia zmienną CSS --anim, a czasy liczone są jako bazowy × (Tempo/100). Wyjątek: rozsuwanie galerii miniatur (Wyróżnienia + „Moje głosy") ma osobny, rozłączny parametr „Prędkość rozsuwania galerii" (patrz niżej). Odczyt tempa w jednym miejscu: public/js/dom.js → animScale(). Część animacji robi CSS (skalowany przez calc()), część JS (Web Animations / transition, z pominięciem animacji przy 0%).

AnimacjaBaza (100%)Sterowanie
Zmiana zakładki — zanikanie starej treści0,2 sbaza × Tempo
Zmiana zakładki — pojawienie nowej treści0,2 sbaza × Tempo
Podświetlenie aktywnej zakładki0,2 sbaza × Tempo
Natywne rozwijaki („jak to działa?", belki maili)0,2 sbaza × Tempo
Zdjęcie na pełny ekran (powiększenie + tło)0,3 sbaza × Tempo
Rozwijanie/zwijanie kategorii (Ustawienia, „Moje zdjęcia")0,3 sbaza × Tempo
Rozsuwanie galerii miniatur (Wyróżnienia, kategorie i „własne" w „Moje głosy")zależny od ekranuPrędkość galerii (px/ms, A′)
TOP3 — przesuwanie kafelków (FLIP)0,3 sbaza × Tempo
TOP3 — wysokość slotu0,3 sbaza × Tempo
„Dymki" powiadomień (toast)0,2 sstała
Powiększenie logo pod kursorem0,2 sstała
Kropki „ładowania" AIcykl 1 sstała

Trzy pozycje „stała" są celowo poza parametrem: toast i logo to drobiazgi, a kropki „ładowania" AI muszą się kręcić niezależnie (wskaźnik postępu — nie znika przy 0%).

Prędkość rozsuwania galerii — parametr rozłączny z Tempem animacji. Rozsuwanie galerii miniatur (galeria wyboru Wyróżnień oraz kategorie i sekcja „własne" w „Moje głosy") ma własny parametr admina „Prędkość rozsuwania galerii" (1–8 px/ms, skok 0,5, domyślnie 4; sekcja Parametry) — rozłączny z „Tempem animacji": tempo nie wpływa na tę prędkość, a prędkość nie wpływa na resztę animacji. Parametr trafia do każdego uczestnika na starcie (/api/me → revealSpeed) i ustawia REVEAL.V w public/js/accordion.js. Model „A′" (viewport-aware): animowana jest tylko widoczna część rozsuwania — ze stałą prędkością V — a część poniżej dołu ekranu domyka się szybkim „ogonem", więc odczuwalna prędkość jest stała niezależnie od liczby zdjęć, a czas ograniczony wysokością ekranu. Zamykanie symetryczne. Podgląd/strojenie: PREVIEW/_demo-anim.html.

15. Struktura plików (stan 2026-07-03)

_konkurs-foto/
  public/                         # frontend (serwowany)
    _headers
    app.js
    dokumentacja.html
    dyplom.html
    icon_180.png
    icon_192.png
    icon_512.png
    index.html
    manifest.webmanifest
    portal-4b.svg
    przewodnik.html
    regulamin.html
    style.css
    sw.js
    trophy_192.png
    trophy_512.png
    js/                           # moduły ES
      about.js
      accordion.js
      ai.js
      auth.js
      confirm.js
      consent.js
      diag.js
      dom-anim.js
      dom.js
      help.js
      infobar.js
      inspect.js
      install-banner.js
      lightbox.js
      phone.js
      photo-image.js
      photos-view.js
      photos.js
      reason-limit.js
      results.js
      session.js
      settings-comm.js
      settings-dyplomy.js
      settings-events.js
      settings-forms.js
      settings-handlers.js
      settings-help.js
      settings-jury.js
      settings-protokol.js
      settings-views.js
      settings.js
      tabs.js
      toast.js
      update.js
      version.js
      voting-view.js
      voting.js
    vendor/                       # biblioteki PDF + fonts/ (kroje dyplomu)
      html2canvas-1.4.1.min.js
      jspdf-2.5.2.umd.min.js
  functions/
    api/                          # Pages Functions (endpointy)
      accept-regulamin.js
      admin/categories.js
      admin/categories/[id].js
      admin/check-delivery.js
      admin/deadlines.js
      admin/derivatives.js
      admin/dyplom-text.js
      admin/dyplomy.js
      admin/events.js
      admin/hashes.js
      admin/jury-favorites.js
      admin/jury-oglos.js
      admin/jury.js
      admin/maintenance-summary.js
      admin/notify.js
      admin/orphans.js
      admin/overview.js
      admin/params.js
      admin/phase.js
      admin/photos.js
      admin/protokol.js
      admin/test-mode.js
      admin/users.js
      admin/users/[id].js
      ai/[id].js
      auth/logout.js
      auth/request-pin.js
      auth/verify-pin.js
      diag-mode.js
      diag.js
      download/[id].js
      dyplom-text.js
      full/[id].js
      health.js
      img/[id].js
      me.js
      my-awards.js
      photos.js
      photos/[id].js
      photos/upload.js
      results.js
      thumbs/[id].js
      thumbs/missing.js
      vote.js
      vote/[category].js
    lib/                          # wspólna logika
      ai.js
      auth.js
      diag.js
      dyplom-text.js
      email.js
      events.js
      exif.js
      imgver.js
      jwt.js
      keys.js
      notify.js
      params.js
      phase.js
      ratelimit.js
      regulamin.js
      scoring.js
      session.js
      testmode.js
      version.js
      wyroznienia.js
  schema.sql  wrangler.toml  package.json  scripts/  dokumenty/

16. Rozmiary plików .js i .css (KB + linie, stan 2026-10-13)

PlikKBLinie
functions/api/
accept-regulamin.js0.813
admin/categories.js1.228
admin/categories/[id].js2.458
admin/check-delivery.js2.357
admin/deadlines.js1.839
admin/derivatives.js2.968
admin/dyplom-text.js0.920
admin/dyplomy.js9.9187
admin/events.js1.842
admin/hashes.js4.089
admin/jury-favorites.js2.453
admin/jury-oglos.js2.144
admin/jury.js6.7146
admin/maintenance-summary.js2.553
admin/notify.js6.3139
admin/orphans.js4.598
admin/overview.js5.8105
admin/params.js0.820
admin/phase.js0.922
admin/photos.js1.026
admin/protokol.js20.1293
admin/test-mode.js1.842
admin/users.js1.748
admin/users/[id].js4.691
ai/[id].js9.5205
auth/logout.js0.48
auth/request-pin.js6.3147
auth/verify-pin.js4.8121
diag-mode.js2.249
diag.js2.446
download/[id].js1.431
dyplom-text.js0.510
full/[id].js9.1169
health.js0.37
img/[id].js4.081
me.js2.554
my-awards.js8.4177
photos.js1.846
photos/[id].js5.8127
photos/upload.js9.3181
results.js8.1137
thumbs/[id].js1.934
thumbs/missing.js1.333
vote.js3.483
vote/[category].js5.4137
functions/lib/
ai.js7.4141
auth.js1.330
diag.js5.2111
dyplom-text.js6.1128
email.js6.6159
events.js6.7110
exif.js10.3245
imgver.js1.418
jwt.js2.677
keys.js1.725
notify.js14.9298
params.js2.651
phase.js4.597
ratelimit.js3.259
regulamin.js3.457
scoring.js7.8127
session.js2.245
testmode.js7.4153
version.js0.12
wyroznienia.js2.038
public/js/
about.js7.0143
accordion.js8.8195
ai.js9.4265
auth.js2.377
confirm.js3.8100
consent.js2.557
diag.js10.3172
dom-anim.js1.127
dom.js2.456
help.js10.598
infobar.js4.0121
inspect.js22.9473
install-banner.js6.6137
lightbox.js30.8545
phone.js2.367
photo-image.js4.3105
photos-view.js8.6176
photos.js24.4564
reason-limit.js9.5180
results.js19.1375
session.js2.858
settings-comm.js8.9182
settings-dyplomy.js27.6582
settings-events.js11.4263
settings-forms.js9.9213
settings-handlers.js32.1660
settings-help.js22.792
settings-jury.js13.8287
settings-protokol.js12.7230
settings-views.js16.7277
settings.js25.9473
tabs.js3.792
toast.js1.135
update.js5.4115
version.js0.12
voting-view.js7.7162
voting.js15.6384
public/
app.js7.3129
style.css98.91347
sw.js2.027

Razem: JS — 104 plików, 13831 linii, ~693 KB · CSS — 1 plik, 1347 linii, ~99 KB.

wyróżnione = pliki ponad 400 linii (kandydaci do podziału).

17. Konserwacja (owner) — spójność plików w magazynie

Owner

Sekcja Ustawienia → Konserwacja, widoczna wyłącznie dla właściciela (owner). Służy do wykrywania i naprawy rozjazdów między bazą (rekordy zdjęć w D1) a magazynem (pliki w R2). Zawiera trzy narzędzia oraz proaktywny znacznik.

Przypomnienie o plikach zdjęcia. Każde zdjęcie ma oryginał (nietknięte archiwum pracy) oraz pliki pochodne liczone z niego: lekką kopię (~2048 px), miniaturę (~640 px) i — tylko dla HEIC — pełną kopię JPEG. Pełna kopia to podgląd zastępczy: ocena zawsze idzie z oryginału, a kopia wchodzi jedynie wtedy, gdy przeglądarka nie pokaże oryginału HEIC (np. Windows). Klucze wszystkich pochodnych liczy jedno źródło prawdy (functions/lib/keys.js) — używane wszędzie (upload, serwowanie, kasowanie, konserwacja), żeby ten sam plik zawsze miał ten sam klucz.

Osierocone zdjęcia (pliki bez zdjęcia)

Pliki, które zostały w magazynie po usuniętych zdjęciach (nie wskazuje ich żaden rekord).

Braki zdjęć pochodnych (zdjęcie bez pliku)

Odwrotność sierot: rekord zdjęcia jest, ale brakuje mu pliku — cichy, częściowy upload (na ekranie wygląda dobrze). Dla każdego zdjęcia sprawdzane są: oryginał, lekka kopia, miniatura oraz — dla HEIC — pełna kopia.

Brakujące sygnatury (zdjęcie bez odcisku treści)

Każde zdjęcie ma sygnaturę — skrót treści oryginału (SHA-256), dzięki któremu to samo zdjęcie bierze udział w Konkursie tylko raz (także w innym rozmiarze czy formacie pliku — patrz Regulamin §3). Nowe zgłoszenia dostają sygnaturę od razu przy wysyłce; to narzędzie dolicza ją zdjęciom sprzed wprowadzenia blokady (mają sygnaturę pustą).

Znacznik „jest coś do zrobienia"

Przy każdym wejściu w Ustawienia (dla ownera) jedno szybkie sprawdzenie liczy naraz sieroty, braki pochodnych i brakujące sygnatury. Gdy jest robota, zapala się czerwona kropka:

Sprawdzenie jest ciche i nieblokujące (widok pojawia się od razu, kropka dochodzi po odpowiedzi). Endpoint: POST /api/admin/maintenance-summary — tylko odczyt, jedno przejście po magazynie, zwraca liczbę sierot, braków pochodnych i brakujących sygnatur oraz todo.

Ograniczenie z natury rzeczy: odbudowa jest po stronie przeglądarki, więc pełne/lekkie kopie HEIC naprawisz tylko na urządzeniu Apple. Oryginał to chronione archiwum — jego utrata jest nieodwracalna (dlatego Konserwacja nigdy sama nie kasuje oryginałów zdjęć, a jedynie osierocone pliki wskazane przez Ciebie).

↑