# Status — Thu 30 July, demo day

Living doc. Update it when something moves.

---

## Built and pushed

- **The machine is a Nuxt 4 app, and it's the only build now.** A Vue SPA with
  `ssr: false`: one page, 28 components, one composable, four utils files. Both
  stylesheets came across from the old single-file app unchanged, so the design
  didn't move — verified screen by screen in a browser against the React build
  before it was deleted (README "How the app is put together" lists what was
  checked, and the one thing that couldn't be: a real take through SEND, which
  needs the mic). **That answers the "which build do we keep" question** — the
  React `index.html`, `tape-parts.js`, `server.py` and both prototype pages are
  gone, so there's one copy of the tape part maps again, at
  `app/utils/tape-parts.js`. Two costs came with it: the app now needs
  `pnpm install`, and **the cassette sandbox went with the React build and has no
  successor** — `prototype-hybrid-tape.html` was where a change to the part maps
  got looked at. It would come back as a route in about forty lines.
- **The Louie hardware path** (Tue night, Hamish) — Louie ships as a seeded
  friend with `hasHardware: true`; sending him a memo also POSTs the take to
  the demo server on Hamish's NUC (`am.mynameishamish.website`), which
  transcodes it and feeds the physical answering machine. Fail-soft: server
  down → the take stays on the tape locally, LCD shows `SEND ERROR`. **This
  deliberately crosses the "no backend / Patrick owns storage" non-goal** for
  one friend only — the server lives outside this repo, the app change is four
  small insertions, and deleting the flag reverts it to pure prototype. The
  auth token is committed on purpose: it's demo-grade, gates only the demo
  inbox, and anything secret (WiFi, DNS creds) stays out of the repo. Full
  detail in README "The Louie hardware path".
- **Real recording** — MediaRecorder, permission handling, `NO MIC` error state
- **Listen-back review** — play back, re-record, delete, then send
- **Sent replies persist** on the recipient's tape, visually subordinate
- **Tapes tab** (was CONTACTS) — **your cassette is your avatar.** Full-size tape
  at the top labelled with your name: pencil opens the customiser (shell / label /
  font), tapping it shares your invite link with the "add more friends to unlock
  extra customisation" line. Friends' tapes below are read-only — they arrive
  looking however their owner made them, and tapping one raises a **bottom sheet**
  out of the foot of the machine: GO TO VOICE NOTES, a brown bin in the corner
  behind an *"are you sure"* confirm, and tap-off to dismiss. Ghost empty state and
  the invite-link add flow with clipboard copy and retro share tray are unchanged
- **Home organised by person** — swipeable carousel of friend cassettes, deck
  loads whatever the carousel settles on, per-friend message lists, unread badges
- **REC from the queue asks who it's for** — on the machine's own tape it raises a
  sheet of cassette spines; the one you tap goes in the deck and starts recording
  on the same tap. REC on a friend's tape is unchanged
- **Sequential playback with beeps** — one `playChain(list)`, so a friend's tape
  and "everything waiting" are the same code path. `BEEP.mp3` between each, LCD
  showing whose message and N of M, unread ticking down as each one finishes,
  `END OF MESSAGES` at the finish, STOP halting cleanly mid-chain
- **Play everything from the machine's own tape** — first card in the carousel,
  holds what's outstanding across everyone, loaded at boot and whenever home
  opens. The carousel travels to each person's tape as their message plays
- `FRIENDS` as single source of truth; the cassette itself walled off for Kat in
  `app/utils/tape-parts.js` and `app/assets/css/tape-parts.css`
- **Every cassette in the app is one component.** The deck, the shelf, your own
  tape, the customiser preview, a friend's sheet and the REC picker's spines are
  all `app/components/Cassette.vue` at a different `TAPE_W`, drawing from
  `app/utils/tape-parts.js`. There were once three copies of the part maps in
  three pages and they drifted; now there is one definition and nothing left to
  drift from
- **Native interaction layer** — hover moved behind `@media (hover: hover)` and
  given `:active` twins everywhere, contained scrollers, no tap flash or tap
  delay, safe-area insets on `body`. Rules in CLAUDE.md under **Native
  interaction rules**; what's in the file is in README under **Native
  interaction layer**. Look native, stay retro — no Material or HIG *visual*
  language anywhere
- **Every control clears the HIG 44×44 minimum on both axes** — audited by
  hit-testing every state on the served page, modals and review panel included,
  with the measured table in README. Most of it is invisible pseudo-element
  expanders. Three structural notes: `.seg-tabs` lost its `overflow: hidden`
  (it was clipping expanders), tappable rows carry `min-height: 44px`
  themselves, and the review buttons stay 33px tall on purpose because the panel
  only has 75px while the LCD is open.
  **One visible change:** the ✓ / ✕ on a message row went 19→24px with their gap
  10→28px. Two 44px targets don't fit in 48px of row, so the keys had to move —
  everything else looks exactly as it did. Revert those two lines if the look
  matters more than the row keys hitting the minimum

- **Onboarding is in the app** — the four-step flow, now
  `app/components/Onboarding.vue` and its `Ob*` steps, reached by **tapping the
  `Julifone` wordmark**.
  It replaces the machine in the frame rather than sitting on it, at the same
  402×874 on the same `--fit`. Finishing writes your name, your tape and your
  invite code (`ME` is a seed now, `me` is state); the back arrow on the intro
  leaves everything untouched. It's a demo shortcut on purpose — there's no
  storage, so a flow that ran at boot would run on every reload

- **Worktrees are how we work now** — one worktree per task, one port each, so two
  versions of the machine can be served and compared side by side. The directory
  suffix is the slot and the slot is the port (`answeringmachine-2` → 3457);
  `pnpm dev` needs no argument in any of them — `scripts/dev.mjs` reads the slot
  off the directory name and refuses a taken port rather than sliding, which
  `nuxt dev` on its own will do. **Slot 1 has to stay on 3456**: the Louie
  hardware path is gated on `location.port === '3456'`, so the demo runs there
  and nowhere else. A finished worktree waits to be merged back on Kat's word
  rather than landing itself. Table and commands in
  [README.md](README.md#worktrees-and-ports)

---

## Still outstanding

**Demo reset** — `friends` seeds from `FRIENDS` with five people, but the demo
opens on the ghost cassette. No way back to the empty state without editing code,
and it needs rehearsing more than once. Hidden long-press on the JULI-FONE 850
badge. Note the wordmark is now spoken for — a tap on it opens onboarding — so
the reset wants its own surface, or it wants folding *into* onboarding: entering
the flow could be the reset, since it's already where you make a new identity.

**Your name is a fixture *until you run onboarding*.** `ME` still seeds `Alice`
and her hardcoded link, so a fresh load is Alice — but the wordmark now opens
the sign-up flow and finishing it writes `me.name`, `me.invite` and `myTape`.
Whoever demos it can be themselves in four taps. Nothing persists, so a hard
refresh is Alice again.

**The unlock line is copy only — but it now has something to point at.** The
customiser's bottom row of four stickers is greyed and padlocked with *"Add more
friends to unlock more stickers!"* under it, so the promise is visible rather
than just written. It's still a constant (`STICKER_LOCKED`) and not driven by
`friends.length`, and every shell, label and font is available at zero friends.
Gating the sticker row on the friend count is now a one-line change — that's
the cheapest way to make the whole claim true.

**iPad scaling is done, with one limit left.** `.device` is 402×874 and scales
by transform to fit the window — `--fit` in `app/assets/css/main.css`, pure CSS,
no media query and no resize listener. It's capped at 1, so the machine fits
*down* onto a small window but never grows to fill an iPad. Lifting that cap is a
one-line change in that calc; whether a blown-up machine looks better than a
sharp small one is the design call nobody has made.

**`prefers-reduced-motion` is not honoured** — the reels, the tape-spin, the row
shake and the 180ms LCD resize all run regardless. It's the one native-guideline
item deliberately left open: honouring it means the reels stop turning, and
whether the demo should ever show a still cassette is a design call, not a
mechanical fix. Decide before it gets built.

**Nav tab unread dots** — small, and makes the other tabs feel alive. Sensibly
merged with the bottom-nav work.

---

## Worth simplifying if there's time

- **`useMachine()` holds about thirty refs.** A couple look vestigial —
  `pressedBtn` and `playRate` were each suspected dead in the React version and
  came across unchanged. Worth an audit as its own commit, in case one turns out
  to be load-bearing.
- **Splitting the file is done** — that was the Nuxt port. What's left of the
  same instinct: `useMachine.js` is ~900 lines and the recording plumbing
  (`startRecording`, `finishRecording`, `publishRecording`, `teardownRecorder`)
  is a clean seam if it ever wants its own composable.

---

## Not doing

Storage, custom greeting, transcription, import, radio changes. See
[PRODUCT-BRIEF.md](PRODUCT-BRIEF.md) for why.
