# snapme: Milestones Ini rencana rilis snapme: sebelas milestone yang dikerjakan berurutan; setiap milestone mencantumkan pekerjaannya dan butir "Selesai bila"-nya. Nomor kebutuhan (FR, GS, SE, SH) merujuk ke `PRD.md` §7. ## Status - [x] M0 Fondasi - [x] M1 Welcome dan kerangka booth - [x] M2 Kamera - [x] M3 Gesture Snap - [x] M4 Hias strip - [x] M5 Hasil dan cetak - [x] M6 Link strip dan QR - [x] M7 Strip Hidup - [x] M8 Strip Estafet - [x] M9 Pengerasan - [x] M10 Migrasi Cloudflare ## Berlaku untuk semua milestone - Butir bertanda **(P1)** dikerjakan hanya setelah semua butir lain di milestone itu selesai. Yang tidak sempat dicatat di laporan dan dibawa ke M9. --- ## M0 Fondasi **Tujuan.** Repo siap dikembangkan: kerangka terpasang, komponen dasar sudah bergaya snapme, dan ada galeri komponen untuk dibandingkan dengan halaman acuan. **Baca.** ARCHITECTURE §2, §3, §4. DESIGN-SYSTEM §2 sampai §9, §13. Ketiga berkas di `docs/ui-reference/` sebagai contoh komponen dalam pemakaian. **Pekerjaan** - [x] `git init` bila belum ada, `.gitignore`, `.env.example` kosong. - [x] Scaffold Next.js dan pasang dependensi (ARCHITECTURE §3). - [x] `shadcn init`, tambahkan komponen di DESIGN-SYSTEM §9. - [x] Evaluasi registry neobrutalism (ARCHITECTURE §3), catat keputusan di `docs/DECISIONS.md`. - [x] Token di `src/app/globals.css` (DESIGN-SYSTEM §13), fon lewat `next/font`. - [x] Ganti ikon `lucide-react` di komponen shadcn dengan `@phosphor-icons/react`, lalu lepas `lucide-react`. - [x] Sesuaikan varian komponen dengan DESIGN-SYSTEM §9. - [x] Skrip npm: `dev`, `lint`, `typecheck`, `test`, `build`. Konfigurasi Vitest dan Playwright dengan satu uji contoh. - [x] Halaman `/dev/ui` yang menampilkan setiap komponen dalam semua keadaan. - [x] Pastikan `docs/ui-reference/` berisi tiga berkas HTML acuan dan gambar referensi, dan ketiganya terbuka di browser. **Bukan bagian milestone ini.** Halaman produk, kamera, backend. **Selesai bila** - `/dev/ui` menampilkan semua komponen umum DESIGN-SYSTEM §9 dalam keadaan bawaan, hover, fokus, aktif, nonaktif, dan memuat. - Tombol, chip, tab pil, sakelar, dan kartu di `/dev/ui` tampak sama dengan yang ada di berkas acuan saat dibandingkan berdampingan. - Pencarian nilai warna heksadesimal di `src/` hanya menemukan `globals.css`. - `lucide-react` tidak ada di `package.json`. - Semua skrip npm (`lint`, `typecheck`, `test`, `build`) berjalan dan lolos. --- ## M1 Welcome dan kerangka booth **Tujuan.** Pengunjung melihat Welcome, memilih layout, dan masuk ke kerangka langkah booth. **Kebutuhan.** FR-01, FR-02, FR-15. **Baca.** `docs/ui-reference/welcome.html`. DESIGN-SYSTEM §5, §10.1, §12. ARCHITECTURE §5. **Pekerjaan** - [x] Welcome (`/`) sebagai komponen server, mengikuti anatomi DESIGN-SYSTEM §10.1. - [x] Booth Shell dan stepper (`src/app/booth/layout.tsx`). - [x] `lib/layouts.ts`: geometri Strip 4 dan Strip 3 (DESIGN-SYSTEM §5) dengan uji unit bahwa jumlah pikselnya pas. - [x] `/booth/layout`: pilih layout, simpan ke state booth. - [x] `stores/booth.ts` (kerangka state di ARCHITECTURE §5) dan pengalihan langkah tanpa prasyarat. - [x] `/privasi`. **Bukan bagian milestone ini.** Tombol "Pakai Foto Galeri" belum ditampilkan sampai FR-13 dikerjakan di M4. **Selesai bila** - Welcome cocok dengan `docs/ui-reference/welcome.html` saat dibandingkan berdampingan pada 360 x 640, 768, dan 1280 px, dan "Mulai Foto" terlihat tanpa menggulir. - Bundel `/` tidak memuat `react-webcam`, `@mediapipe/tasks-vision`, `konva`. - Uji Playwright: Welcome ke Pilih layout ke `/booth/capture`. --- ## M2 Kamera **Tujuan.** Pengguna mengambil semua foto untuk layout terpilih dengan tombol atau keyboard. **Kebutuhan.** FR-04, FR-05, FR-06, FR-07. FR-08 **(P1)**. **Baca.** `docs/ui-reference/capture.html`. DESIGN-SYSTEM §10.2. ARCHITECTURE §5 (mesin keadaan), §6 (kamera). **Pekerjaan** - [x] Layar penjelas izin kamera dan penanganan izin ditolak. - [x] `features/camera`: pratinjau dicerminkan, crop sesuai rasio slot. - [x] `features/capture`: mesin keadaan sebagai fungsi murni dengan uji unit. - [x] `CameraStage`, `CountdownOverlay`, `ShutterButton`, `ShotTray`. - [x] Hitung mundur 3 detik, kilat tunggal, pengumuman `aria-live`. - [x] Ulang foto terakhir dan ulang foto tertentu. - [x] Spasi mengambil foto saat fokus di area kamera. - [x] **(P1)** Pilih kamera. **Selesai bila** - Uji Playwright dengan kamera palsu: empat foto terambil, satu diulang, lanjut ke Hias. - Uji Playwright izin ditolak: pesan dan jalan keluar tampil. - Foto tersimpan sama dengan pratinjau (tercermin) dan berasio slot. - Kilat tidak muncul saat `prefers-reduced-motion`. - Halaman cocok dengan `docs/ui-reference/capture.html` pada 360 dan 1280 px (kecuali HUD gesture, yang masuk di M3). --- ## M3 Gesture Snap **Tujuan.** Foto bisa dipicu dengan tanda dua jari tanpa menyentuh perangkat. **Kebutuhan.** GS-01 sampai GS-07. GS-08, GS-09, GS-10 **(P1)**. **Baca.** PRD §7.2. ARCHITECTURE §6 (gesture), §9. `docs/ui-reference/capture.html` untuk empat keadaan HUD dan indikator tahan. **Pekerjaan** - [x] Salin berkas wasm dan model MediaPipe ke `public/` (ARCHITECTURE §4). Tidak memuat dari CDN. - [x] `gestureSource`: dimuat dinamis setelah izin kamera, laju dibatasi. - [x] `holdDetector`: fungsi murni dengan uji unit (tahan cukup lama, lepas lebih awal, jeda setelah foto). - [x] `GestureHUD` dengan empat keadaan dan sakelar Gesture Snap yang diingat. - [x] Status "Menyiapkan gesture..." dan jalur gagal muat. - [x] Uji Playwright: tidak ada permintaan jaringan yang membawa gambar selama `/booth/capture`. - [x] **(P1)** Telapak untuk ulang, jempol untuk lanjut, dua tangan. Dua tangan lewat `numHands: 2` sejak P0; pengenalan telapak dan jempol dibuktikan uji unit `holdDetector`, karena kamera palsu tidak punya tangan. **Selesai bila** - Semua uji unit `holdDetector` dan uji privasi GS-01 lolos. - Dengan sakelar mati, model tidak dimuat dan tidak berjalan. - Hasil uji manual dicatat di `docs/DECISIONS.md`: perangkat, cahaya, berapa kali berhasil dari 10 percobaan, dan ambang akhir yang dipakai. (terpenuhi atas konfirmasi pengguna 16 Oktober 2026: deteksi di HP berhasil; jumlah dari 10 dan uji laptop tidak dirinci; ambang akhir tetap nilai awal, tabel di `docs/DECISIONS.md` M3) **Butuh dari pengguna.** Uji manual di satu HP Android dan satu laptop; deteksi tidak bisa diverifikasi dengan kamera palsu. --- ## M4 Hias strip **Tujuan.** Pengguna menghias strip dan mendapat PNG siap cetak. **Kebutuhan.** FR-09, FR-11. FR-10, FR-13 **(P1)**. **Baca.** `docs/ui-reference/customize.html`. DESIGN-SYSTEM §5 (geometri), §8, §10.3, §11. ARCHITECTURE §6 (editor). **Pekerjaan** - [x] `StripCanvas` dengan react-konva dari `lib/layouts.ts`. - [x] Minimal 6 frame dan 6 efek (preset filter Konva). - [x] Minimal 12 stiker di `public/stickers`. Boleh memakai gambar hasil generasi, wajib ditinjau pengguna sebelum masuk repo. (Dua belas SVG ditulis tangan di repo atas keputusan pengguna, bukan gambar hasil generasi.) - [x] Stiker: tempel, geser, putar, ubah ukuran, hapus, dengan `Transformer` bawaan Konva. - [x] Padanan daftar untuk keyboard dan pembaca layar (DESIGN-SYSTEM §11). - [x] Tab pil Frame, Efek, Stiker. Lembar bawah di layar sempit. (Ditambah tab Teks untuk FR-10.) - [x] Ekspor PNG. - [x] **(P1)** Teks dan tanggal. **(P1)** Pakai Foto Galeri, lalu tampilkan tombolnya di Welcome. **Selesai bila** - Uji otomatis: PNG ekspor Strip 4 berukuran tepat 600 x 1800 px dan di bawah 2 MB. - Satu stiker bisa ditempel, dipindah, diputar, dan dihapus hanya dengan keyboard. - Konva tidak ikut termuat di `/` dan `/booth/capture`. - Halaman cocok dengan `docs/ui-reference/customize.html` pada 360 dan 1280 px. --- ## M5 Hasil dan cetak **Tujuan.** Pengguna mengunduh strip dan mencetaknya di kertas 4R. **Kebutuhan.** FR-11 (unduh), FR-12. FR-03, FR-14 **(P1)**. **Baca.** DESIGN-SYSTEM §5, §9. PRD §7.1. **Pekerjaan** - [x] `/booth/result`: pratinjau, "Selesai & Unduh", "Cetak 4R", "Mulai dari awal". - [x] Nama berkas `snapme-YYYYMMDD-HHmm.png`. - [x] Lembar 4R 1200 x 1800 px: dua strip berdampingan dengan garis potong, dan gaya cetak satu halaman 4 x 6 inci. - [x] **(P1)** Layout Grid 2 x 2. **(P1)** Sesi bertahan saat dimuat ulang (IndexedDB). **Selesai bila** - Uji Playwright: alur penuh dari Welcome sampai berkas terunduh. - Pratinjau cetak Chromium menampilkan tepat satu halaman. - Alur dasar selesai tanpa satu pun permintaan ke server selain aset statis. Setelah M5, snapme sudah bisa dipakai sebagai photobooth lokal (tahap Alpha di PRD §10). --- ## M6 Link strip dan QR **Tujuan.** Pengguna bisa membuat link pribadi untuk stripnya dan menaruh QR di strip. **Kebutuhan.** SH-04, SH-05, SH-06. **Baca.** ARCHITECTURE §7, §8. PRD §7.4, §8. **Butuh dari pengguna sebelum mulai.** Proyek Supabase (URL dan kunci service role di `.env.local`), dan keputusan cara pembatasan laju (ARCHITECTURE §8). **Pekerjaan** - [x] Skema `strips` dan bucket pribadi sebagai berkas migrasi di repo. RLS menolak `anon`. (`supabase/migrations/20261012000000_strips_bucket.sql`; penerapan ke proyek menunggu proyek pengguna) - [x] Rute `POST /api/strips`, `POST /api/strips/[id]/complete`, `DELETE /api/strips/[id]`, `GET /api/cron/purge`. (diuji unit dengan klien Supabase palsu) - [x] Validasi `zod`, batas jenis dan ukuran berkas, token hapus yang di-hash. - [x] Dialog persetujuan unggah, unggah langsung ke Storage dengan URL bertanda tangan. (jalur 503 sudah diuji e2e; unggahan sungguhan menunggu proyek) - [x] `QRPlate` di kaki strip (168 px pada ekspor), render ulang strip setelah link dibuat. (kotak QR dan glif penanda sudah terbukti di browser; render ulang ber-QR menunggu unggahan sungguhan) - [x] Halaman `/s/[id]`: strip, unduh, tanggal kedaluwarsa, hapus. `noindex`. Jawaban 410 bila kedaluwarsa. (halaman menjawab 404 untuk strip yang tidak ada, terbukti lewat `curl`; status 410 harfiah dijawab rute API, penyimpangan tercatat di `DECISIONS.md`) - [x] Header keamanan (ARCHITECTURE §8). (diverifikasi lewat `curl -i` pada `/` dan `/s/abc`; CSP penuh menyusul di M9) **Selesai bila** - Uji end-to-end: buat link, buka `/s/{id}` di konteks lain, hapus, lalu halaman menjawab tidak ditemukan. (hijau: `SUPABASE_E2E=1 npx playwright test share.spec.ts` → 3 lolos, 1 dilewati; alur sungguhan sampai unggah ke Storage, konteks kedua, dan hapus) - Uji unit kedaluwarsa: strip lewat 7 hari menjawab 410 walau pembersih belum berjalan. (hijau: `tests/api-strips.test.ts`) - Kunci service role tidak muncul di bundel klien. (hijau: build dengan `.env.local` sungguhan, 50 berkas `.next/static` dipindai, nol memuat kuncinya; `tests/client-bundle-secrecy.test.ts`) - Tinjauan keamanan atas `src/app/api` dan `lib/supabase` sudah dilakukan, dan temuannya ditangani atau dicatat. (delapan temuan: satu blocker dan empat major sudah diperbaiki, sisanya diperbaiki atau dicatat; tabelnya di `docs/DECISIONS.md` bagian M6) **Batas aman.** Migrasi sudah dijalankan ke proyek Supabase `snapme` (ref `owdjkpzywzljnvuyfiui`) atas persetujuan pengguna 9 Oktober 2026, lewat `supabase link` + `supabase db push`. Perubahan skema berikutnya mengikuti aturan yang sama dan selalu dengan persetujuan pengguna. --- ## M7 Strip Hidup **Tujuan.** Tiap pose punya klip pendek, strip bisa diputar, dan QR membuka versi bergeraknya. **Kebutuhan.** SH-01, SH-02. SH-03 **(P1)**. **Baca.** PRD §7.4. ARCHITECTURE §6 (Strip Hidup), §9. **Pekerjaan** - [x] Perekam bingkai di mesin keadaan pemotretan: 10 bingkai 480 px pada 1,5 detik terakhir hitung mundur. (`src/features/capture/clipRecorder.ts` + `useClipRecorder.ts`; sampling 1500 sampai 2850 ms, jadi momen jepret di 3000 ms tidak tersentuh) - [x] `LiveStripPlayer` dan sakelar Strip Hidup di Hias. (diwujudkan sebagai prop `live` di `StripStage` + `useClipTicker`, dengan lencana "Klip"; alasannya di `DECISIONS.md` M7) - [x] Unggah versi hidup bersama strip, tampilkan di `/s/[id]`. (migrasi bucket 5 MB + jenis video diterapkan ke proyek atas persetujuan pengguna 9 Oktober 2026; bukti ber-jaringan di `DECISIONS.md` M7) - [x] Hormati `prefers-reduced-motion`: berhenti dengan tombol Putar. (di Hias dan di pemutar `/s/[id]`) - [x] Lepas `ImageBitmap` saat booth dimulai ulang. (`setShot`, `setLayout`, dan `reset` memanggil `close()`; dijaga `tests/booth-live.test.ts`) - [x] **(P1)** Unduh video lewat `MediaRecorder`, tombol tersembunyi bila tidak didukung. (2 putaran x 10 bingkai = sekitar 3 detik, di bawah 5 MB; dijaga `e2e/live.spec.ts`) **Selesai bila** - Perekaman bingkai tidak mengubah waktu pengambilan foto utama (uji unit pada mesin keadaan). (hijau: `tests/clip-recorder.test.ts` menjalankan tabel peristiwa yang sama dengan dan tanpa pengamat perekam) - `/s/{id}` menampilkan versi hidup untuk strip yang punya klip, dan versi diam untuk yang tidak. (hijau: `SUPABASE_E2E=1 npx playwright test share.spec.ts live.spec.ts` → 8 lolos, 1 dilewati; konteks kedua melihat `