# v0.1 MVP — Product design

> Scope: the minimum hardware-justifying release. If you take any of this
> away, the device stops being a typewriter.
>
> Companion docs:
> [README](../README.md) (overview) ·
> [technical design](v0.1-mvp-technical.md) (how it's built) ·
> [ADR log](adr.md) (load-bearing decisions) ·
> [QFD](qfd.md) (requirements ↔ functions ↔ components) ·
> [macroplan](macroplan.md) (where v0.1 sits in the sequence).

## One-line summary

Boot the device, type into one file, hit `Ctrl-S` to save, hit `Ctrl-G` to
publish it to GitHub. That's the entire product surface.

## Target user

Me. Solo. v0.1 is not for anyone else — it's the bring-up vehicle that proves
the loop closes end to end.

## User stories

1. **Write.** I open the lid (= plug in power). Within a few seconds the
   screen shows my `notes.md` file. I start typing. What I type appears on
   screen with sub-second latency.

2. **Save.** I hit `Ctrl-S`. The file on SD is updated. A short status flash
   confirms it.

3. **Publish.** I hit `Ctrl-G`. The device ships my latest changes to GitHub
   atomically — if the remote has moved on, it merges in (no-edit) and pushes
   again, so I only see one outcome. If it succeeds, a short hash is briefly
   shown. If it fails, I see why and I can retry.

4. **Recover.** Power gets pulled mid-edit. I plug it back in. My file is at
   the last saved state — I lose unsaved changes, but the file isn't
   corrupted.

That's it. No other stories ship in v0.1.

## Provisioning (build-time, dev-only)

v0.1 has **no on-device provisioning** — no captive portal, no in-editor
wizard, no NVS config file, no LittleFS config. The device is the dev's own
machine (see [Target user](#target-user)); config is baked into the firmware
at build time:

- Wi-Fi SSID + password, git remote URL, GitHub username, PAT, and commit
  author name + email are read by `build.rs` from environment variables (or
  a `.env` file gitignored locally) and embedded as `const &'static str`s in
  the binary.
- The dev clones the git working copy onto the SD card from their laptop
  (`git clone` directly onto the mounted SD), so `/sd/repo/notes.md` is
  present at first boot.
- Re-configuring = re-build + re-flash. For a single-dev MVP, that's fine.

If the firmware boots and finds no `/sd/repo` or no `/sd/repo/notes.md`, it
shows a one-line error on the splash and stops. No recovery flow ships in
v0.1 — the dev re-mounts the SD and reboots.

On-device provisioning UI (Wi-Fi captive portal, PAT rotation, settings
screen) lands in v0.9 when the first non-dev user becomes a concern; at that
point the constants migrate to NVS-on-LittleFS per
[ADR-007](adr.md#adr-007-storage-split--fat-on-sd-for-working-copy-littlefs-on-flash-for-config).
Auth model rationale: [ADR-005](adr.md#adr-005-auth--https--github-personal-access-token).

## Steady-state flow

```
[POWER ON, config present]
        │
        ▼
  Boot (~3 s): Wi-Fi stays off (brought up on demand by `Ctrl-G`)
        │
        ▼
  Open /sd/repo/notes.md, place cursor at end of file
        │
        ▼
  [EDITING] ← keypresses, partial refresh per edit
        │
        ├── Ctrl-S → write file, full refresh, status: "saved 14:02"
        │
        └── Ctrl-G → save buffer; in parallel: commit locally + bring Wi-Fi up
                    (if nothing staged: status "nothing to publish", stop)
                    git add . && git commit -m "<ISO-8601 timestamp>"
                    t≈0.2 s, status: "✓ committed abc1234 · pushing…"
                    once Wi-Fi up: git push
                      └─ on failure: git pull --no-edit, retry push once
                    t≈5–10 s, status: "✓ pushed abc1234" or "publish failed: <reason>"
                    (local commit preserved either way)
```

## Screen layout

GDEY0579T93 — 5.79", **792×272**, 1-bit, partial refresh capable. Wide
strip aspect (~2.9:1) — biases the UX toward "current line and recent
context," not a full page. Hardware rationale: [ADR-003](adr.md#adr-003-display-medium--e-ink-gdey0579t93-panel).

```
┌───────────────────────────────────────────────────────┬─────────────────────┐
│ Lorem ipsum dolor sit amet, consectetur adipiscing    │ notes.md         ●  │
│ elit. Sed do eiusmod tempor incididunt ut labore et   │                     │
│ dolore magna aliqua. Ut enim ad minim veniam, quis    │                     │
│ nostrud exercitation ullamco laboris nisi ut aliquip  │ 1 240 words         │
│ ex ea commodo consequat.                              │                     │
│                                                       │ 12:07 elapsed       │
│ Duis aute irure dolor in reprehenderit in voluptate   │                     │
│ velit esse cillum dolore eu fugiat nulla pariatur.    │                     │
│ Excepteur sint occaecat cupidatat non proident.       │                     │
│                                                       │ 14:02               │
│ The quick brown fox jumps over the lazy▎              │ Wi-Fi —             │
│                                                       │ -- INSERT --        │
└───────────────────────────────────────────────────────┴─────────────────────┘
   writing column (~60 cols, full height)        side panel (~20 ch)
```

- **Writing column** (left, ~60 cols, full panel height): only the text and a
  solid-block cursor at the insertion point. **No blink.** The seam at `x = 396`
  runs through it invisibly (the driver's split-and-mirror blit handles it).
  This is the only region that repaints as you type.
- **Side panel** (right, ~150 px / ~20 cols, full height): all metadata lives
  here, so the writing column keeps the full height. Top — **filename** + dirty
  dot (●/○). Middle — **word count** and **elapsed time**, refreshed on a short
  typing pause, *not* per keystroke. Bottom-left — the **mode** indicator
  (always `INSERT` in v0.1) with any pending count/operator echo. Bottom —
  ambient state: **clock** (if SNTP has set the time), **Wi-Fi** (`—` =
  off/on-demand, `✓` = connected, `✗` = failed), a **keyboard-disconnect** flag
  (rendered `NO KBD` — Latin-9 has no `⌨`/`✗` glyph — shown *only* while the
  keyboard is dropped; blank when healthy, per
  [`CONTEXT.md`](../CONTEXT.md) "No state the user didn't ask for"), and
  **publish state**. `Ctrl-S` briefly flashes "saved HH:MM";
  `Ctrl-G` transitions through `✓ committed abc1234 · pushing…` at ~0.2 s — the
  commit-landed moment, when the user's work is durable (see "Durability before
  delivery" in [`CONTEXT.md`](../CONTEXT.md)) — and ends at `✓ pushed abc1234`
  (or `publish failed: <reason>`, with the local commit preserved either way).
- **Deliberately not shown:** any cursor-position readout (*line* or *column*),
  character count, or reading-time estimate — each repaints a panel row on cursor
  movement or per keystroke, on a row apart from the cursor, doubling the ghosting
  rate for little writerly value. v0.1 ships no positional readout at all.

Fonts: one mono font for the writing column, a smaller one for the panel, both
embedded in firmware. Sizing target: ~60 columns of writing (down from the
full-width ~79 — too wide for a comfortable line length) across the full ~13
visible lines, now that metadata has moved off the top/bottom bars into the side
panel. Final size tuned during spike 2 against real legibility on the panel.

**UX consequence of the strip aspect:** the cursor stays near the bottom of
the visible area during normal typing; the writing column scrolls one line at a
time. There is no "page" — just a continuously advancing strip. Embrace it,
don't fight it.

**What scroll looks like on e-ink:** every line-scroll is a partial refresh
of the full writing column — every line moves, there is no small dirty rect for
scroll — so each scroll costs ~200–300 ms. Per-keystroke partial refreshes
accumulate ghosting; the render module clears it by upgrading every 20th
refresh to a full refresh, deferred to the next idle pause ≥ 1 s so it
usually lands during a thinking moment rather than mid-burst (implementation:
[v0.1 technical → render](v0.1-mvp-technical.md#render--dirty-rect-to-epd)).
`Ctrl-S` and `Ctrl-G` also force a full refresh. Net user experience:
smooth-ish line scrolling while typing, occasional ~1 s flash on a pause or
a save.

## Error UX

| Failure                                                | What the user sees                                                                                                                                                                   |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| No SD card on boot                                     | "no SD card — insert one and reboot"                                                                                                                                                 |
| SD card mounted but `/sd/repo` missing                 | "missing /sd/repo — re-provision and reboot" (dev re-clones onto SD)                                                                                                                 |
| Wi-Fi credentials wrong (build-time env vars)          | panel Wi-Fi field shows `✗`; dev rebuilds with correct env vars and re-flashes                                                                                                       |
| Wi-Fi won't connect                                    | panel Wi-Fi field shows `✗`; editing still works; `Ctrl-G`'s publish-state field shows "no network"                                                                                  |
| `Ctrl-G` publish fails (auth, network, merge conflict) | panel publish-state field shows the reason; local commit is preserved; user can retry — non-fast-forward auto-recovers via pull-then-push and is not surfaced as a failure unless the pull itself conflicts |
| File write fails                                       | panel save field flashes "save failed"; buffer stays dirty                                                                                                                           |
| Keyboard disconnects                                   | panel shows `⌨ ✗` (blank once reconnected); editing pauses; reconnects automatically                                                                                                |

No modal dialogs. Errors live in the side panel's ambient-state fields. The
editor is never blocked from accepting keystrokes (except during the
partial-refresh frame itself).

## Out of scope for v0.1

Explicitly **not** in this release, to keep it shippable:

- Vim modes (Normal/Visual). Everything is Insert.
- Line numbers.
- Multiple files / file palette.
- Branches. Always on whatever branch was cloned (default).
- `git pull`. Manual recovery if the remote diverges.
- Conflict handling.
- Commit message editing — always an ISO-8601 timestamp, no user prompt ever
  (see [`gct`](../CONTEXT.md#user-facing-actions)).
- Battery. Wall power only.
- Sleep / lid-close behaviour.
- Font selection.
- On-device provisioning UI (captive portal, wizard, settings screen). v0.1 is
  provisioned out-of-band from the dev's laptop; users are a v0.9 concern.
- Markdown rendering (headings, bold, etc.) — plain text rendering of MD.
- Search.

## Acceptance criteria

v0.1 ships when **all** of these pass on real hardware. Each criterion maps
to an engineering function with a measured target in
[qfd.md §6](qfd.md#6-critical-performance-budget); that's the
place to check before declaring an item done.

> **Status 2026-07-11:** v0.1 was declared **delivered**, with acceptance
> criteria run as **post-ship hardening**. The 1-hour soak is attested from
> sustained real use, and cold boot is now **verified at 4258 ms** (under the 5 s
> gate, after a boot-refresh fix — see below). One criterion remains known **not**
> to pass: `Ctrl-G`'s pull-then-retry path is **not yet implemented** (deferred
> to v0.9).

- [x] After a cold boot with valid pre-flashed config, cursor is ready in ≤ 5 s.
      — **VERIFIED 2026-07-11: 4258 ms power-on → cursor** (`boot: cursor ready`
      log prefix; 742 ms under the gate). First measured ~5.5 s (over the gate);
      the fix was to bring the editor up with a full-area partial (~630 ms) instead
      of a second full refresh (~1.9 s) — panel confirmed clean, no splash
      ghosting. (Instrument note: `esp_timer_get_time` reads only the ~2.85 s
      app-side slice — it starts ~1.4 s in, after the bootloader + the ~0.74 s
      PSRAM memtest; the log prefix / `esp_log_timestamp` is the real
      from-power-on number.) Remaining boot levers for the v1.0 ≤ 3 s target: the
      ~1.9 s splash full refresh and the ~0.74 s PSRAM memtest.
- [ ] Typing a 1000-word paragraph never drops a keystroke and never lags
      more than 300 ms behind the keyboard.
- [ ] `Ctrl-S` durably writes the file (verified by power-cycling immediately
      after the status confirms save — file is intact).
- [ ] `Ctrl-G` produces a commit visible on github.com within 30 s on a
      typical home Wi-Fi (including the pull-then-retry path when the remote
      has moved on since the last publish).
- [ ] Pulling power during typing never corrupts the file; the previous saved
      state is recoverable.
- [x] One hour of continuous typing without crash, freeze, or memory
      exhaustion. — attested by the author 2026-07-11 from sustained real-use
      sessions (no crash, freeze, or memory exhaustion since the editor was wired
      up). Author attestation, not a controlled instrumented run; a heap-watermark
      log over a timed session would make it rigorous.

## Non-goals as success criteria

These are deliberately _not_ measured in v0.1:

- Boot time below 3 s (Phase 1 target).
- Battery life (no battery yet).
- Push latency under any specific threshold beyond "the user doesn't think
  it's stuck."
