snapme
PublicCHANGELOG.md
snapme is built in the milestone order of MILESTONES.md, one milestone per commit.
Entries below follow the milestone, not a tag: 0.1.0 is the first released state and contains the
whole of M0 to M10. The reasoning, measurements, and failed attempts behind each entry are in
docs/DECISIONS.md.
Unreleased
Housekeeping after 0.1.0: the repository was audited for dead weight and reorganised, and snapme now carries its own mark. Nothing else in the flow changed.
Added
- snapme's own mark, and the wordmark, favicon, and app icon built from it. The mark is a paper tile
holding a fan of four photo prints; the favicon uses a single print, because four of them collapse
into a colour patch at 16 px.
public/brand/snapme-mark.svgis the source,src/app/icon.svgthe small-size variant, andnpm run brand:iconsderivessrc/app/apple-icon.pngandsrc/app/favicon.ico(16, 32, and 48 px) from both. The 15 KBcreate-next-appfavicon, whose three frames were uncompressed bitmaps, is replaced by a 4.6 KB one with PNG payloads sharpas a dev dependency: the only rendererscripts/render-brand-icons.mjsneeds
Changed
- Server-only modules moved from
src/libtosrc/server(the binding seam,db/,r2/,purge,rateLimit,tokens,expiry), so the client-boundary rule is a path, not a hand-kept list tests/now mirrorssrc/(tests/app/api,tests/components,tests/features,tests/lib,tests/server,tests/security,tests/stores), with the miniflare harness and the route helpers intests/support- Client network calls share one module,
src/lib/http.ts(messageFrom,putToSignedUrl) src/features/gallery/muatFotoGaleri.tsisloadGalleryPhotos.ts; the exported names follow it- Every module in
src/components/uiimportscnfrom@/lib/utils, matching whatshadcn addgenerates - The repository root now holds only the entry points and the tool configs: the four working
documents (PRD, design system, architecture, milestones) moved to
docs/next to the decision log,r2-cors.jsonmoved toinfra/, and the Playwright specs moved frome2e/totests/e2e/so there is one test root tsconfig.tsbuildinfois written into.next/cache/, so no TypeScript artifact lands in the repository root- Product components moved out of
src/components/boothinto the features that own them: the booth shell tosrc/features/booth, the camera picker tofeatures/camera, the capture stage, shot tray, countdown and shutter tofeatures/capture, the gesture HUD and toggle tofeatures/gesture, the relay invite card tofeatures/relay.src/componentsnow holds onlyui/(the shadcn/ui primitives) andsite/(header and wordmark) - The result screen moved out of the editor:
ResultView, the strip export, and the 4R print sheet now live insrc/features/result, sosrc/features/editoris only the decorating step GalleryPickermoved fromsrc/components/sitetosrc/features/gallery, next to the loader it uses; component tests followed their components intotests/features/<area>- The placement rule is written down in
ARCHITECTURE.md §4and in the repository conventions in.github/CONTRIBUTING.md, so the next milestone does not have to rediscover it DESIGN-SYSTEM.md §8now documents the mark, its files, and the rule that the raster icons are derived rather than drawn twice; §10.1 shows the mark in the top bar, andARCHITECTURE.md §4listspublic/brand/and the three icon files undersrc/app/brag-output*/**is ignored byeslint.config.mjsand by git: a/bragrun writes a Hyperframes composition (with a vendoredgsap.min.jsthat fails the project's lint rules, exactly like the MediaPipe wasm) plus a rendered video, and neither belongs tonpm run lintor to the repository
Removed
CreateStripPayloadandCreateRelayPayloadinsrc/lib/schemas.ts: type aliases nothing imported, while the routes take their types from the Zod schemas directly- The
exportkeyword on 26 symbols that only their own module uses (StripLayout,CropRect,GESTURE_ACTIONS,BoothSettings, and the like), so the module surface matches what is really shared.src/components/uikeeps the exports the shadcn CLI writes, soshadcn addstays comparable with upstream --color-pink-700: defined and documented but never used in any component. The palette and the contrast table inDESIGN-SYSTEM.md §2no longer list it- Duplicated helpers: two copies of the canvas checksum (three, counting
customize.spec.ts), the four-slot fill, the camera permission step intests/e2e/gesture.spec.ts, and the request, rate-session, route-context, and bucket-clearing helpers in the route tests. The two bucket copies had already drifted: only one paginated
Backend dibongkar: snapme jadi client-only (18 Oktober 2026)
Keputusan pengguna: cabut seluruh backend Cloudflare (Workers/OpenNext, D1, KV, R2, wrangler) dari proyek ini. snapme kini aplikasi klien murni — booth kamera berjalan penuh secara lokal, tanpa unggahan ke mana pun dan tanpa env yang dibutuhkan build.
Removed
- Seluruh sisi server:
src/server/(cloudflare.ts,db/,r2/,rateLimit.ts,expiry.ts,purge.ts,tokens.ts),src/app/api/cron/,worker.ts,open-next.config.ts,wrangler.jsonc,playwright.cf.config.ts,infra/,migrations/,scripts/cf-typegen.mjs,scripts/patch-opennext.mjs - Komponen dan modul yang hanya berguna di atas backend:
RelayJoinView,RelayCompleteView,RelayCancel,relay/bootstrap.ts,StripOwnerActions,LiveStripVideo - Uji dan penopangnya:
tests/support/,tests/app/api/,tests/server/, jalur e2e ber-binding (CLOUDFLARE_E2E) beserta helperpasangSidangBaru/undangEstafet - Dependensi
@opennextjs/cloudflare,aws4fetch,wrangler,lighthouse; skrip npmcf:patch,preview,preview:e2e,deploy,cf-typegen,test:e2e:cf,perf:lcp - Berkas env
.env.exampledan.dev.vars.example, termasuk salinan lokalnya (.env.local,.dev.varsyang berisi kredensial R2) dari mesin pengembangan
Changed
- Sembilan rute
/api(strips,strips/[id],strips/[id]/complete,relays,relays/[id],relays/[id]/claim,relays/[id]/complete,relays/[id]/strip,events) menjadi stub yang menjawab 503 dengan pesan "belum tersedia" yang tetap, supaya klien menampilkan pesan itu, bukan galat jaringan atau 404 /s/[id]dan/r/[id]menjadi stubnotFound()(selalu 404) dengan metadatanoindex; headernoindexdanno-referreruntuk/s/*dan/r/*tetap dipasangnext.config.ts- CSP di
next.config.tstanpa satu pun host luar;postinstallkini hanyacopy-mediapipe-wasm.mjs;src/lib/schemas.tsdipangkas ke ekspor yang masih dipakai klien - Dokumen kondisi-kini (README, ARCHITECTURE, PRD, CONTRIBUTING, SECURITY) ditulis ulang untuk kondisi client-only
Konsekuensi
- Alur booth tidak berubah: welcome, layout, capture, gesture MediaPipe, customize, hasil, ekspor PNG/cetak, perekaman lokal Strip Hidup, dan sesi IndexedDB tetap berjalan seperti sebelumnya
- Fitur bagikan/estafet tetap utuh di UI (dialog persetujuan, kartu bagikan, QR lokal) dan kini selalu menerima pesan "belum tersedia"; tidak ada gambar yang meninggalkan perangkat
- Antrean analitik klien tetap ada dan diam-diam membuang batchnya selama rute peristiwa menjawab 503
0.1.0 - 2026-10-16
First release. The full v1 scope of PRD.md §5, deployed on Cloudflare Workers.
Added
M0 Fondasi (1988139)
- Next.js App Router project with TypeScript, Tailwind v4, and shadcn/ui components restyled through the design tokens
- Design tokens as the single source of color, radius, shadow, and font, plus the component gallery
at
/dev/ui - Vitest, Playwright, ESLint, and the
wranglertoolchain wired into npm scripts
M1 Welcome dan kerangka booth (7a0872e)
- Welcome page with one primary action, and the three feature panels where the Gesture Snap panel is the widest
- Booth shell: stepper, three-column layout down to one column, and the main action pinned to the bottom on narrow screens
- Layout picker for Strip 4, Strip 3, and Grid 2x2
M2 Kamera (880bc00)
- Camera permission screen shown before the browser dialog, with the gallery picker as the fallback when permission is refused
- Mirrored preview with a crop guide that matches the chosen layout, so the photo is stored as it looks on screen
- Countdown capture from the shutter button or Space, single flash, shot tray with per-slot retakes, and camera selection when a device has more than one
M3 Gesture Snap (89a5aea, 40dc887)
- Self-hosted MediaPipe wasm and gesture model, loaded only after camera permission and never on the Welcome route
- Hold detection as pure functions with thresholds in one file, the capture state machine reduced from events, and the on-screen indicator with "Terdeteksi. Tahan..."
- Gesture switch that is remembered on the device,
Open_Palmto retake the last photo, andThumb_Upto move on once every slot is filled - A network assertion that no request carrying image data leaves the capture step
M4 Hias strip (40dc887)
- Konva strip canvas with 6 frames, 7 effects, and 12 stickers that can be dragged, rotated, scaled, and deleted
- Short text line and date, keyboard controls for every decor item, and a list equivalent for screen readers so the canvas is not the only way to reach them
M5 Hasil dan cetak (771ea64)
- PNG export at 600 x 1800 (2 x 6 inches at 300 dpi) without metadata, named
snapme-YYYYMMDD-HHmm.png - 4R print sheet with two strips side by side and a cut line
- Session persistence in IndexedDB, restored after a reload, with "Mulai dari awal" to clear it
- Gallery picker for JPEG, PNG, WebP, and HEIC where the browser supports it
M6 Link strip dan QR (b4372be)
- Consent dialog before the first upload, naming what is uploaded, who can see it, and when it is deleted
- Direct browser upload to a private R2 bucket through presigned URLs, verified server side with
bucket.head - Strip page at
/s/{id},noindexandno-referrer, with a delete token kept on the device - QR placed in the strip footer at 168 px, so it survives a 300 dpi print
M7 Strip Hidup (099d70a)
- Ten 480 px frames captured during the last 1.5 seconds of each countdown, without delaying the still photo
- Live preview that stops under
prefers-reduced-motionand offers a play button - Clip export as WebM or MP4 following what the browser supports, with the button hidden when recording is unavailable
M8 Strip Estafet (292ca73)
- Relay link at
/r/{id}for two people on two devices: the creator fills the odd slots, the friend fills the rest, and the friend's preview carries a 35% shadow overlay of the creator's photos - Single claim enforced by a conditional database update, so a second visitor is told the relay is taken
- Relay links expire in 48 hours and strips in 7 days; the creator can cancel and delete
- After completion both sides get the strip page, and the creator sees "Menunggu teman" then "Selesai"
Changed
M9 Pengerasan (ee4ae8e)
- Full CSP,
Permissions-Policy,X-Content-Type-Options, and no-referrer rules on private pages - Rate limits: exact strip and relay quotas counted in the database, and an hourly analytics budget in KV
- Analytics through a first-party endpoint, without cookies and without an identity column
- Accessibility pass: axe on every page, keyboard-only completion of the whole flow,
aria-livecountdown, and status text beside every color-coded state - Cross-browser test projects for WebKit and Gecko
M10 Migrasi Cloudflare (1a2d64a)
- Backend moved from the previous hosted stack to Cloudflare Workers with the OpenNext adapter, so the whole app, the API routes, and the daily purge job run as one Worker
- Data in D1, files in a private R2 bucket, the analytics budget in KV, and scheduled cleanup through a Cron Trigger
- Route tests run against real D1, R2, and KV bindings through
wrangler getPlatformProxy - Welcome LCP measured in production at a 2.416 s median against the 2.5 s budget
Not in this release
Out of v1 scope on purpose, per PRD.md §5: accounts and a personal gallery, payment and print ordering, AI face filters or generated images, real-time shared booths, relays of more than two people, native apps and locked kiosk mode, and a template CMS.