Konkurs fotograficzny — dokumentacja aplikacji

Projekt osobny od Rambulo · wdrożone — wersja 0.27.0 · aktualizacja 2026-06-29
Wymaganie — to, co ustaliłeś Propozycja — rozwiązanie AI — funkcja „Zapytaj Claude" Uwaga — na co zwrócić uwagę

Architektura wdrożona (stan 0.27.0) — diagram klikalny

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ę
<5 MBna zdjęcie
~240zdjęć (teoretyczny szczyt)
Wymaganie
Wymaganie — domena

Kupujemy domenę w Cloudflare: czwarta-b.com. Aplikacja działa na subdomenie foto-konkurs.czwarta-b.com. Sama czwarta-b.com nie daje dostępu do aplikacji.

2. Role, uprawnienia, komisja i etapy

Wymaganie

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

Uwaga — fair play (ustalone)

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.

Wymaganie — role i uprawnienia (wdrożone)
RolaWgrywanieGłosowanieUstawieniaKategorieDane testowe
owner (chroniony)taktaktakdodajetak
admintaktaktaknienie
uczestniktaktaknienienie
tylko głosy (voter_only)NIEtaknienienie

Ustawienia widoczne tylko dla admina i ownera. Dodawanie kategorii oraz przyciski „Dane testowe / Usuń dane testowe"tylko owner. Rola „tylko głosy" realnie blokuje wgrywanie zdjęć (twarda reguła w API + ukryte „wstaw zdjęcie"). Owner/superadmin nadaje i odbiera admina, nie może być zdegradowany (is_owner).

Właściciel (owner) i uczestnicy „tylko głosujący"

Owner (właściciel) — jeden, chroniony superadministrator (zwykle organizator). Tylko on: nadaje i odbiera rolę administratora, dodaje kategorie oraz włącza/usuwa dane testowe. Ownera nie da się zdegradować (is_owner), więc konkurs zawsze ma właściciela; w pozostałych czynnościach działa jak zwykły admin.

Uczestnicy „tylko głosujący" (voter_only) biorą udział wyłącznie w głosowaniu — nie wgrywają własnych zdjęć. Blokada jest twarda: API odrzuca próbę uploadu, a w interfejsie znika „wstaw zdjęcie". Rola dla osób, które mają tylko oceniać (np. zaproszeni goście, szersze grono jury), bez startu w konkursie. Głosują na tych samych zasadach co pozostali (TOP3, bez głosów na własne — których i tak nie mają).

Wymaganie — komisja

Komisja = admin + 2 osoby, bez uprawnień w systemie. Wyróżnienia ustalane są poza systemem, a wpisuje je wyłącznie admin (uznaniowe, ujawniane w trakcie prezentacji wyników). Bez osobnej roli i logowania dla komisji.

Wymaganie — 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.

Szczegóły progresywnego odsłaniania — do zaprojektowania.

3. Stack — w całości Cloudflare

Propozycja
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
KVkody PIN (TTL) + flaga fazy konkursu
Resendmaile: PIN + potwierdzenia/przypomnienia
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

Propozycja
  1. E-mail → Functions sprawdza allowlist (users). Brak → odmowa.
  2. Losowany PIN, hash do KV z TTL (~10 min), PIN wychodzi mailem przez Resend.
  3. Wpisany PIN → weryfikacja (hash, niewygasły, limit prób, jednorazowość) → sesja (JWT w cookie).
  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.

5. Model danych (D1 / SQLite) — szkic

Propozycja
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
  ai_sessions_used INTEGER NOT NULL DEFAULT 0);  -- licznik sesji AI

CREATE TABLE categories (id INTEGER PRIMARY KEY, name TEXT NOT NULL);  -- 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,
  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 3 chipów + odpowiedź
  created_at TEXT DEFAULT (datetime('now')));

PIN-y w KV z TTL; sesja jako JWT w cookie.

6. Endpointy (Pages Functions)

Propozycja
EndpointDziałanie
POST /api/auth/request-pin{email} → allowlist → PIN, hash do KV, Resend
POST /api/auth/verify-pin{email, code} → cookie sesji (długa)
GET /api/mebieżący użytkownik + stan licznika AI
GET /api/categorieskategorie (definiowane przez admina)
POST /api/photosupload: oryginał + lekka kopia, limit 3/kategorię; blokada dla roli „tylko głosy" — faza upload
DELETE /api/photos/:idusunięcie własnego zdjęcia — tylko przed głosowaniem
GET /api/photos?category=Xkategoria bez własnych, anonimowo, losowa stała kolejność
POST /api/vote{category_id, ranking:[...]} → punkty → zapis (nadpisuje) — faza voting
POST /api/ai/review{photo_id} → strumieniowana recenzja Opus 4.8; zużywa 1 sesję AI; cache
POST /api/ai/followup{photo_id, question} → jedno pytanie z 3 zaproponowanych, w obrębie sesji
GET /api/resultspodium per kategoria + zbiorczo + wyróżnienia — po odsłonięciu
...adminuczestnicy, kategorie, postęp, usuwanie zdjęć, etapy, wyróżnienia, odsłanianie

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

7. Punktacja i wyniki

Propozycja
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ęć

Propozycja — „all devices first"
Propozycja — mechanizm głosowania TOP3

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

Propozycja — obsługa zdjęć (oryginał nietknięty)
Uwaga — HEIC / miniatury (do testu)

Konwersja HEIC i generowanie kopii: w przeglądarce (najtaniej, zależne od urządzenia/biblioteki WASM) czy przez Cloudflare Images (płatne, „samo działa"). Rekomendacja: konwersja+zmniejszanie w przeglądarce, z CF Images jako planem B. Przetestować na realnym iPhonie w etapie 1.

Propozycja — pozostałe funkcje (zaakceptowane)

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 — 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, oceń płeć po imieniu. Koncentruj się na pozytywnym przekazie, ale wskaż też rzeczy do poprawy. Nie stosuj sformułowań typowych dla AI, w tym przesadnego chwalenia autora zdjęcia.

Opisz załączone zdjęcie w 300 słowach. Oceń jego walory artystyczne, pokazane emocje i warsztat twórcy. Przekaż sugestie.

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

Jeśli autor chce, odpowiesz na dodatkowe pytanie - zaproponuj trzy do wyboru w formie chipów.

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 3 chipów autor wybiera jedno pytanie.

10. Skala vs darmowe limity Cloudflare

Propozycja — przeliczone na ~240 zdjęć
ZasóbZużycie (max)Darmowy próg
R2 (magazyn)oryginały <5 MB × 240 ≈ do ~1,2 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)

Propozycja
  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 + zbiorczo + wyróżnienia, progresywne odsłanianie, eksport/link.
  10. Testy + wdrożenie — smoke (logowanie, upload, AI, głos, wynik), deploy, seed realnych osób, próbny przebieg.

12. Struktura projektu i git

Propozycja

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

konkurs-foto/
  SPEC.md  PREVIEW/                     # dokumentacja + makiety
  public/                              # frontend (vanilla JS, lekko)
  functions/api/...                    # Pages Functions (login, photos, vote, ai, results, admin)
  schema.sql                           # tabele D1 + seed
  wrangler.toml  .dev.vars  .gitignore  README.md

13. Decyzje

Podjęte ✅

Cloudflare + Resend + Anthropic domena czwarta-b.com → foto-konkurs.* 20 + admin 4 kategorie (definiuje owner) 3 zdjęcia / kategorię · <5 MB JPG/HEIC/WEBP/PNG TOP3 · 5-3-1 · pełny · dobrowolny remisy: więcej wyższych miejsc / ex aequo brak głosów na własne PIN na e-mail · długa sesja 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) budżet AI = kategorie×3 + bufor role: owner / admin / uczestnik / tylko-głosy „tylko głosy" bez wgrywania 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

Otwarte ⏳

  1. Nazwy/liczba kategorii (ustali admin, konfigurowalne).
  2. Regulamin, harmonogram i zgody (RODO) — treść i umiejscowienie (przy projektowaniu layoutu). Draft: PREVIEW/regulamin-harmonogram.html.
  3. HEIC / generowanie kopii — metoda (przeglądarka vs CF Images), test na iPhonie w etapie 1.

14. Ekrany, nawigacja i język wizualny

Ustalone — język wizualny
Ustalone — nawigacja
Ustalone — 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.

Ustalone — ekran zgód (jednorazowy)
Ustalone — zakładka 1 „Moje zdjęcia"
Ustalone — przycisk AI (3 stany)

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

Ustalone — zakładka 2 „Głosowanie"
Ustalone — zakładka 3 „Wyniki"
Ustalone — 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.

Ustalone — zakładka 4 „Ustawienia" (panel, ukryty dla zwykłych użytkowników)
Ustalone — ekran „O aplikacji" (modal-akordeon ze stopki)

15. Struktura plików (stan 2026-06-29)

konkurs-foto/
  public/                         # frontend (serwowany)
    app.js
    dla-komisji.html
    dyplom.html
    index.html
    konkurs-foto.html
    manifest.webmanifest
    przewodnik.html
    regulamin.html
    style.css
    sw.js
    js/                           # moduły ES
      about.js
      accordion.js
      ai.js
      auth.js
      confirm.js
      consent.js
      dom.js
      help.js
      infobar.js
      lightbox.js
      phone.js
      photos.js
      results.js
      session.js
      settings-events.js
      settings-handlers.js
      settings-jury.js
      settings-views.js
      settings.js
      tabs.js
      toast.js
      version.js
      voting.js
  functions/
    api/                          # Pages Functions (endpointy)
      accept-regulamin.js
      admin/categories.js
      admin/categories/[id].js
      admin/deadlines.js
      admin/events.js
      admin/jury-favorites.js
      admin/jury.js
      admin/notify.js
      admin/overview.js
      admin/params.js
      admin/phase.js
      admin/photos.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
      health.js
      img/[id].js
      jury/img/[pos].js
      me.js
      my-awards.js
      photos.js
      photos/[id].js
      photos/upload.js
      results.js
      vote.js
      vote/[category].js
    lib/                          # wspólna logika
      ai.js
      auth.js
      email.js
      events.js
      jwt.js
      notify.js
      params.js
      phase.js
      regulamin.js
      scoring.js
      testmode.js
      version.js
  schema.sql  wrangler.toml  package.json  scripts/  PREVIEW/

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

PlikKBLinie
functions/api/accept-regulamin.js0.510
functions/api/admin/categories.js1.228
functions/api/admin/categories/[id].js2.258
functions/api/admin/deadlines.js1.839
functions/api/admin/events.js1.332
functions/api/admin/jury-favorites.js1.541
functions/api/admin/jury.js3.696
functions/api/admin/notify.js4.9126
functions/api/admin/overview.js3.472
functions/api/admin/params.js0.820
functions/api/admin/phase.js0.922
functions/api/admin/photos.js1.335
functions/api/admin/test-mode.js1.842
functions/api/admin/users.js1.748
functions/api/admin/users/[id].js4.491
functions/api/ai/[id].js7.8183
functions/api/auth/logout.js0.411
functions/api/auth/request-pin.js3.899
functions/api/auth/verify-pin.js3.6103
functions/api/health.js0.37
functions/api/img/[id].js1.128
functions/api/jury/img/[pos].js0.924
functions/api/me.js1.339
functions/api/my-awards.js4.4122
functions/api/photos.js1.640
functions/api/photos/[id].js4.5109
functions/api/photos/upload.js4.3102
functions/api/results.js2.871
functions/api/vote.js2.972
functions/api/vote/[category].js4.8132
functions/lib/ai.js6.9135
functions/lib/auth.js0.822
functions/lib/email.js4.8133
functions/lib/events.js4.271
functions/lib/jwt.js2.677
functions/lib/notify.js8.2188
functions/lib/params.js1.537
functions/lib/phase.js1.746
functions/lib/regulamin.js0.719
functions/lib/scoring.js0.34
functions/lib/testmode.js6.7148
functions/lib/version.js0.12
public/app.js1.334
public/js/about.js4.6101
public/js/accordion.js2.071
public/js/ai.js9.4265
public/js/auth.js2.377
public/js/confirm.js3.8100
public/js/consent.js2.356
public/js/dom.js0.726
public/js/help.js6.888
public/js/infobar.js3.5114
public/js/lightbox.js1.444
public/js/phone.js1.547
public/js/photos.js14.7405
public/js/results.js9.1241
public/js/session.js0.623
public/js/settings-events.js6.4156
public/js/settings-handlers.js21.4553
public/js/settings-jury.js9.3223
public/js/settings-views.js16.3325
public/js/settings.js22.1351
public/js/tabs.js1.538
public/js/toast.js1.135
public/js/version.js0.12
public/js/voting.js16.4432
public/style.css49.5742
public/sw.js1.218

Razem: JS — 67 plików, 6609 linii, ~274 KB · CSS — 1 plik, 742 linii, ~49 KB.

← Wróć do aplikacji