Frontend: B4 — Cook mode (full-screen step-by-step) #25
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Full-screen cooking mode. One step at a time, large readable text, tap-anywhere to advance. Kitchen-critical — the user has wet hands and is standing at a stove.
Journey: J3 — Cook tonight
Role: Planner
Screen: B4
Context: Mobile, hands busy, kitchen environment
Layout — IDENTICAL on all breakpoints
This is the only screen that uses the same layout on mobile, tablet, and desktop. No sidebar, no tabs, no navigation chrome on any breakpoint.
--greenfill, width = step/total percentage--green-lightcolorCritical Design Constraints
navigator.wakeLock.request('screen')on enter, release on exitBehavior
cooking_logwith today's date → return to C1Acceptance Criteria
Spec file:
specs/frontend/j3-cook-tonight.html— screen B4 (KITCHEN CRITICAL) with mobile + desktop previews (identical layout), agent table with non-negotiable constraints (16px/1.75 body text, wake lock, tap-anywhere), and LLM implementation guide.👨💻 Kai — Frontend Engineer
B4 is unusual — it's the simplest layout in the app (single column, identical across breakpoints) but has the most critical non-visual requirements: wake lock, tap-anywhere interaction, and a write operation at the end. Let me think through the implementation.
Component structure:
Single page, minimal decomposition needed:
CookModeHeader(exit + progress),StepDisplay(step number + text + hint),CookModeFooter(only visible on final step). Maybe 3 files total. The simplicity is intentional — don't over-split this one.Tap-anywhere interaction:
<button>or<div role="button">covering the full viewport (minus the topbar exit button). The tricky part: the exit button must not advance the step when tapped — it needs to stop event propagation from the overlay click handler.SpaceorArrowRightalso advance? The spec doesn't say, but I'd add keyboard support for accessibility — especially since a keyboard user standing at a desk is a real scenario.Screen wake lock:
navigator.wakeLock.request('screen')— I'll call this in$effect()on mount, notonMount(), since we're in Svelte 5.document.addEventListener('visibilitychange', ...)handler to re-acquire it when the tab comes back into focus. If I don't, the screen will still sleep after the user checks their phone.try/catchand fail silently — the feature degrades gracefully.release()the lock explicitly.Confirmation dialog on exit:
window.confirm()(simple but ugly) or a custom modal component? Given the kitchen context, native confirm might actually be preferable — it doesn't require building a modal. But if a custom modal already exists in the design system, I'd use that."Mark as cooked" → INSERT cooking_log:
+page.server.tsaction (not a client-side fetch) so it works even if JS is slow. The action receives the recipe ID (or plan slot ID?), inserts the log entry, then redirects back to C1.{ recipe_id, date }or{ plan_slot_id, date }? If a recipe is cooked outside of the plan, does that also feed the variety score?State management:
currentStep: $state(0)— simple integer, incremented on taptotalSteps: number— derived from the steps array length/cook/[recipeId]), or does it receive the full recipe data via some other mechanism (store, URL state)?One concern: The spec says "Final step shows 'Done — mark as cooked' action." If a user taps the final step area, does it advance to a "done" state, or does the "Mark as cooked" button appear and the user must tap it explicitly? I'd make it explicit — accidental logging would be annoying.
🔧 Backend Engineer
B4 is lightweight on the backend — one write endpoint and one read. But the
cooking_logentry it creates feeds the variety algorithm, so the data model matters a lot here.The write:
POST /api/cooking-log{ planSlotId: UUID }or{ recipeId: UUID, cookedOn: LocalDate }. The plan slot approach is cleaner — it links the log entry to the specific plan, which is more queryable for the variety algorithm.(household_id, recipe_id, cooked_on)), or return a 409? I'd lean toward idempotent upsert — in a kitchen, a confused second tap shouldn't show an error.planSlotId(orrecipeId) belongs to the user's household. Don't accept arbitrary IDs from the client.The
cooking_logtable:The variety algorithm depends on this. I'd expect:
cooked_onis aDATE(notTIMESTAMPTZ) — do we agree on that? The variety algorithm cares about which day something was cooked, not the exact time.The variety algorithm:
cooking_logfor a rolling window (e.g., last 30 days), or only from the current week's plan?GET /api/weeks/{year}/{week}response needs to joincooking_logfor historical data — that could be a heavier query than it looks. Worth discussing the algorithm before the endpoint is built.The read: navigating to B4
stepsJSONB column onrecipe, or a normalizedrecipe_steptable? If it's normalized, the API needs to return steps ordered by their sequence number.plan_slot_id(to link the log correctly) or just arecipe_id? If someone opens cook mode from the recipe list without a plan slot, can they still log it?One edge case: What if the recipe has no steps (the spec says steps are optional at save time in B3)? B4 can't show an empty step-by-step view. Does the "Cook now" button get hidden on B2 if there are no steps, or does B4 handle this gracefully?
🧪 QA Engineer
B4 looks simple on the surface, but it has some genuinely tricky test scenarios — async browser APIs, stateful progression, and a write at the end that feeds a downstream algorithm.
Happy paths:
cooking_logentry exists in the databasecurrentStep / totalSteps * 100%cooking_logentry createdEdge cases that concern me:
navigator.wakeLock.requestrejects (unsupported browser, permission denied, low battery on some devices). Does the component handle the rejection gracefully and continue working?visibilitychangeevent.Component tests (Svelte):
readonlymode: not applicable here (B4 is planner-only), but verify the route is guardednavigator.wakeLockand verifyrequest('screen')is called on mount,release()is called on exitIntegration tests:
POST /api/cooking-logwith validplanSlotId→ 201, entry in DBPOST /api/cooking-logwithplanSlotIdfrom a different household → 403POST /api/cooking-logunauthenticated → 401E2E (critical — this is a core user journey):
One question I need answered before writing the component tests: does the step progression happen by advancing a
currentStepindex, or does the component actually remove/replace DOM elements? The test strategy differs slightly depending on how it's implemented.🔒 Sable — Security Engineer
B4 is simpler than C1 from a security standpoint, but the "Mark as cooked" write and the full-screen, navigation-free layout create a few specific concerns worth addressing explicitly.
Authorization on the write endpoint:
POST /api/cooking-logmust validate that the authenticated user belongs to the same household as the referenced plan slot or recipe. This is not optional — it's a direct write to thecooking_logtable, and if theplanSlotIdis not validated against the session's household, a user could log cooking events for another household's recipes.hooks.server.tsthat prevents members from navigating to B4 at all? If not, and a member somehow reaches/cook/[recipeId], the page will render but the "Mark as cooked" action must still fail at the API level with 403.No navigation chrome = CSRF surface consideration:
POST /api/cooking-logaction uses the SvelteKit CSRF protection mechanism (either form action with the built-in token, or a custom header likeX-Requested-Withfor JSON endpoints). The absence of a nav doesn't remove the CSRF surface.Wake lock API:
navigator.wakeLockis a browser API that requires the page to be focused and visible. It does not have security implications for our app beyond needing to handle errors gracefully (which Kai is already planning). No concerns here from a security perspective.Input validation:
planSlotIdorrecipeIdsent toPOST /api/cooking-logmust be validated as a valid UUID format before any database query. Malformed UUIDs sent by a manipulated client should return 400 (bad request), not a 500 from the database.cookedOndate (if client-provided): should this come from the client at all? I'd prefer the backend derive the date from the server clock at the time of the request. A client sendingcookedOn: "1970-01-01"to backdate a cooking log entry could manipulate the variety algorithm. Server-side timestamp is safer.Information leakage on error:
One open item: The confirmation dialog on exit — is it a native
window.confirm()or a custom modal? If it's a custom modal rendered in-page, confirm it hasrole="dialog",aria-modal="true", and focus is trapped inside it. From a security standpoint, a custom modal dismissible by pressingEscapeis fine, but focus must not leak to the background.🎨 Atlas — UI/UX Designer
B4 is the screen I'm most protective of. The design constraints are not aesthetic choices — they are functional requirements for a kitchen environment. Here's what I'm watching for.
The non-negotiables (I will flag any deviation):
text-sm(14px) here —text-base(16px) only.max-w-[260px](or equivalent token) on the step text container.--green-lightcolor. This large number is the primary visual anchor — it tells the user at a glance where they are without reading. Don't reduce the size.Tap target:
cursor-pointerover the body area to communicate that clicking advances the step.Exit button:
--color-error. Confirm the exact hex/token. The exit button should be visually distinct and immediately findable — but not so large that it gets accidentally tapped while reading. A 40×40px minimum tap target with the icon centered is appropriate.Progress bar:
--greenfill. Very thin — intentional. It's ambient feedback, not primary UI. Placed at the very top below the topbar header. Confirm it spans the full viewport width (not padded).Final step state:
Breakpoint behavior:
+layout.sveltefor the cook mode route tree.