Ctrl K
ComunidadGuías…

proposals/2026-07-27-local-library-alias.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# A local alias for a library

**Goal** - let you call a library whatever you want ON YOUR OWN PHONE, without
touching the server it came from and without needing to own that server.

**Tier** - **T2.** One new optional persisted field (`alias` on the `hosts.json`
record) and one new IPC method. No wire change, no host change: the alias never
leaves the phone.

---

## Why

A library is named by its HOST. `identity.get` carries `libraryName`, the app
stores it, and every rename path (`syncHostNames`, the `library-renamed` push,
`addHost` on a re-pair) overwrites the stored name with whatever the server just
said. That is correct for a library you run. It is useless for a library you do
not:

- **You cannot relabel a friend's library at all.** There is no path that sets a
  name the host did not push, so "My Library" stays "My Library" no matter how
  little that tells you.
- **The #suffix is a disambiguator, not a name.** PR #212 made two same-named
  libraries distinguishable - "My Library #jud4" / "My Library #rxtj" - because
  the desktop host ships with `--name "My Library"` and two friends running
  defaults gave you two identical rows in the switcher, the chips, Settings, the
  request list and the Manage picker. It works, and it means nothing to a human.
  `jud4` is the right four characters for a bug report and the wrong four for
  someone deciding which library to play from.

Tim agreed to this as the follow-up to #212. The suffix fixed "I cannot tell
these apart"; the alias fixes "I cannot tell what these ARE".

The shape is already half-built. `libraryLabels()` in `worklet/hosts.js` is the
single place the app decides what a library is called, `labelFor()` in
`src/bare.js` funnels every user-facing payload through it, and the comment on
`libraryLabels` already anticipates this: *"the stored libraryName stays exactly
what the host said, so a later rename (or an alias) has something honest to
compare against."*

---

## Scope

### In

**1. `alias` on the host record.** Optional, absent by default. `record()` carries
it when it is a non-empty trimmed string and omits the key otherwise, so a file
written before this change and a file written after it are the same shape for a
library with no alias. No migration, no version bump.

**2. `setAlias(raw, hostKey, alias)` in `worklet/hosts.js`.** Pure, unit-tested,
alongside `renameHost`. Trims, caps at 40 characters, and treats an empty or
whitespace-only string as CLEAR (delete the key, fall back to the host's name).
A missing hostKey is a no-op, matching `renameHost`.

**3. `libraryLabels()` prefers the alias, and the clash rule runs on the result.**
The effective name becomes `alias || libraryName || 'Library'`, and the tally that
decides whether to append `#jud4` counts EFFECTIVE names. So:

| Libraries | Labels |
| --- | --- |
| "My Library", "My Library" | `My Library #jud4`, `My Library #rxtj` (today) |
| alias "Sam's music", "My Library" | `Sam's music`, `My Library` |
| alias "Music", alias "Music" | `Music #jud4`, `Music #rxtj` |
| alias "My Library", host-named "My Library" | `My Library #jud4`, `My Library #rxtj` |

That last row is the reason the clash check must move behind the alias rather than
in front of it: an alias can COLLIDE with a name a host pushes, and the pair still
has to be tellable apart.

**4. `setLibraryAlias({ hostKey, alias })` in `src/bare.js`.** Saves via
`setAlias`, emits `host:renamed` with the new label, returns `listHostsData()`.
`listHostsData()` also starts returning `hostName` (the raw stored server name)
next to the label, so the UI can show both.

**5. The three `host:renamed` emits go through `labelFor()`.** Today
`src/bare.js:619`, `:1177` and `:2157` push the RAW server name to the UI. With an
alias set that is a live bug: an operator renaming their library would flash their
name over your alias in the header and the switcher until the next full reload.
Every emit sends the label instead. The STORED `libraryName` keeps tracking the
server exactly as it does now - that is what makes clearing an alias reveal the
server's CURRENT name rather than a stale one.

**6. Settings > Libraries gets a pencil.** Beside the existing trash button, it
opens a small text sheet prefilled with the current alias, placeholdered with the
host's name. Saving sets it; clearing the field reverts to the host's name. When
an alias is set, the row's sub-line says what the server calls it, so the mapping
is discoverable and a server-side rename is never invisible.

Everything else relabels for free. The header, the source-filter chips, the merged
status, the request rows, the You > Manage picker and the switcher all render
`labelFor()` output already.

### Out

- **No host change and no wire change.** The alias is never sent anywhere. The app
  has never sent a library name to a host (`host/server.js:244` is the operator
  renaming their own library from the dashboard) and this does not start.
- **It does not sync between your own devices.** Deliberate. Syncing it would mean
  storing it on the host, which (a) makes it host state rather than phone state,
  a much bigger T2, and (b) tells your friend's server the private name you gave
  their library. "Sam's crap music" is exactly the string that must not travel.
- **Not editable at pair time.** One place to set it (Settings) beats two for v1.
- **No per-library colour or icon.** Same axis, separate feature, no demand yet.
- **Removing a library forgets its alias.** A removal is a forget; re-adding gives
  you the host's name back and one tap to re-alias.

---

## Compat

- **A fresh field, absent by default.** An unaliased record round-trips
  byte-identically through `normalize` before and after, so an upgraded phone sees
  no change until it sets one.
- **Downgrade is lossy but safe.** `record()` is a whitelist, so an older build
  reading a new `hosts.json` silently drops `alias` and shows host names. Nothing
  throws, nothing wedges.
- **Old host, new app** is the only combination that exists here, and it is
  unaffected: the host is not in this feature.

---

## Risks

**An alias hides a server rename.** You call it "Sam's music", Sam renames it
"Sam's jazz", and you never learn. Mitigated by showing the server's own name on
the Settings row whenever an alias is set, and by clearing an alias always
revealing the CURRENT server name (we keep syncing `libraryName` underneath).
Accepted: the whole point of an alias is that it wins.

**Aliasing two libraries the same thing.** Handled by construction - the clash
check runs on effective names, so you get `#jud4` / `#rxtj` back. Slightly funny
(you asked for that name twice) but never ambiguous.

**Hostile input.** Trim and a 40-character cap in the pure layer. The alias is
rendered by React as text, never as HTML, and never reaches the dashboard, so the
stored-XSS class that bit the device labels in July has no path here. Worth
stating rather than assuming: this string is displayed on ONE device, by one
renderer, and is written by the person reading it.

**Scope creep into "rename my own library".** An owner CAN rename a library they
own, from the dashboard, and that is a different feature with a different blast
radius (it changes the name for everyone). The pencil sets a local alias for every
library including one you own, which is the simpler and more honest rule.

---

## Verify

Unit (`test/hosts.test.js`, extending the existing `libraryLabels` cases):

- `setAlias` sets, clears on empty/whitespace, caps at 40, no-ops on a missing host
- an alias survives `renameHost` and `addHost` (a re-pair must not wipe it)
- `libraryLabels` prefers the alias over the host name
- a lone alias is left completely alone (no suffix)
- two identical aliases clash, and an alias clashing with a host-pushed name
  suffixes BOTH
- `record()` omits the key when the alias is absent or blank

Gate: `npm run verify` green (564/564 today, plus the new cases), including
`test/worklet-refs.test.js`.

Hardware (TCL, both libraries paired, both currently named "My Library"):

1. Alias the Umbrel "Tim's Umbrel". Confirm the switcher, the Settings row, the
   home chips, the Library header and the You > Requests rows all say it, and that
   the OTHER library drops its `#rxtj` suffix (the clash is gone).
2. Rename the library on the dashboard while the alias is set. Confirm the alias
   holds in the UI and the Settings sub-line updates to the new server name.
3. Clear the alias. Confirm the row goes back to the server's CURRENT name and the
   `#suffix` pair returns.
4. Kill and relaunch the app. Confirm the alias persisted.