feat(servers): scan actions + edit existing server profiles (#780)

* feat(server-scan): plumbing for triggering library scans

Adds `startScan` / `getScanStatus` against the Subsonic API
(`fullScan=true` is Navidrome's extension), a small per-server scan
store, and a global polling hook (2 s cadence) that emits a toast when
each scan finishes. Scans can run on any configured server, including
inactive ones, by reusing `apiForServer`.

UI surfaces follow in the next commit.

* feat(server-scan): expose Quick / Full Scan in switcher + settings cards

Adds a `ServerScanActions` component with two variants (compact for the
server-switcher dropdown, card for the Settings server cards) backed by
the scan store from the previous commit. Full Scan requires a second
click within 3 s to confirm, matching the playlist-delete pattern.
Status slot shows a spinner with running track count while scanning, a
green check when finished, and a red icon on error.

The switcher row is converted from a single button to a flex container
so per-server scan controls don't hijack the server-switch click.
i18n added across all 9 locales.

* fix(server-scan): reorder switcher row to check / name / scan actions

Moves the check / spinner slot from the right edge to the left so the
spinner pop-in on server switch doesn't sit next to the scan icons.
Removes the layout shift that briefly hovered the Quick scan button
when the row re-rendered.

* feat(servers): edit existing server profiles in Settings → Servers

* Pencil-button on each server card opens an inline edit form that
  replaces the card (prefilled name / URL / username / password).
* `AddServerForm` reused with an `editingServer` prop — title flips to
  "Edit Server", submit label to "Save", magic-string field hidden (the
  edit scope is manual fields; magic-string remains an add-time invite
  shortcut).
* Edit saves unconditionally — ping runs post-save as a status indicator
  (analog to the existing Test button) instead of gating the save. Lets
  users update a profile when the server is currently unreachable.
* Translations across all 9 locales (`editServer`, `editServerTitle`).

* fix(servers): submit Add/Edit Server form on Enter

Wrapped the form body in a real <form>, made the submit button
type="submit", marked Cancel as type="button" so Enter no longer
cancels. Add-Mode now also responds to Enter — same flow, consistent
across both modes.

* fix(servers): collapse card action buttons to icon-only on narrow screens

* Quick-Scan / Full-Scan / Test buttons in each server card hide their
  text label below 1100px viewport via the .server-card-btn-label class
  and a single media query in connection-indicator.css.
* Labels remain accessible via data-tooltip and aria-label so screen
  readers + hover both keep working in the collapsed state.
* No content reflow above the breakpoint — pure additive CSS.

* fix(servers): include Use button in icon-only narrow-screen collapse

The Use ("Verwenden") button on inactive server cards lacked the
.server-card-btn-label wrapper, so its text stayed visible at narrow
viewports and pushed Edit/Delete off-screen. Added a Power icon and
wrapped the label so it collapses alongside the other action buttons.

* docs(changelog,credits): #780 server scan + edit
This commit is contained in:
Frank Stellmacher
2026-05-18 16:32:40 +02:00
committed by GitHub
parent 562218f447
commit bca45d5a80
20 changed files with 592 additions and 37 deletions
+14
View File
@@ -22,6 +22,8 @@ export const settings = {
serverPassword: 'Passwort',
addServer: 'Server hinzufügen',
addServerTitle: 'Neuen Server hinzufügen',
editServer: 'Bearbeiten',
editServerTitle: 'Server bearbeiten',
useServer: 'Verwenden',
deleteServer: 'Löschen',
noServers: 'Keine Server gespeichert.',
@@ -461,4 +463,16 @@ export const settings = {
floatingPlayerBarSub: 'Player-Leiste über dem Inhalt schweben lassen',
uiScaleTitle: 'Interface-Skalierung',
uiScaleLabel: 'Zoom',
scan: {
quick: 'Quick-Scan',
full: 'Full-Scan',
quickTip: 'Quick-Scan — nur neue/geänderte Dateien indexieren',
fullTip: 'Full-Scan — alles neu indexieren',
confirmFull: 'Nochmal klicken zum Bestätigen',
confirmFullShort: 'Full-Scan bestätigen',
scanning: 'Scannt…',
done: 'Scan fertig',
error: 'Scan fehlgeschlagen',
toast: '{{name}}: Scan fertig — {{count}} Tracks indexiert',
},
};
+14
View File
@@ -22,6 +22,8 @@ export const settings = {
serverPassword: 'Password',
addServer: 'Add Server',
addServerTitle: 'Add New Server',
editServer: 'Edit',
editServerTitle: 'Edit Server',
useServer: 'Use',
deleteServer: 'Delete',
noServers: 'No servers saved.',
@@ -464,4 +466,16 @@ export const settings = {
floatingPlayerBarSub: 'Keep the player bar floating above content',
uiScaleTitle: 'Interface Scale',
uiScaleLabel: 'Zoom',
scan: {
quick: 'Quick scan',
full: 'Full scan',
quickTip: 'Quick scan — index new/changed files',
fullTip: 'Full scan — re-index everything',
confirmFull: 'Click again to confirm full scan',
confirmFullShort: 'Confirm full scan',
scanning: 'Scanning…',
done: 'Scan finished',
error: 'Scan failed',
toast: '{{name}}: scan finished — {{count}} tracks indexed',
},
};
+14
View File
@@ -22,6 +22,8 @@ export const settings = {
serverPassword: 'Contraseña',
addServer: 'Agregar Servidor',
addServerTitle: 'Agregar Nuevo Servidor',
editServer: 'Editar',
editServerTitle: 'Editar Servidor',
useServer: 'Usar',
deleteServer: 'Eliminar',
noServers: 'No hay servidores guardados.',
@@ -461,4 +463,16 @@ export const settings = {
floatingPlayerBarSub: 'Mantener la barra del reproductor flotando sobre el contenido',
uiScaleTitle: 'Escala de Interfaz',
uiScaleLabel: 'Zoom',
scan: {
quick: 'Escaneo rápido',
full: 'Escaneo completo',
quickTip: 'Escaneo rápido — indexar archivos nuevos/modificados',
fullTip: 'Escaneo completo — reindexar todo',
confirmFull: 'Haz clic de nuevo para confirmar',
confirmFullShort: 'Confirmar escaneo completo',
scanning: 'Escaneando…',
done: 'Escaneo finalizado',
error: 'Error en el escaneo',
toast: '{{name}}: escaneo finalizado — {{count}} pistas indexadas',
},
};
+14
View File
@@ -22,6 +22,8 @@ export const settings = {
serverPassword: 'Mot de passe',
addServer: 'Ajouter un serveur',
addServerTitle: 'Ajouter un nouveau serveur',
editServer: 'Modifier',
editServerTitle: 'Modifier le serveur',
useServer: 'Utiliser',
deleteServer: 'Supprimer',
noServers: 'Aucun serveur enregistré.',
@@ -459,4 +461,16 @@ export const settings = {
floatingPlayerBarSub: 'Garder la barre du lecteur flottante au-dessus du contenu',
uiScaleTitle: "Mise à l'échelle de l'interface",
uiScaleLabel: 'Zoom',
scan: {
quick: 'Analyse rapide',
full: 'Analyse complète',
quickTip: 'Analyse rapide — indexer les fichiers nouveaux/modifiés',
fullTip: 'Analyse complète — tout réindexer',
confirmFull: 'Cliquer à nouveau pour confirmer',
confirmFullShort: "Confirmer l'analyse complète",
scanning: 'Analyse en cours…',
done: 'Analyse terminée',
error: "Échec de l'analyse",
toast: '{{name}} : analyse terminée — {{count}} pistes indexées',
},
};
+14
View File
@@ -22,6 +22,8 @@ export const settings = {
serverPassword: 'Passord',
addServer: 'Legg til tjener',
addServerTitle: 'Legg til ny tjener',
editServer: 'Rediger',
editServerTitle: 'Rediger tjener',
useServer: 'Bruk',
deleteServer: 'Slett',
noServers: 'Ingen tjenere lagret.',
@@ -458,4 +460,16 @@ export const settings = {
floatingPlayerBarSub: 'Hold spillerlinjen flytende over innholdet',
uiScaleTitle: 'Grensesnittskala',
uiScaleLabel: 'Zoom',
scan: {
quick: 'Hurtigskanning',
full: 'Full skanning',
quickTip: 'Hurtigskanning — indekser nye/endrede filer',
fullTip: 'Full skanning — reindekser alt',
confirmFull: 'Klikk igjen for å bekrefte',
confirmFullShort: 'Bekreft full skanning',
scanning: 'Skanner…',
done: 'Skanning ferdig',
error: 'Skanning mislyktes',
toast: '{{name}}: skanning ferdig — {{count}} spor indeksert',
},
};
+14
View File
@@ -22,6 +22,8 @@ export const settings = {
serverPassword: 'Wachtwoord',
addServer: 'Server toevoegen',
addServerTitle: 'Nieuwe server toevoegen',
editServer: 'Bewerken',
editServerTitle: 'Server bewerken',
useServer: 'Gebruiken',
deleteServer: 'Verwijderen',
noServers: 'Geen servers opgeslagen.',
@@ -459,4 +461,16 @@ export const settings = {
floatingPlayerBarSub: 'Houd de spelerbalk zwevend boven de inhoud',
uiScaleTitle: 'Interface schaal',
uiScaleLabel: 'Zoom',
scan: {
quick: 'Snel scannen',
full: 'Volledige scan',
quickTip: 'Snel scannen — nieuwe/gewijzigde bestanden indexeren',
fullTip: 'Volledige scan — alles opnieuw indexeren',
confirmFull: 'Klik nogmaals om te bevestigen',
confirmFullShort: 'Volledige scan bevestigen',
scanning: 'Scannen…',
done: 'Scan voltooid',
error: 'Scan mislukt',
toast: '{{name}}: scan voltooid — {{count}} tracks geïndexeerd',
},
};
+14
View File
@@ -22,6 +22,8 @@ export const settings = {
serverPassword: 'Parolă',
addServer: 'Adaugă Server',
addServerTitle: 'Adaugă Server nou',
editServer: 'Editează',
editServerTitle: 'Editează Server',
useServer: 'Folosește',
deleteServer: 'Șterge',
noServers: 'Niciun server salvat.',
@@ -464,4 +466,16 @@ export const settings = {
floatingPlayerBarSub: 'Păstrează bara playerului plutind deasupra conținutului',
uiScaleTitle: 'Scara interfaței',
uiScaleLabel: 'Zoom',
scan: {
quick: 'Scanare rapidă',
full: 'Scanare completă',
quickTip: 'Scanare rapidă — indexează fișierele noi/modificate',
fullTip: 'Scanare completă — reindexează totul',
confirmFull: 'Apasă din nou pentru a confirma',
confirmFullShort: 'Confirmă scanarea completă',
scanning: 'Se scanează…',
done: 'Scanare finalizată',
error: 'Scanare eșuată',
toast: '{{name}}: scanare finalizată — {{count}} piese indexate',
},
};
+14
View File
@@ -22,6 +22,8 @@ export const settings = {
serverPassword: 'Пароль',
addServer: 'Добавить сервер',
addServerTitle: 'Новый сервер',
editServer: 'Изменить',
editServerTitle: 'Изменить сервер',
useServer: 'Выбрать',
deleteServer: 'Удалить',
noServers: 'Нет сохранённых серверов.',
@@ -478,4 +480,16 @@ export const settings = {
floatingPlayerBarSub: 'Держать панель плеера плавающей над содержимым',
uiScaleTitle: 'Масштаб интерфейса',
uiScaleLabel: 'Масштаб',
scan: {
quick: 'Быстрое сканирование',
full: 'Полное сканирование',
quickTip: 'Быстрое сканирование — индексация новых/изменённых файлов',
fullTip: 'Полное сканирование — переиндексировать всё',
confirmFull: 'Нажмите ещё раз для подтверждения',
confirmFullShort: 'Подтвердить полное сканирование',
scanning: 'Сканирование…',
done: 'Сканирование завершено',
error: 'Ошибка сканирования',
toast: '{{name}}: сканирование завершено — проиндексировано треков: {{count}}',
},
};
+14
View File
@@ -22,6 +22,8 @@ export const settings = {
serverPassword: '密码',
addServer: '添加服务器',
addServerTitle: '添加新服务器',
editServer: '编辑',
editServerTitle: '编辑服务器',
useServer: '使用',
deleteServer: '删除',
noServers: '未保存任何服务器。',
@@ -459,4 +461,16 @@ export const settings = {
floatingPlayerBarSub: '保持播放栏悬浮在内容上方',
uiScaleTitle: '界面缩放',
uiScaleLabel: '缩放',
scan: {
quick: '快速扫描',
full: '完整扫描',
quickTip: '快速扫描 — 仅索引新增/变更文件',
fullTip: '完整扫描 — 重新索引全部',
confirmFull: '再次点击确认',
confirmFullShort: '确认完整扫描',
scanning: '扫描中…',
done: '扫描完成',
error: '扫描失败',
toast: '{{name}}: 扫描完成 — 已索引 {{count}} 首曲目',
},
};