# pyre CLI changelog

All notable changes to the `pyre` command-line client. Versions follow
semver; the tag is `cli-v<version>`. Since 1.0.59 the CLI, the desktop
apps (`desktop-v<version>`), the API, the appliance image and the hubs
share one version number per release.

## 1.0.60 — 2026-10-16 (CLI, Linux and macOS apps, API, appliance image, hubs)

- Desktop apps: in-app updates. Account & engine shows whether a newer
  PyreData is published (checked once at launch and on "Check for
  updates"). macOS: "Download and install" fetches the installer
  package, checks it against the published checksum, opens it and
  quits so it can replace the app (the installer asks for the
  administrator password; nothing runs privileged inside PyreData).
  Linux: "Update now" runs the apt upgrade through PolicyKit and
  restarts the app. The sidebar shows "Update to <v>" when one waits.
  Agent methods update.check and update.download; the macOS manifest
  is /dl/desktop/macos/latest/latest.json.
- Desktop apps, Hubs → Promote: the confirmation carries "Accept a lag
  of (s)", pre-filled with the replica's current lag plus 60 s as on
  the web, and sends it as accept_lag_s. Before, the apps could only
  use the API's 300 s default, so a replica further behind a dead
  primary could not be promoted from the app at all.
- Desktop apps, Hubs: a promotion is now visible and followed. Promote
  shows "Promoting <hub>…" at once (banner on Linux, card on macOS),
  the page polls the promotion every 5 s and ends with "<hub> is now
  primary" or the failure detail, then reloads the rows. Before, the
  API accepted the detached promotion and the app showed nothing
  beyond a toast; the web dashboard already showed it.
- Linux app: the "Operator" label above the operator pages (Hubs &
  replication, Users & registrations, Export orders, Tokens) now shows;
  GTK dropped it the way it was set, so the pages looked like ordinary
  sections. The sidebar is also rebuilt when the signed-in account's
  operator right changes, instead of only at the first sign-in.
- Desktop apps: an iSCSI or NFS volume starts its appliance as soon as
  it is created (it is served to the network, not mounted here, but
  nothing is served until the appliance runs). A CHAP change on a
  volume whose appliance is stopped starts the appliance and applies
  the change, instead of answering "start it and try again".
- Desktop apps, iSCSI volume: turning CHAP off no longer asks for
  confirmation; the switch just turns it off and the note under it
  says the target is open.
- macOS app, New volume: "Add a store…" now opens the store form on top
  of the volume sheet. Cancel brings the volume form back as it was;
  Save selects the new store in "Where the blocks live". Before, the
  button closed the sheet and jumped to Storage, so a change of mind
  meant starting the volume over.
- macOS app: explanatory text under the create-volume sheet (the
  "Encrypt with a key only I hold" note, plan space, compression and
  upload-timing help), the engine checks and the agent error now wrap
  instead of being cut off with an ellipsis after one line.
- Desktop apps (Linux and macOS), Operator → Hubs: the first hub can now
  be set up from the app. Before, the "Add a hub" box asked the API to
  preflight a replica build, which is refused with "no primary" when
  nothing is serving, and the Linux app said to use the dashboard. Both
  apps now carry the web's two wizards step for step: **Set up the first
  hub** when nothing is serving (machine → key → DNS and UDP → build →
  registered, over `GET/POST /v1/admin/hubs/bootstrap` and the build's
  status route, with the known builds listed and forgettable) and **Add
  a hub** when a primary serves (key → DNS and UDP → build: host-key
  confirm, register, the primary builds it; `GET /v1/admin/hubs/onboard-key`,
  `scan`, `POST /v1/admin/hubs`, `POST …/{id}/build`). A site registered
  and not yet built shows "not built yet" with a Build button; a running
  build shows its step and phase on the row; a failed one its reason and
  "Build again". No API change.
- Agent: a POST or PUT whose only parameters are in the path (build,
  demote, promote, enable) now sends `{}` as its body instead of no
  body. The API's request models for those routes are all defaults,
  but the body must exist: a bodiless POST is 422 "Field required".
  Seen live 2026-10-15 when the desktop app registered hub-d and its
  build was refused three times.
- Desktop apps, Hubs: registering a hub no longer stops at a host-key
  dialog (the key is pinned as scanned and shown on the build step);
  Demote, Retire and the other danger buttons are bordered buttons at
  the row's height instead of red text; Retire acts at once, without
  a confirmation dialog.

## 1.0.59 — 2026-10-15 (CLI, Linux and macOS apps, API, appliance image, hubs)

- One version number for everything from here on. The CLI and the
  desktop apps no longer count separately: this release is 1.0.59 on
  every component. The standalone CLI skips 1.0.57 and 1.0.58 for that
  (a `cli-v1.0.57` of the same code was published earlier on
  2026-10-15 and is superseded by 1.0.59; nothing in it differs).
- Space reclaim (docs/TRIM-RECLAIM-DESIGN.md). Deleting files on a
  volume gave no space back: a Pyre volume is a block device in 4 MiB
  blocks and nothing in the chain told the store a block was free. Now,
  on by default: an iSCSI volume's target advertises UNMAP in 4 MiB units
  (the computer that mounted the disk trims it -- fstrim in a Linux
  guest, Optimize-Volume on Windows, discard=on on a Proxmox disk; after
  switching it on, rescan the disk or re-login), and an SMB/NFS volume's
  appliance trims its own filesystem on every delete (`discard`) and on
  demand. The engine frees whole blocks only: a partial trim is ignored
  instead of re-uploading the block (tenth carried s3backer patch,
  `s3backer-trim-whole-blocks.patch`). A freed block is a delete at the
  next upload, through the same consistent batches; the current size
  drops then, the stored total when history lets the old versions go.
  `pyre volume reclaim <volume> [--on|--off|--now]`, GET/PUT
  `/v1/volumes/{id}/reclaim`, POST `/v1/volumes/{id}/reclaim/now`,
  VolumeOut `reclaim`, `pyre volume create --no-reclaim`, the volume
  page's "Space reclaim" card with the end-user limits (which stacks
  pass discards and which do not), `pyre volume status` lines.
  Dispatcher verbs `reclaim-set` and `reclaim-now`; `volume-usage`
  reports `RECLAIM_*`.

- Hubs: replication passes come in two modes (docs/DR-REPLICATION-DESIGN.md
  section 22.4). A replica no longer walks every object over the wire
  each minute: it asks the primary what was written since its last pass
  (`changed-since`) and fetches exactly that; a full walk with deletions
  still runs hourly, on the first pass, after a failed pass and before a
  promotion. On the live pair a changed pass costs a few seconds of CPU
  where a walk took twelve, and a pass that copied a few hundred MB no
  longer takes ten minutes.
- The release gate checks space reclaim on all three protocols: after a
  delete, an SMB/NFS volume runs `reclaim --now`, the iSCSI client sees
  discard advertised and runs fstrim, and the store's current size must
  drop.
- Desktop apps (1.0.59): the volume page shows the Space reclaim row
  (mode, last reclaim, bytes handed back).

## 1.0.56 — 2026-10-14 (standalone CLI; API, appliance image, hubs; desktop apps as 1.0.58)

Write-back design items 4-6 (docs/WRITE-BACK-DESIGN.md):

- Consistent batches. The appliance uploads in batches: a batch is
  everything dirty when its first upload comes due, and no upload of the
  next batch starts until every upload of this one has completed, so the
  offsite copy is always an instant image of acknowledged writes
  (crash-consistent), never half of a burst. A block overwritten while
  its batch still owed it extends the batch (it ends later, still
  consistent). Each batch end is marked in the store
  (`.pyre-control/consistent`, at most one a minute); restore points
  carry `consistent: true` when a marker proves them and the dashboard's
  picker names them. The hubs' GC keeps the marker that proves each kept
  checkpoint and releases the rest (`test_history_gc_consistent.sh`).
  Ninth carried s3backer patch, `s3backer-writeback-batches.patch`;
  PYRE_WB_CONSISTENT=0 switches the batches off.
- Profiles. `pyre volume writeback <volume> [--profile vm|files|database|
  custom|default] [--quiet 30s --idle 10s --max-age 15m]`, `pyre volume
  create --writeback vm`, the volume page's "Upload timing" form, GET/PUT
  `/v1/volumes/{id}/writeback`; VolumeOut carries `writeback_profile`.
  The default is vm for iSCSI (30 s quiet, 10 s idle, 15 min max age) and
  files for SMB/NFS (15 s, 5 s, 10 min); database is 5 s, 2 s, 5 min. A
  current appliance applies a change within a second (engine control
  file `/run/pyre/writeback`, dispatcher verb `writeback-set`); the
  enrolment carries `volume.writeback` for the next start.
- Visibility. The engine writes `/run/pyre/writeback-status` each second;
  volume-usage reports it (`WB_*`), the status's `sync.writeback` carries
  the backlog in bytes, the age of the oldest unuploaded write (the live
  RPO), the measured upload rate, the catch-up time and the last
  consistent point; `pyre volume status` prints a Write-back line and the
  volume page's sync card prints "Waiting to upload ... the oldest waiting
  write is N old" or "Synced". The Sync line's own rate and time-to-go
  (dashboard, CLI, apps) come from the engine too when it reports: the
  store-reading byte delta beside it said "1 KB/s, 90 days left" while
  the engine said 20 s (live check, 2026-10-13); while the engine holds
  the upload no rate is claimed (`sync.held`).

- Start rebuilds a lost appliance on its own (2026-10-14). When the
  control plane still records an appliance for the volume on this kind of
  computer, it is not reporting, and this computer's engine holds neither
  its container nor its state (the Pyre VM deleted and recreated: both of
  Daniel's volumes), the apps' Start sends a rebuild instead of the plain
  deploy the control plane refuses ("already deployed on a MACOS
  computer") with a pointer to the dashboard's Rebuild page. An appliance
  reporting from another computer of that kind is still refused. The
  refusal says "not reporting" after a failed check instead of "last seen
  8 s ago" (the age of the failed check, about an appliance dead for an
  hour).
- No copy can be delivered for a volume in the customer's own object
  store (2026-10-14): PyreData holds none of its data, so neither a
  download link nor a shipped medium can be prepared. The control plane
  refused the request already; now the media options refuse too, the
  dashboard's "Get a copy" page (new and classic) replaces both forms
  with the reason and names the store, the Mac and Linux apps list such a
  volume as unselectable with the same sentence, the stores page and the
  landing page's Physical Export block state the rule, and
  docs/PHYSICAL-EXPORT.md tells the operator never to accept such an
  order by e-mail either. The customer exports it from their store with
  `pyre export prepare`.
- Hubs: rsync 24 (files vanished during the pass) is a complete data
  pass, not a failure; a finished promotion's result stays readable for
  15 minutes so `pyre admin hubs promote-status` sees it even when the
  Hubs page is open; a measurement refresh treats an appliance ssh
  timeout as a failed reading instead of an API exception.
- Mac app: the mount waits up to 60 s for the share's port after Start
  (the appliance reports online before its share listens); every call
  into the Pyre VM runs from `/` so a guest `cd` never enters the
  exported Mac home (a zombie SMB mount there after a VM stop wedged
  virtiofs and hung the app's list and mounts). Desktop apps ship as
  1.0.58 from this commit; the Mac 1.0.57 build of the same evening
  lacks these two.
- Hubs: one ssh control master per replication pass, never shared
  between the config and data tiers (`server/pyre-hub-dr.sh`,
  hand-deployed). The two timers fire in the same second, each tier is a
  oneshot service, and systemd kills a service's process group when it
  ends: the config tier's 40-second pass owned the shared master, and its
  end closed the data tier's rsync every time a data pull took longer
  than that. Found by the 1.0.56 gate's failback (345 passed, 1 failed):
  hub-a, a replica again after the planned failover, had hours of writes
  to pull over a home uplink and could never catch up.

## 1.0.55 — 2026-10-13 (Linux, macOS, standalone CLI; API and hubs)

From the release gate for 1.0.54 (361 passed, 4 failed):
- Restore points are listed LIVE by default again; only the desktop apps
  ask for the cached listing (cached=true). The 1.0.54 cache served the
  CLI and scripts a listing up to 15 minutes old: a point made a minute
  ago was missing (the gate's history phase, 3 failures).
- Hubs: with the "none" history policy, a GC run between a moment and its
  view refused the volume's PRESENT state as "no longer version-backed":
  the planner's boundary fell back to the time of the run when every kept
  instant was archived. It is now the newest current version, from which
  every block's version is its current one. (server/pyre-history-gc.sh and
  its appliance copy; test_history_gc_archive.sh.)

## 1.0.54 — 2026-10-13 (Linux and macOS; API and appliance with the next deploy)

Daniel's client issues list, 2026-10-13:

1. Slow switching between sections (all clients). The agent answered the
   requests on a connection ONE AT A TIME, and both apps keep one
   connection, so every call -- a section's loads, a switch -- waited
   behind whatever was in flight: a store listing (6 s), a refresh, a
   mount. Now each request runs on its own (bounded at 16 per
   connection; TestSlowCallDoesNotBlockTheConnection). The agent's HTTP
   client keeps 16 warm connections (HTTP/2 when offered): a new TLS
   connection to the control plane is ~0.46 s from the US West Coast. The
   Mac refresh runs its calls in parallel (was ~7 s one after another),
   and the Linux status refresh too.
2. Compression level not always updating in the GUI: fixed in 1.0.53
   (the row followed a cached volume list).
3. Mounts come back after a reboot. The agent remembers the volumes you
   mount (mounts.json; your own unmount, or deleting the volume, forgets
   them; seeded from today's mounts on first start) and mounts them
   again once the engine and each appliance are up.
4. Amazon S3 setup guide: /dashboard/stores/aws-guide (bucket type,
   namespace, Object Ownership, Block Public Access, versioning, default
   encryption, Bucket Key, Object Lock and default retention, lifecycle
   cautions, the two identities, registering), linked from the store
   pages and from Storage in both apps.
5. Time travel slow (~30 s): restore points are served from the last
   listing and re-listed behind it (fresh=true lists now); the Mac board
   shows the points it last saw at once and loads points, views and
   copies in parallel, and no longer reloads on every list refresh.
6/7. "New volume from this second" appeared at 0% and vanished. The
   appliance allowed copy names of 31 characters, the server 64 -- the
   default name testvol-202610120239-202610130550 is 33 -- and the clone
   failed at once; the server ignored the appliance's OPENED=no and kept
   a "restoring 0%" row that a later listing removed. Names are 64 on
   both sides now, and a refused clone is an error the app shows. A
   clone IS the instant restore by design; this one never started.
8. iSCSI usage: the volume status carries `usage` -- the filesystem's
   own figure for SMB/NFS, and for iSCSI (whose filesystem is the
   initiator's) the blocks written, shown as "≈ … written".
9. Time travel from a volume opened the first volume (Mac): the board
   missed the button's navigation; it now starts from the selected one.
10. "Open this second read-only" failed ("could not build a manifest") on
    a promoted copy: its blocks live under .recovery/<name>/, which the
    manifest builder refused as "not a single path segment". That prefix
    is accepted now (and only that one).
11. Restore points listed four times at the same second: writes a few
    milliseconds apart; points within one second are merged into the
    last of them (blocks summed, labels kept).

## Appliance and API — 2026-10-13, live compression (no client change)

- A saved compression level applies within a second, no restart
  (Daniel: "Why can't this be applied in flight?"). New s3backer patch
  appliance/s3backer-live-compression.patch: --compressControlFile, re-read
  at most once a second; every upload takes one snapshot of the algorithm
  and level, so its bytes and its Content-Encoding agree; "off" is refused
  on an encrypted volume. The entrypoint seeds /run/pyre/compress; the
  compression-set verb rewrites it and answers APPLIES=now; the API then
  reports it in use at once (compression_live_at) and no restart prompt.
  Tested against a real S3 API: zstd 1 -> zstd 19 -> off -> deflate 9 mid
  run, each block stored with its own encoding, all read back intact.

## 1.0.53 — 2026-10-13 (Linux and macOS; API and appliance with the next deploy)

- Compression shows what is really in use (Daniel, 2026-10-13: a level
  saved in the Mac app looked as if it had fallen back to 3). The saved
  setting applies at the appliance's next start; the appliance now reports
  what its running engine uses (COMPRESS_IN_USE in volume-usage), the API
  answers `in_use` and `restart_needed`, and every screen says "Saved
  zstd 19; the appliance still uses zstd 3 until it restarts".
- Mac and Linux apps: a Restart appliance button on that line
  (appliance.restart: ownership check, the mount detached, a patient stop
  and start, the mount put back). The Mac row follows a save at once; it
  redrew from the volume list cached before the save.
- Dashboard: the level box follows the algorithm (zstd 1-19, deflate 1-9,
  off has none) and states the range.

## 1.0.52 — 2026-10-13 (Linux and macOS; CLI, API and appliance with the next deploy)

- Upload compression per volume, everywhere (Daniel, 2026-10-13:
  "compression levels as well everywhere"). zstd 1-19 (default 3),
  deflate 1-9 (default 6), or off. Chosen when a volume is created and
  changeable later; a change applies to NEW blocks from the appliance's
  next start, and blocks already stored stay readable (each object
  records its encoding).
  - API: `GET/PUT /v1/volumes/{id}/compression`; `POST /v1/volumes`
    takes `compression` and `compression_level`; VolumeOut carries both
    (contract amended first).
  - CLI: `pyre volume compression <volume> [--set zstd|deflate|off]
    [--level N]`; `pyre volume create --compression --compression-level`;
    `pyre volume show` prints it, and History dedup.
  - Dashboard: New volume has the setting; the volume page has a Save
    form under History dedup.
  - macOS and Linux apps: New volume has it; the volume page has an
    "Upload compression" row with Save.
  - Savings, measured: the volume status carries `upload_compression`
    (blocks x block size against what the store holds for them: ratio,
    GiB, % saved), shown on the dashboard's volume page, in both apps and
    in `pyre volume show`.
  - Appliance: the enrolment carries the setting; every attach (live,
    resize, restore) reads it through s3_compress_setup unless
    PYRE_COMPRESS[_LEVEL] override; the dispatcher verb compression-set
    updates a running appliance's record.

## Appliance — 2026-10-13, smart write-back (no client change)

- Uploads no longer compete with the writes they protect
  (docs/WRITE-BACK-DESIGN.md, items 1-3). Stock s3backer pulled every
  upload forward as the dirty set grew, so a big write uploaded while it
  was still writing. Now a block uploads 15 s after its LAST write, and
  only once the volume has had no write for 5 s; a block older than
  15 min uploads regardless (one upload thread while busy); at 70 % of
  the dirty limit the holds drop. Measured in s3backer's test mode, 192
  blocks written over ~36 s: stock uploaded from the 3rd second on;
  smart uploaded nothing until the writes ended, then drained.
  New patch: appliance/s3backer-smart-writeback.patch. Settings:
  PYRE_SMART_WRITEBACK (0 = stock), PYRE_WB_QUIET_MS, PYRE_WB_IDLE_MS,
  PYRE_WB_MAX_AGE_MS, PYRE_WB_PRESSURE_PCT, PYRE_WB_BUSY_UPLOADERS.
- Flush means "get it offsite now": the volume-flush verb raises an
  upload-now flag (every due block, every thread) until the write cache
  holds nothing unuploaded.

## 1.0.51 — 2026-10-13 (Linux and macOS; API with the next deploy)

- History dedup on the volume page of both apps, as the dashboard shows
  it: the dedup and compression ratios, the overall ratio, and how much
  history is kept in how much disk; before the archive of old history
  has run, "nothing archived yet" and why. The API's volume status
  carries it as `dedup` (contract amended first).

## Appliance — 2026-10-13, third (no client change)

- After a resize the volume serves on its normal engine settings again.
  The resize's own attaches leave out local flush and dedup (both break
  the size-metadata step), and that transient instance used to keep
  serving until the next restart: a FLUSH then waited for the entire
  upload backlog. A Proxmox clone onto a just-grown iSCSI volume wrote
  10 GB into the cache, its final flush waited ~5 GB of upload, the
  initiator's timeout fired and the clone was rolled back. The resize
  now re-attaches with local flush, write delay, sync interval and dedup
  once the new size is durable (restore_live_engine).
- A cache that matched the volume grows with it on a grow (when the
  computer has the room), and the new size is recorded for later starts.
  A 10 -> 50 GB grow used to leave a 10 GB cache, so a large write
  filled it and then ran at uplink speed.

## Appliance and API — 2026-10-13, second (no client change)

- Resizing a never-written volume works. s3backer stores no object for
  an all-zero block 0, so the resize waited a minute for size metadata
  that could never appear and rolled back (a fresh iSCSI LUN's 10 -> 50 GB
  grow). The size then lives in the enrolment record and the control
  plane, which every start reads; the same grow took 17 s.
- A resize keeps the volume's CHAP choice: the rebind re-published an
  open target WITH CHAP, so the next Proxmox login would have failed.
- An iSCSI LUN keeps its identity: its serial (the NAA id Proxmox names
  the LUN by, and an LVM storage on it refers to) is derived from the
  target instead of drawn at random on every publish, so a resize or a
  restart no longer changes it.
- iSCSI notes: discovery works once an IPv4 portal is set; rescan after a
  resize; Proxmox needs an LVM storage on top to hold VM disks.

## Appliance and API — 2026-10-13 (no client change)

- iSCSI from another machine works through the appliance on a Mac or
  under Docker. Discovery used to advertise the container's bridge
  address (e.g. 172.17.0.4:3260): Proxmox discovered the target, recorded
  that address and refused the storage as "not online". With a portal set
  on the volume, the appliance now takes that address inside its own
  network namespace and redirects its iSCSI connections to it, so
  discovery advertises the portal (pyre-iscsi-advertise.sh). Applied at
  start, and live when the portal is changed (dispatcher verb
  iscsi-portal). Nothing on the host is changed. IPv4 portals only; a
  host name stays a record.
- CLI: the standalone CLI builds for Windows again (mount_other.go); the
  CLI package job had failed since the mount work.

## 1.0.50 — 2026-10-13 (Linux and macOS)

- Storage: adding a store can be abandoned. The add form now opens with
  **+ Add a store**, and **Cancel** (Esc on the Mac) closes it and clears
  what was typed, on the Mac and the Linux app. The web dashboard's form
  already had Back on both steps.

## 1.0.49 — 2026-10-13 (macOS published; Linux superseded by 1.0.50)

- macOS app: no more crash when the window is closed and reopened. Each
  reopen started a second agent connection on the same object; two
  readers shared one buffer and the app trapped in the reader
  (EXC_BREAKPOINT in RPC.readLoop, reported on 1.0.48). Connecting is now
  idempotent, and every connection has its own reader state.
- Start/Mount on a volume whose appliance was removed on this computer
  reinstalls it (a rebuild) instead of failing with "an appliance for X
  is already deployed on a MACOS computer". The agent does this only when
  the volume's state volume is still on this engine and the volume was
  last deployed on this kind of computer.

## 1.0.48 — 2026-10-12 (Linux and macOS; the label with the next deploy)

- An account sees and touches only its OWN appliances (Daniel,
  2026-10-12: "Operator should NOT see other users volumes or
  appliances"). A computer's engine is shared by every account that ran
  volumes on it; the apps listed every appliance on it. Every appliance
  is now labelled with its account at start (`--label
  io.pyredata.bucket=<bucket>` in the run block); the agent lists, offers
  as leftovers, starts, stops and removes only appliances it can prove
  belong to the signed-in account (the label, else the enrolment record
  inside a running one; unknown = not shown, not touched). A remove
  without that check stopped another account's appliance during testing
  on 2026-10-12 (its writes had reached the vault; restarted from its
  own account). Test: TestAnotherAccountsApplianceIsNotTouched.

## 1.0.47 — 2026-10-12 (macOS published; Linux superseded by 1.0.48)

- Your own storage is back in sight everywhere (Daniel, 2026-10-12: "I
  used to have an option to connect to external AWS, where did it go?").
  The dashboard lost its "your own S3 store" panel when it became wizards;
  the store pages were reachable only from a help sentence.
  - Dashboard: a **Storage** link in the navigation, and New volume says
    where the blocks live and links to registering a store.
  - macOS app: a **Storage** section (your stores with their state,
    features and warnings; test connection; add; remove) and "Where the
    blocks live" on New volume.
  - Linux app: Storage is its own section (it was under Advanced), and New
    volume can choose a store. Its add-store form sent the wrong field
    (`endpoint` for `endpoint_url`) and providers the API does not accept
    (`backblaze`, `s3`): adding a store from the Linux app could not work.
  - CLI unchanged: `pyre store list|add|probe|remove`, `pyre volume create
    --store <id>`.

## 1.0.46 — 2026-10-12 (macOS published; Linux superseded by 1.0.47)

- `pyre auth login` makes the profile it logged in to the current one.
  A default login left `current_profile` pointing at the desktop app's
  profile, so the next command said "not logged in" (Daniel, 2026-10-12).
- Signing out of the desktop app only revokes and clears the CLI profile
  it wrote for its OWN account, and removes it so the CLI stops pointing
  at it. It used to empty the default profile too -- including another
  account's login, such as an operator token saved with `pyre auth login`.

## 1.0.45 — 2026-10-12 (macOS published; Linux superseded by 1.0.46)

- macOS app: an **Operator** section for operator accounts (Daniel,
  2026-10-12: "Operator account doesn't show admin section for managing
  users and hubs"), as the Linux app has: Hubs (poll, rescan, promote,
  demote, retire with a typed confirmation, replication, add a hub with
  preflight and host key), Users and accounts (approve, reject, delete),
  API tokens (revoke, prune) and media exports (mark shipped, delete).

## 1.0.44 — 2026-10-12 (macOS published; Linux superseded by 1.0.45)

- Every volume shows whether its APPLIANCE runs, not only whether it is
  mounted (Daniel, 2026-10-12): "running here" (and "connecting" until the
  control plane hears it), "stopped here" with a Start button, "running on
  another computer", or "not running" -- this computer's engine first,
  then what the control plane hears. A column on the Mac's Volumes table,
  a line in the Linux list, and the volume section of both apps.

- Deleting a volume removes its appliance from this computer, in every
  client (Daniel, 2026-10-12: "this fix has to apply everywhere"). The
  apps only deleted the volume on the server: the appliance kept running
  and holding its ports, and the next volume given the freed slot failed
  ("Bind for 0.0.0.0:3260 failed: port is already allocated").
  - Mac and Linux apps: Delete = unmount, delete, then stop the appliance
    patiently (its last writes reach the vault) and remove the container
    (agent `volumes.remove`); its cached state stays for a restore.
  - CLI: `pyre volume delete` asks the PyreData desktop agent on the same
    computer to remove the appliance; elsewhere it prints the steps, as
    before.
  - Dashboard and API cannot reach a customer's computer: the apps now
    list appliances with no volume in this account (agent
    `appliance.orphans`) with a Remove button each, so a leftover from a
    dashboard, API or older-client delete is cleared where it runs.

## 1.0.43 — 2026-10-12 (Linux and macOS; standalone CLI waits for a full gate)

- iSCSI without CHAP, per volume (Daniel, 2026-10-12: "Need an option for
  iSCSI NOT to have CHAP or login"). A switch in both apps' iSCSI card
  (with a confirmation), a checkbox on the dashboard, `pyre volume iscsi
  <v> --no-chap` / `--chap`, and `PUT /v1/volumes/{id}/iscsi {"chap":
  false}`. Applied to the RUNNING target at once (dispatcher verb
  `iscsi-auth`; the appliance keeps the choice across restarts), and
  refused with 409 when the appliance cannot be reached, so the record
  never says open while the target is not. With CHAP off the details and
  lines carry no CHAP steps and say the volume is open to anyone who
  reaches the portal.
- iSCSI target names are shown lower-case, as the target advertises them
  (`...iscsitest`), and the notes say to install `open-iscsi` (and
  `multipath-tools`) first on Linux and Proxmox VE.

## 1.0.42 — 2026-10-12 (macOS published; superseded by 1.0.43 on Linux)

- macOS app shows its version, as the Linux app does: the About panel
  (app and agent), a line in the sidebar, the Account & engine card and
  the menu-bar popover.

## 1.0.41 — 2026-10-12 (macOS published; superseded by 1.0.42 on Linux)

- iSCSI details everywhere (Daniel, 2026-10-12: "I need them everywhere..
  API, CLI, WEB GUI and desktop app for Linux and Mac OS"). The control
  plane now issues each iSCSI volume's CHAP secret (as it does the SMB
  password) and stores a per-volume **portal**: the address other
  machines use to reach the target, e.g. the LAN address of the Mac or
  Linux machine running the appliance.
  - API: `GET /v1/volumes/{id}/iscsi` (target IQN, portal, port, LUN, CHAP
    user and secret, Linux and Windows initiator lines, notes) and `PUT`
    with `{"portal"}` (docs/CLI-API-CONTRACT.md).
  - CLI: `pyre volume iscsi <volume>`, `--portal <address>`,
    `--clear-portal`.
  - Dashboard: a "Connect over iSCSI" card on the volume page with the
    portal form.
  - Linux and macOS apps: the same card on the volume, with copy buttons,
    the portal field ("This Mac's address" on macOS) and the initiator
    lines. An appliance started before this release made its own secret:
    the apps read it from the appliance's log (`volumes.iscsi.local`),
    and a redeploy adopts the issued one.
  - Appliance: adopts the issued secret and prints the portal in its
    banner. The address LIO binds stays the container's own, so another
    machine logs in to the portal directly (the lines do) rather than
    through discovery.
- macOS: NFS volumes mount (Daniel, 2026-10-12: "I can't mount new NFS
  volume on MAC ... permission denied"). The app mounted SMB only; it now
  mounts NFS into ~/PyreData/<volume> as the user (`mount_nfs`, no root:
  the appliance does not insist on a privileged port), read-only views of
  NFS volumes too. A fresh volume's root directory belonged to root inside
  the appliance, so a mount could not be written to; the agent hands it
  to the Mac user through the appliance, as the Linux helper does with
  root rights. A root that already has an owner is left alone.
- The Linux app's volume section showed a guessed, wrong target IQN
  (lower-cased, without the bucket); it now shows the real one.

## 1.0.40 — 2026-10-12 (CLI, Linux and macOS from one commit)

- macOS: a second PyreData volume could not be mounted while another was
  ("mount_smbfs: server rejected the connection: Authentication error").
  macOS keeps one SMB session per server NAME, and two volumes reached
  as `127.0.0.1` with different passwords collided -- across accounts
  too, which the release gate found (its volume beside the owner's).
  Every Mac volume's server is now `<container>.localhost` (resolves to
  this Mac with no setup): the mount line, Finder's text and the app's
  own mount. A saved Finder mount of an older volume keeps working; the
  app remounts under the new name.
- `pyre update` / `pyre uninstall` no longer replace or remove the copy of
  `pyre` that ships inside the PyreData app (macOS) or the `pyredata`
  package (Linux): they say to update the app or the package instead.
  Updating in place had left a 1.0.37 Mac app running a 1.0.30 `pyre`
  with a broken bundle signature.

## 1.0.39 — 2026-10-12 (macOS and Linux published; CLI superseded by 1.0.40: no gate report)

- From this release the CLI, the Linux package and the macOS app carry
  the same version number and are cut from the same commit; `pyre
  update` and the Download page agree.

- Linux app: the volume section fetched the restore points on every
  re-render (every agent event and status tick), so the app asked the API
  200 times in ten minutes and the header spinner never stopped; now once
  a minute per volume. Time travel re-added its entry and timeline to
  fresh rows on each render, which GTK refused ("gtk_box_append:
  assertion ... parent == NULL") and the row vanished; they are detached
  first. Daniel, 2026-10-12, on the Linux client.

## 1.0.38 — 2026-10-12 (macOS published; Linux and CLI superseded by 1.0.39)

- Export of a copy of an encrypted volume opens. A promoted copy's blocks
  are encrypted under the SOURCE volume's salt (its original prefix); the
  hub packed the copy with the copy's own path as salt, so the key was
  "accepted" and block 0 then failed ("incorrect block signature",
  Daniel 2026-10-12). The API now tells the hub the volume's salt prefix
  (`export-start ... [label [prefix [salt-prefix]]]`, `pyre export
  prepare --salt-prefix`), and `pyre export open|extract --salt-prefix
  X/` opens an export made before this fix.

- Linux: the engine could not start on a clean machine: Lima finds its
  guest agents two directories above its own binary under `share/lima`,
  and the package had limactl at `/usr/libexec/pyredata/limactl`, so it
  looked in `/usr/libexec/share/lima` ("guest agent binary could not be
  found for Linux-x86_64"). Every earlier test passed only because the
  test machine had a system Lima. limactl now lives at
  `/usr/libexec/pyredata/bin/limactl`; the package test checks the
  guest agent is where Lima looks. Daniel, 2026-10-12: "limactl not
  starting on s_pyreclient with 1.0.35".

## 1.0.37 — 2026-10-12 (macOS published; Linux tag desktop-v1.0.37) -- NOT published for Linux (the layout above)

- Linux CI: the arm64 package is install-tested in Debian trixie only (the
  emulated Ubuntu pass alone took 40 minutes); amd64 still runs both.

## 1.0.36 — 2026-10-12 (macOS only)

- Get a copy works. Request copy did nothing on the Mac because no volume
  was pre-selected (Daniel, 2026-10-12), and both apps asked the API for
  delivery "download", which it does not know (`link` or `media`). The
  first volume is selected; Download requests a link; Ship it to me shows
  the priced media options for the chosen region (`export.media`), takes
  a postal address and places the order; Recent copies read the job's
  real fields (`state`, `link`, `expires_at`, `retrieval_note`,
  `key_note`, `tracking`, `error`).
- A copy made from an encrypted volume reads with the SOURCE volume's key.
  The API now says whose (`VolumeOut.key_volume`, docs/CLI-API-CONTRACT.md
  §10a) and both apps' Encryption section points at that key file
  (`~/.pyre/pyre-<source>.key`) instead of a file named after the copy
  that never existed; the appliance start block already mounted the
  right one. Daniel, 2026-10-12: "which encryption key should be used in
  this volume? The original one or ??"
- A promoted copy showed "appliance off" until Mount was clicked; the
  state now reads "not started · Mount to start" and the new volume is
  selected after promotion. Ready copies are also listed on the Volumes
  board (Mac) with "Make it a volume", and the toasts say where to look.

## 1.0.35 — unreleased

- Time travel works end to end in both apps. "Open this second read-only"
  sent `at` where the API wants `timestamp` ("timestamp: Field required"
  in the Mac app, Daniel 2026-10-12), and "New volume from this second"
  demanded an open view and then called promote with the wrong key. New
  agent methods `views.open` (open the view, wait for it, mount its share
  as `<volume>-view-<name>`; verified live on a Mac) and `views.close`
  (unmount + release); `mount.attach` takes `view` to mount an open view's
  share. A new volume from a moment now goes the real way: `restore.start`
  prepares a writable copy, listed under "New volumes in the making" with
  its copy progress, and "Make it a volume" promotes it (`new_name`); the
  view list reads the API's `open` / `point_in_time` keys.
- Resize puts the mount back. A grow or shrink unmounts the share inside
  the appliance for its whole duration, and no client came back with the
  new size: a Mac dropped the SMB mount, a Linux cifs mount kept the old
  size, an iSCSI initiator kept the old LUN size. New agent method
  `volumes.resize` (used by both apps' Resize): grow/shrink through the
  API, wait for the appliance to finish (phases shown as toasts), then
  unmount and mount again on this machine; for a grown iSCSI volume the
  new helper verb `iscsi-grow` rescans the session and grows the ext4 /
  xfs / btrfs filesystem online instead (polkit action
  `io.pyredata.mount.iscsi-grow`). Daniel, 2026-10-12: "On volume resize,
  shrink or grow, the current mount is not auto updated."
- Linux app: the boards now follow the Linux mockups (docs/design/mockups):
  GNOME greys with the teal accent, white cards, protocol pills, a
  property table per volume, the plan card in the sidebar, a drawn
  time-travel timeline, History policy as the six tiers with the named
  plans (Light / Standard / Extended), Get a copy and Account & engine as
  cards. New volume is opened from the + button instead of a sidebar entry.
- Linux app: History policy reads and writes the real six-number policy
  (`history.show` → `output`, `history.set` → `policy`); before, the page
  showed the raw field and Save sent a shape the API rejected.
- Linux app: restore points are read from the appliance's list
  (`restore_points.list` → `raw.recovery_points`); the volume section and
  Time travel count them and show the oldest.
- Linux app: API timestamps without a zone (`…T22:41:14.393733`) are
  parsed as UTC; token rows read "Created Oct 12 · Expires Jan 10, 2027".
- Linux app: the plan card never shows the tenant's e-mail as the plan
  name; a section already open keeps its detail page when the sidebar
  re-emits its selection.

## 1.0.34 — 2026-10-12 (macOS only)

- macOS app rebuilt in SwiftUI to the design boards: Solarized palette,
  sidebar with the plan meter, Volumes table with the selected volume's
  section (restore points, oldest point, local cache, waiting to upload,
  mount path, encryption key), dark Time travel board with a draggable
  log-scale timeline over the real restore points, History policy as the
  six tiers plus the Light/Standard/Extended plans (wire format
  "720,24,7,4,6,1"; the previous page expected a tier list that the API
  never sent), Get a copy, Account & engine, Advanced, a menu-bar
  popover, and the New volume sheet.
- macOS app: restore points are read from the appliance's JSON
  (`restore_points.list` returns it as a string); API timestamps without
  a zone parse as UTC; boards refresh every 15 s; the New volume sheet
  no longer crashes when the plan figure has not arrived yet.


## 1.0.33 — 2026-10-12

- Agent: the control plane's run block and limactl get the bundled
  limactl's directory on their PATH. A login agent has the bare system
  PATH, and the package keeps limactl out of it, so "limactl: command not
  found" ended every deploy on a clean install (found on a Mac; Linux was
  saved only by a system Lima on the test machine). 1.0.32 was never
  published.

## 1.0.32 — 2026-10-12

- Desktop package: the arm64 build is part of the signed release (1.0.31's
  CI job still built amd64 only).
- macOS desktop client, first build: PyreData.app + installer for Apple
  silicon (`desktop/macos/`), signed with the Developer ID.

## 1.0.31 — 2026-10-11

- **Desktop package for arm64** (Apple-silicon virtual machines, Ampere,
  Raspberry Pi 5) beside amd64; the apt repository serves both, so
  `apt install pyredata` picks the right one.
- The app opens `pyredata://enroll?code=...` links and takes
  `pyredata --enroll <code|link>`: First run fills the code in and signs in.
- The app carries its own symbolic icons, so it renders the same under
  every desktop theme (XFCE's Tango had none of them).
- Inside an ssh session the app defaults to no accessibility bus, the
  cairo renderer and X11 (the first window took minutes over `ssh -Y`).
- Package: postinst reloads each user's systemd manager before starting
  the agent.

## 1.0.30 — 2026-10-11

- **Desktop client for Linux (package `pyredata`, Debian 13 / Ubuntu 24.04).**
  One package carries the GTK 4 + libadwaita app, the per-user agent
  `pyre-agent`, the polkit-guarded `pyre-mount-helper`, the engine runtime
  (limactl) and this CLI; it provides, conflicts with and replaces `pyre`.
  The app enrols the computer with a one-time code from the Download page,
  mounts SMB, NFS and iSCSI volumes under `/mnt/pyre/<volume>`, offers time
  travel, history policy, copies, the engine and the account, and lists
  every API operation under Advanced. `docs/DESKTOP-CLIENT.md`.
- `pyre auth enroll <code|link>`: sign a computer in with a one-time
  enrolment code (or the `pyredata://enroll` link); the token is saved as
  with `auth login`. `pyre auth enroll-code [--label]` mints one (24 h,
  single use) for another computer.
- `pyre auth logout --expire`: the one deliberate way to revoke this
  computer's own token. `pyre auth token revoke` refuses the token in use
  (exit 2) and names that command; the API answers 409 unless `?self=1`.

- One appliance runtime on every OS: the Pyre VM. `pyre appliance deploy`
  for macOS and Windows now prints the Pyre VM install step (Lima from
  pyredata.io; Apple's virtualisation framework on macOS, QEMU with the
  Windows Hypervisor Platform on Windows; the VM definition for that OS;
  the launch agent or logon task) and a run block that goes through
  `limactl shell pyre docker`, exactly as Linux has since 1.0.x. Docker
  Desktop is no longer installed, required or mentioned. `remove` and
  `cleanup` print the VM steps for every OS. The Windows path is
  unverified on a Windows machine at the time of writing (the testbed has
  none); macOS is verified on Apple silicon.

## 1.0.29 — 2026-10-11

- One unit, GiB. Purchases were always GiB and the dashboard always sent
  whole GiB while every label said "GB" and the capacity refusal printed
  raw bytes. `pyre volume create --size 100GB` now rounds UP to the next
  whole GiB and says so ("creating 94GiB (100.9 GB)"); the API refuses a
  create that is not a whole GiB (422 naming the two nearest), as grow
  and shrink already did; every capacity message says GiB with one
  decimal. New 409 code `hub_capacity`: a volume (or a grow) larger than
  the serving hub's free space is refused even when the purchase allows
  it -- the hub-capacity ceiling the fleet never had.

- The first hub registers itself. `pyre admin hubs bootstrap --address
  <host> --start --name <name>` builds the machine and, when the build
  finishes, the control plane registers it as the primary under that
  name (the address's first label without `--name`); `bootstrap-status`
  says so, and `hubs add` is no longer a step of the first-hub process
  (adding a machine that is registered already is refused by address,
  not only by name). A hub that reports itself primary while the
  registry has none is adopted at the next poll instead of flagged as
  drift.

## 1.0.28 — 2026-10-10

- `pyre restore <volume> <when> --as <name>` is writable in seconds. The
  recovery reads the blocks it has not written through the volume's
  history at that moment and copies the rest in behind you; `pyre
  recovery list` shows how much is its own ("own data", 0-99% then
  "all"), and `promote` waits for "all". Needs the appliance image from
  the same release; an older appliance keeps copying first.
- `pyre admin hubs list` says "ok, replication STALLED" in the health
  column when a replica's passes are being refused (the delete guard
  after a big deletion on the primary); `hubs replication` has the
  detail and `accept-deletions` the remedy.

## 1.0.27 — 2026-09-29

- `pyre export download <link>`: fetches every part of an export from
  its temporary download link (https://api.pyredata.io/dl/...), resumes
  a broken download, keeps a part that is already there and intact,
  checks every file against its SHA-256, and prints what to run next.
  No account, token or network beyond the link.
- `pyre volume export <volume>`: asks the hub for a copy of the volume
  behind such a link (48 hours; `--status`, `--remove`, `--email`).
  Works for a deleted volume whose data is still stored. The link is
  on the hub that holds the files (https://<hub>/x/<token>/).
- `pyre volume export <volume> --medium list|<key> --region --address`:
  the same copy shipped on a BD-R XL disc, an LTO-9 tape, an SSD or a
  hard disk, priced (media, work, shipping) and ordered from the CLI.
- `pyre export show|verify|extract|open` read zip parts (what a
  download is made of) as they are, in place, next to directories on
  media; a folder holding either kind is scanned.
- `pyre export prepare --archive`: each part as one stored zip file.
  The help now names the tenant's store credentials on a converged
  hub (`/etc/pyre/tenants/<bucket>.env`), not `/etc/default/minio`.
- iSCSI volumes: `extract` refuses (the filesystem is the customer's)
  and points at `open`; `open --vhd` appends a fixed-VHD footer for
  Windows Disk Management; the README and `open` print how to attach
  the image on Windows, macOS and Linux.
- `pyre admin exports list|create|remove|ship|setup-hub`: every export
  job, one for any tenant (a preserved account, a media order), the
  shipping of a media order (tracking number emailed to the customer),
  and a hub's HTTPS download endpoint.

## 1.0.26 — 2026-09-28

- `pyre appliance deploy`: a volume whose appliance is already deployed
  is refused with one sentence -- which computer, whether it is online,
  `pyre appliance remove <volume>` to move it, `--rebuild` if that
  computer is gone -- instead of a run block that could only end in
  docker's "Conflict. The container name ... is already in use". The
  run block itself (every OS and protocol) looks for the container
  before docker run and says the same. The Linux install step warns
  when two Pyre VM host agents are running (a mount then reaches an
  appliance from an earlier setup).
- `pyre admin hubs bootstrap` / `onboard-key`: the paste block makes ssh
  keys-only (root and password login off); help text says so.

## 1.0.25 — 2026-09-28

- `pyre appliance mount` / `remove`: the commands are for the OS the
  appliance was deployed on (recorded at deploy), not the OS running
  pyre; `--os` still overrides, and the first line says which OS and
  why. From a Mac, an NFS appliance on a Linux client had printed the
  macOS block. The `mount --help` example no longer shows a literal
  `\n`.
- Installer (`install.sh`): installs into `~/.local/bin` by default, no
  sudo and no password; `pyre update` and `pyre uninstall` then need
  none either, the same per-user model as the Windows installer.
  `PYRE_INSTALL_DIR=/usr/local/bin` (or a root shell) keeps the
  system-wide install. An older copy in `/usr/local/bin` is pointed out
  at the end of the install so it cannot shadow the new one unnoticed.

## 1.0.24 — 2026-09-27

- `pyre admin hubs list`: a SERVING column -- for the primary, how many of
  its WireGuard peers shook hands in the last 3 minutes over how many it
  has (`recent/peers`); `0/N NOBODY` plus a notice naming inbound UDP
  51820 when a primary serves nobody. A primary whose firewall dropped
  every handshake had looked healthy for 14 minutes.
- `pyre admin hubs poll <hub>`: prints the health detail (role, tenants,
  tunnel identity, and for the primary whom it serves).
- Gate: run 23, every check of the release gate against the live fleet.

## 1.0.23 — 2026-09-26

- `pyre admin hubs replication`: a table (name, role, lag s, measured,
  pending, speed, eta, data, last pass). The lag is the worse of the
  measured frontier delta and the time since the last successful pass,
  as the hub's own promote guard decides; a replica whose passes the hub
  refused shows `FAILED: delete guard ...` with the count.
- `pyre admin hubs accept-deletions <hub>`: let a replica's next data
  pass mirror a primary that shrank (the delete guard at 100% for one
  pass), the answer to a refused pass without a shell on the hub.
- `pyre view open`: exits 1 with the appliance's reason when the instant
  is no longer version-backed, naming the nearest archived instants.
- `pyre volume grow/shrink`: whole GiB, and the nearest sizes named when
  not; sizes need a unit and are at least 1 GB on create too.
- `pyre view list` and `pyre recovery list`: every column filled.
- Every API call errors out after 30 s with the status and the next
  step (`build-status`, `promote-status`, `volume status`, ...).
- Behind the CLI, found by the release gate and fixed in the appliance,
  the hubs and the control plane: a resize no longer restarts the SMB
  service (a reconnected Linux client could not write afterwards) and
  closes client connections again after the remount; a promoted
  recovery attaches at the copy's real size; a released NFS view drops
  its export block; a dead iSCSI target from a killed container is
  swept before the next appliance's; a refused replication pass is
  visible on the dashboard and in the CLI; the onboarding preflight no
  longer deletes a built hub's tunnel firewall rule, answers its probe
  only to real probes, runs in a fifth of the time, and a refused build
  leaves no stale "done"; an account from before a fleet rebuild can
  be deleted; a hub removed from the control plane has its ssh master
  closed.
- Gate: run 22, 370 checks against the live fleet, every CLI call under
  30 s.

## 1.0.22 — 2026-09-26

- The release gate (`cli/e2e/release-gate.sh`): one run of the whole
  product as a customer and an operator meet it, on the real fleet;
  `cli-publish` refuses a tag without its green report. First gated
  release: 242 checks.
- `pyre appliance status` converges after a deploy; views and
  recoveries list what the API sends; recovery listings reconcile
  behind the response; a demote starts the first replication pass at
  once.

## 1.0.21 — 2026-09-25

- `pyre appliance deploy`: asks which OS and prints that OS's install
  step (the Pyre VM on Linux) before the run block.

## 1.0.20 — 2026-09-25

- `pyre admin`: hidden from a customer's help and completion; a
  customer's token gets a plain refusal naming it.

## 1.0.19 — 2026-09-25

- `pyre admin hubs build`: the primary preflights the machine first and
  refuses with the FAIL lines; the CLI prints its own next step.

## 1.0.18 — 2026-09-25

- Admin CLI sanity run (`cli/smoke/admin-hubs.sh`): the real API and CLI
  through the whole hub lifecycle against a fake fleet, in CI.

## 1.0.17 — 2026-09-25

- `pyre admin hubs onboard-key`: prints the block the new machine must
  run (sudo, the primary's onboarding key, the management key, root
  login off), not the bare key.

## 1.0.16 — 2026-09-25

- `pyre admin hubs remove`: sends only the flags that are set (an empty
  `force=` was a 422).

## 1.0.15 — 2026-09-25

- `pyre admin hubs bootstrap`: the first hub of a fleet can be built
  from the CLI (`--start`, `bootstrap-status`, `--rebuild`).

## 1.0.14 — 2026-09-25

- `pyre export`: the physical export. `prepare` (operator, on the hub)
  copies a volume's blocks off the store into per-medium directories
  with an index, README and SHA256SUMS; an encrypted volume leaves as
  the appliance's own ciphertext, a volume that was never encrypted is
  sealed to a public key from `pyre export keygen`, so nobody who
  prepares or carries the media can read it. `show` and `verify` need
  no key; `extract` copies the files out of the media on Linux, macOS
  and Windows with no mount, no image and no Docker (a built-in
  read-only ext4 reader that handles sparse files); `open` writes the
  raw disk image. A wrong key is refused by fingerprint before a block
  is read. Format and runbook: docs/PHYSICAL-EXPORT.md.

## 1.0.13 — 2026-09-24

- `pyre volume rediscover` reports volumes whose deleted row was just
  marked encrypted: a volume older than the storage's own record is
  known to be encrypted by the key-check object beside its blocks, so
  its row now says so and deploy asks for the key file.

## 1.0.12 — 2026-09-24

- `pyre volume rediscover`: your storage now carries its own record of
  every volume (written at create, resize and every appliance attach),
  and this lists any volume it describes that the account has no
  record of -- storage attached with a recovery key from before
  records were kept, a volume lost with a database -- as a deleted
  volume, ready to restore. `pyre account reclaim` runs it by itself
  and names what it found. A volume older than that record is
  described from its first block, type unknown: listed as smb, and
  `pyre volume deleted restore <id> --type nfs|iscsi` says what it was.
- `pyre volume deleted list` shows each volume's type and whether it is
  encrypted (which rows need their key file).

## 1.0.11 — 2026-09-24

- `pyre account close --mode preserve` keeps every volume as a deleted
  volume (the control plane change of the same day) and names them;
  `pyre account reclaim` then says how many came back and points at
  `pyre volume deleted list` / `deleted restore <id>` /
  `appliance deploy <volume> --rebuild`, instead of "create a volume".
  A tenant preserved before 2026-09-24 has no volume records; the
  message then explains the recreate-by-name path.

## 1.0.10 — 2026-09-24

- `pyre account reclaim --key <recovery key>`: attach the storage of an
  account closed with `account close --mode preserve` to the account
  this token belongs to. That account must have nothing of its own yet
  (no storage, volumes, stores or appliances); the key is accepted in
  any spelling (case, dashes, spaces) and is spent on success, never
  by a refusal. Until now the key could only be entered on the
  password sign-up form, which a "Sign in with Google" account never
  sees; the dashboard has the same page under "Recover it with your
  key".

## 1.0.9 — 2026-09-23

- `pyre volume deleted restore` now says what comes next: `pyre
  appliance deploy <volume> --rebuild`. Deleting a volume revokes its
  appliance's identity, so a container started again from its old
  state comes up unable to connect; the rebuild form's fresh token
  re-enrols it and the state volume and key file can stay.

## 1.0.8 — 2026-09-23

- After `pyre volume delete` of a volume that had an appliance, the CLI
  says how to clear it from the computer (`pyre appliance cleanup
  --names <volume>`), as the dashboard's redirect to /cleanup does.
- A command given the name of a DELETED volume (`appliance remove`,
  `appliance disconnect`, `volume show`, ...) now says so: when it was
  deleted, the cleanup line for its appliance, and the `deleted restore`
  line that brings it back. Before, "no volume named" left the customer
  hunting for a way to remove a stale appliance.
- `pyre appliance cleanup` with no `--names` takes every deleted volume
  of the account (logged in). Logged out it still needs `--names`.

## 1.0.7 — 2026-09-23

- `pyre volume create --encryption customer` works and is documented
  (the 1.0.0–1.0.6 example said `customer-key`, which the API refuses;
  it is now accepted as an alias of `customer`, as are `none`/`off`).
  Any other value is refused with exit 2 and the two choices, before
  anything is sent. After creating an encrypted volume the CLI says
  where the key file will be made (`pyre appliance deploy`,
  `~/.pyre/<container>.key`) and that losing it makes the volume
  unrecoverable.
- Every input is checked against the API's own rule before a request
  is built: ids in paths are numeric; volume, recovery and view names,
  labels, policies, points in time, `--since`, `--type` (now also
  `nfs+smb`), `--os`, `--cache-mb`, `--store`, store fields, emails
  and URLs (`--api`, `api_base`, `download_base`, `--endpoint`) are
  validated; a bad value is a usage error (exit 2) naming the choices.
  Validated path segments are additionally percent-escaped.
- `pyre store probe|add`: the provider list is the API's (`aws`,
  `wasabi`, `minio`, `weka`, `other`; default `other`). The old
  default `s3` and the documented `backblaze` were refused by the
  server on every call.
- Exit codes: cobra's own parse errors (`unknown shorthand flag`,
  `flag needs an argument`) are usage errors (exit 2, was 1); a stray
  word after a group command (`pyre volume nosuch`) is a usage error
  (was help with exit 0).
- A server-side validation answer (HTTP 422) is rendered as
  `field: message`, not as a JSON dump.
- Tests: the customer command matrix (`internal/cmd/permutations_test.go`,
  over 220 rows: valid, invalid and injection cases against a fake control
  plane) and new smoke checks against the real API; design and
  pass/fail criteria in `docs/CLI-TESTING.md`.

## 1.0.6 — 2026-09-23

- Encryption is shown as "customer key" or "none" from the API's
  `encryption` field (server section 10); a server that does not send
  the field shows "unknown" instead of a false "none". Also fills the
  `pyre volume list` ENCRYPTION column.

## 1.0.5 — 2026-09-23

- `pyre update` says which file it replaces ("replacing /path/pyre"),
  checks that directory is writable BEFORE downloading and names it in
  the error (with `sudo pyre update` only when that path needs it), and
  after updating lists any other `pyre` on PATH it did not touch.
  `pyre version` shows the binary's path and any others on PATH.
- Sizes match the dashboard: decimal GB with one decimal
  (26843545600 bytes → 26.8 GB); appliance measurements that arrive
  as strings are formatted too (no more raw byte counts).
- `volume show` / `volume status`: the Offline line appears only while
  the appliance is offline; buffer fill and disk-low facts sit on the
  Sync line otherwise, and an empty buffer is not mentioned.

## 1.0.4 — 2026-09-23

- `pyre volume show` is readable: one line each for Volume, Appliance,
  Sync, Offline, Storage, Errors and Measured, sizes in the dashboard's
  units, times local. Nothing in table mode prints raw JSON any more
  (nested data becomes dotted lines); `-o json` is unchanged, `-o plain`
  flattens to `key<TAB>value`.
- New `pyre volume status <volume>`: the live block alone, with
  `--watch [--every 5s]` redrawing until Ctrl-C on macOS, Linux and
  Windows (no watch(1) needed).
- `pyre appliance status` uses the same rendering.
- `pyre completion` without a shell now says
  `pyre completion <bash|zsh|fish|powershell> — e.g. pyre completion zsh`.
- `pyre update` rewrites the completion files it finds (bash, zsh,
  fish, PowerShell) with the new binary, clears stale `~/.zcompdump*`,
  and says when zsh completion is installed but `~/.zshrc` does not
  load it.

## 1.0.3 — 2026-09-23

- Homebrew: the generated formula carries a top-level `url` so current
  Homebrew accepts the tap (it validates every platform, Linux arm64
  included, and refused 1.0.2's formula with "requires at least a URL").
  No change to the binary.

## 1.0.2 — 2026-09-23

(1.0.1 was tagged on the same day but never published: its installer
still had the PATH gap below. Everything listed here is new since 1.0.0.)

- install.sh: installs into /usr/local/bin through sudo (the password
  prompt works under `curl | sh`); the ~/.local/bin fallback now adds
  itself to PATH and the zsh completion dir to fpath in your shell rc,
  and ends with "Open a new terminal (or run: source ~/.zshrc)". Found
  by the first real install on a Mac: `pyre` was not on PATH.

- Token lifecycle: `pyre auth token create --expires 30d|1y|never`
  (server default 90 days), `list` shows state and expiry, `remove`
  deletes a revoked or expired row (`--force` revokes first), `prune`
  drops every such row. Operators: `pyre admin tokens
  list|revoke|remove|prune` across tenants.
- `pyre uninstall` now removes every man page it installed
  (`pyre-<command>.1.gz`), not only `pyre.1.gz`.
- Install docs: `brew trust pyredata/pyre` before `brew install pyre`
  (current Homebrew refuses formulae from an untrusted tap).

## 1.0.0 — 2026-09-23

First packaged release, a rewrite in Go of the 0.1 Python prototype.

- One static binary per platform: macOS (Apple Silicon and Intel),
  Linux x86-64, Windows x86-64.
- Install channels: apt repository (signed), Homebrew tap, `curl | sh`,
  PowerShell one-liner, Scoop manifest, plain archives.
- `pyre update` (signed releases, in-place replace; defers to apt /
  Homebrew / Scoop when they installed it) and `pyre uninstall`.
- Shell completion for bash, zsh, fish and PowerShell; man pages.
- Scripting: `-o json` / `-o plain`, `--yes`, stable exit codes,
  `pyre batch` to run a file of commands with one login.
- Profiles for more than one control plane (`--profile`, `pyre config`),
  `--ca-cert` / `--insecure` for self-hosted managers, every host a
  config value.
- Customer commands: auth (token per user), volume (incl. deleted
  volumes), restore-points, restore, recovery, view, history, snapshot,
  appliance (deploy/mount/remove/status/disconnect/flush/cleanup),
  store (bring your own S3), account.
- Operator commands (`pyre admin`): hubs (scan/add/preflight/build/
  promote/demote/remove/bootstrap/...), users (approve/reject/delete),
  tenants.
