feat(settings): show server software and version on server cards (#1045)

* feat(settings): show server software and version on server cards

Render the OpenSubsonic-reported software and version (e.g. "Navidrome
0.62.0") under each server name on Settings -> Servers. The value comes
from the existing ping identity (subsonicServerIdentityByServer), so no
extra request is made; the line is omitted when the server reports no
type (e.g. plain Subsonic without OpenSubsonic).

Extract the formatting into a shared formatServerSoftware() helper and
reuse it in the PsyLab perf-probe server section in place of its local
copy.

* docs: add CHANGELOG entry for PR #1045
This commit is contained in:
Psychotoxical
2026-06-09 10:25:28 +02:00
committed by GitHub
parent b2a5baa48d
commit ad8e376c9c
5 changed files with 65 additions and 14 deletions
+16 -6
View File
@@ -2,8 +2,9 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { open as openUrl } from '@tauri-apps/plugin-shell';
import { AlertTriangle, CheckCircle2, Lock, LogOut, Pencil, Plus, Power, Server, Sparkles, Trash2, User, Wifi, WifiOff } from 'lucide-react';
import { AlertTriangle, CheckCircle2, Globe, Lock, LogOut, Pencil, Plus, Power, Server, Sparkles, Trash2, User, Wifi, WifiOff } from 'lucide-react';
import { useAuthStore } from '../../store/authStore';
import { formatServerSoftware } from '../../utils/server/subsonicServerIdentity';
import { useLibraryIndexStore } from '../../store/libraryIndexStore';
import { libraryDeleteServerData, librarySyncClearSession } from '../../api/library';
import { bootstrapIndexedServer } from '../../utils/library/librarySession';
@@ -394,6 +395,7 @@ export function ServersTab({
const status = connStatus[srv.id];
const isBefore = psyDragState.isDragging && serverDropTarget?.idx === srvIdx && serverDropTarget.before;
const isAfter = psyDragState.isDragging && serverDropTarget?.idx === srvIdx && !serverDropTarget.before;
const serverSoftware = formatServerSoftware(auth.subsonicServerIdentityByServer[srv.id]);
return (
<div
key={srv.id}
@@ -426,16 +428,24 @@ export function ServersTab({
</span>
)}
</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: 4, overflow: 'hidden' }}>
{srv.url.startsWith('https://') && (
<Lock size={11} style={{ color: 'var(--positive)', flexShrink: 0 }} />
{serverSoftware && (
<div style={{ fontSize: 11, color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: 4, marginBottom: '2px', overflow: 'hidden' }}>
<Server size={10} style={{ flexShrink: 0 }} />
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{serverSoftware}</span>
</div>
)}
<div style={{ fontSize: 11, color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: 4, overflow: 'hidden' }}>
{srv.url.startsWith('https://') ? (
<Lock size={10} style={{ color: 'var(--positive)', flexShrink: 0 }} />
) : (
<Globe size={10} style={{ flexShrink: 0 }} />
)}
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{srv.url.replace(/^https?:\/\//, '')}
</span>
</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: 4, marginTop: 1 }}>
<User size={11} />
<div style={{ fontSize: 11, color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: 4, marginTop: 1 }}>
<User size={10} />
{srv.username}
</div>
</div>
@@ -1,6 +1,6 @@
import { useAuthStore } from '../../../store/authStore';
import type { NavidromeAdminRole } from '../../../hooks/useNavidromeAdminRole';
import { isNavidromeServer } from '../../../utils/server/subsonicServerIdentity';
import { isNavidromeServer, formatServerSoftware } from '../../../utils/server/subsonicServerIdentity';
import { FEATURE_AUDIOMUSE_SIMILAR_TRACKS, OP_SIMILAR_TRACKS } from '../../../serverCapabilities/catalog';
import {
isFeatureActiveForServer,
@@ -12,12 +12,6 @@ import { PerfProbeMetricSection } from './PerfProbeMetricCard';
import PerfProbeDetailList, { type PerfProbeDetailRow } from './PerfProbeDetailList';
import PerfProbeStatusBadge, { type PerfProbeBadgeTone } from './PerfProbeStatusBadge';
function formatServerType(identity: { type?: string; serverVersion?: string; openSubsonic?: boolean } | undefined): string {
if (!identity?.type?.trim()) return 'Unknown';
const version = identity.serverVersion?.trim();
return version ? `${identity.type} ${version}` : identity.type;
}
function detectionBadge(status: CapabilityStatus): { tone: PerfProbeBadgeTone; label: string } {
switch (status) {
case 'present': return { tone: 'ok', label: 'Detected' };
@@ -97,7 +91,7 @@ export default function SidebarPerfProbeServerSection({ adminRole = 'na' }: Prop
const rows: PerfProbeDetailRow[] = [
{ label: 'Name', value: server.name || server.url },
{ label: 'Profile URL', value: <code className="perf-server-dl__code">{server.url}</code> },
{ label: 'Subsonic server', value: formatServerType(identity) },
{ label: 'Subsonic server', value: formatServerSoftware(identity) ?? 'Unknown' },
{
label: 'OpenSubsonic',
value: identity?.openSubsonic