# PUTTV device flasher (`enduserprovision`)  — BETA

The customer-facing tool for turning a blank SD card or USB stick into a PUTTV
signage device. It downloads the current OS image for the chosen hardware type
(Raspberry Pi or x86) and writes it to a **removable** drive. No server secrets
are involved — the device shows a QR code on first boot and is claimed from the
dashboard.

> This is distinct from the internal [`../provision-and-flash.sh`](../provision-and-flash.sh),
> which mints device credentials from the server `MASTER_SECRET` and is for
> operators pre-provisioning fleet hardware. End users use **this** tool.

## Usage

```bash
# Download the latest Pi image and pick a removable drive interactively:
./puttv-flash.sh --type pi

# Flash a specific drive, x86 image:
./puttv-flash.sh -t x86 -d /dev/disk4        # macOS
./puttv-flash.sh -t x86 -d /dev/sdb          # Linux

# See what it would do, without writing anything:
./puttv-flash.sh -t pi --dry-run

# Use a local image file instead of downloading:
./puttv-flash.sh -i ./puttv-pi.wic.bz2 -d /dev/sdb

# Just list the removable drives it can see:
./puttv-flash.sh --list
```

Every flag has a short and long form; run `./puttv-flash.sh --help` for the full
list. If you omit `-d/--device`, it lists the removable drives and asks you to
pick one — so both scripted and interactive use are supported.

## Safety model

Writing a raw image is destructive, so refusal is the tool's main job:

- **System/boot disk is always refused** — it compares the target (normalised to
  its whole-disk node) against the disk the OS booted from.
- **Non-removable disks are refused** by default. USB SSDs sometimes report as
  non-removable; `--allow-nonremovable` overrides this, but only do that if you
  are certain.
- **Typed confirmation** — you must type the exact device node to proceed (skip
  with `-y/--yes` for automation).
- **`--dry-run`** performs every step except the write and prints the exact
  command it *would* run.
- **Size sanity check** — warns if the target is larger than a typical SD/USB
  stick (default 512 GB).
- **Least privilege** — the script runs as your normal user; only the unmount
  and the `dd` write use `sudo`.

⚠️ **BETA.** You are the last line of defence — always double-check the device
name. We strongly recommend trying it against a **virtual machine** disk before
using real hardware.

### Tests (TDD)

The safety logic lives in pure functions with the OS calls isolated behind
`probe_*` seams, so it is unit-tested with fakes — no real disks required:

```bash
./tests/run-tests.sh
```

The suite asserts the tool refuses the system disk (and its partitions),
refuses non-removable drives, refuses nonexistent nodes, accepts a genuine
removable drive, honours `--allow-nonremovable`, and lists only removable,
non-system candidates. Run it in CI before shipping a build.

## How it finds the image

Resolution order: `-i <path>` → `--image-url` → **a bundled image in `./images`**
next to the tool → the download manifest.

The simplest, offline path is the bundled one: drop a `.wic` / `.wic.bz2` into
[`images/`](images/) and run the tool — it uses the bundled image, so no path or
network is needed.

**Production UX:** the download page ships a bundle containing only the *one*
ROM for the device type the user selected. With a single bundled image, no flags
are needed at all — the user just runs `./puttv-flash.sh`, picks the drive, and
confirms. `--type` is only needed to disambiguate when more than one image is
bundled (as in this dev repo, which ships both):

```
images/apitv-image-raspberrypi0-wifi.rootfs.wic.bz2   # -t pi
images/apitv-image-genericx86-64.rootfs.wic.bz2        # -t x86  (symlink)
```

If no bundled image is found, `--type pi|x86` falls back to fetching a manifest:

```
GET {BASE_URL}/{type}/latest.json
{ "version": "2026.07.0", "url": "https://.../puttv-pi-2026.07.0.wic.bz2", "sha256": "…" }
```

`BASE_URL` defaults to `https://puttv.io/downloads` (override with
`--base-url` or `$PUTTV_BASE_URL`). The image is cached under
`~/.cache/puttv-flash` and the SHA-256 is verified after download.

**Publishing images (server side — TODO):** the download host needs to serve
`/{pi,x86}/latest.json` plus the referenced `.wic.bz2`. Until that exists, the
build box can hand off images directly (e.g. `scp` the `.wic.bz2` to the client
and run with `-i ./that-file.wic.bz2`), or point `--image-url` at any reachable
URL.

## Status / remaining work

- [x] Removable-only, system-disk-refusing target validation
- [x] Interactive + flag-driven drive selection
- [x] Dry-run, typed confirmation, beta warning, VM recommendation
- [x] Least-privilege (sudo only for unmount + write)
- [x] TDD safety suite (`tests/run-tests.sh`)
- [x] Versioned download via `latest.json` manifest + checksum verify
- [ ] Stand up the `downloads/{pi,x86}/latest.json` publishing endpoint
- [ ] Windows (PowerShell) port — see [`../provision-and-flash.ps1`](../provision-and-flash.ps1) for the flashing approach
- [ ] Optional: gate downloads behind login so only signed-up users can fetch
