feat: name your machines from web settings (#1214)

Machines are labelled by hostname with no way to give them a friendlier
name. `MachineMetadataSchema` has declared `displayName` all along and the
whole read path already honours it (`displayName → host → id`), but nothing
could ever write it: the CLI never sends the field, the hub exposed no route
that sets it, and the web UI had no editor.

Add the missing write path:

- `PATCH /api/machines/:id` with `{ displayName }`, guarded by the existing
  `requireMachine`. An empty value removes the key so the label falls back to
  the hostname; the empty string is never stored.
- `machineCache.renameMachine` merges that one key into the stored metadata
  and lets `refreshMachine` publish `machine-updated`, which `useSSE` already
  invalidates on — so every connected client relabels without new plumbing.
- A `/settings/machines` page listing online machines with inline rename,
  placed between Voice and About so the existing preference pages keep their
  order. Each row keeps the hostname visible, so a renamed machine is still
  identifiable.

The merge reads the raw stored metadata rather than the cached `Machine`
view. That view is narrowed by `MachineMetadataSchema`, which strips unknown
keys and yields `null` for a row that fails validation — reachable, since the
CLI's `machine-update-metadata` handler accepts `z.unknown()`. Merging
against it would have written those fields out of existence.

The row's save is guarded by a ref rather than `isPending`: disabling the
focused input forces a blur, so Enter otherwise reaches `save` twice and
fires two PATCHes, the second of which can lose the version race and report
a failure for a rename that succeeded.

`mergeMachineMetadata` already preserves hub-side fields on CLI
re-registration, so a reconnect does not clobber the name.

Closes #1210
This commit is contained in:
Haoqing Wang
2026-07-29 10:04:33 +08:00
committed by GitHub
parent e32fe146c3
commit 8d1f84e20b
14 changed files with 750 additions and 0 deletions
+8
View File
@@ -613,6 +613,14 @@ export class ApiClient {
return await this.request<MachinesResponse>('/api/machines')
}
/** Pass an empty string to clear the custom name and fall back to the hostname. */
async renameMachine(machineId: string, displayName: string): Promise<void> {
await this.request(`/api/machines/${encodeURIComponent(machineId)}`, {
method: 'PATCH',
body: JSON.stringify({ displayName })
})
}
async listMachineDirectory(
machineId: string,
path: string