diff --git a/frontend/src/components/ds/StatusDot.jsx b/frontend/src/components/ds/StatusDot.jsx
new file mode 100644
index 00000000..8924ff40
--- /dev/null
+++ b/frontend/src/components/ds/StatusDot.jsx
@@ -0,0 +1,9 @@
+import { cn } from '@/lib/utils';
+
+// A Pill's dot without the label; the status text rides along as the accessible
+// name and tooltip. kind: same vocabulary as Pill.
+export function StatusDot({ kind = 'gray', label, className }) {
+ return ;
+}
+
+export default StatusDot;
diff --git a/frontend/src/components/ds/index.js b/frontend/src/components/ds/index.js
index 5ae38562..a66deb05 100644
--- a/frontend/src/components/ds/index.js
+++ b/frontend/src/components/ds/index.js
@@ -2,6 +2,7 @@
// Shared, reusable building blocks for the new "infra console" look. Styles
// live in styles/components/_design-system.scss (sk-* namespace).
export { Pill } from './Pill';
+export { StatusDot } from './StatusDot';
export {
statusKind, statusVariant, statusDotClass, statusLabel,
alertStatusKind, processStateVariant, envActionKind,
diff --git a/frontend/src/components/serverdetail/ServerHeaderMeta.jsx b/frontend/src/components/serverdetail/ServerHeaderMeta.jsx
new file mode 100644
index 00000000..1ed9c32d
--- /dev/null
+++ b/frontend/src/components/serverdetail/ServerHeaderMeta.jsx
@@ -0,0 +1,33 @@
+import { useTranslation } from 'react-i18next';
+import FavoriteStar from '@/components/FavoriteStar';
+import InfoTip from '@/components/InfoTip';
+import CopyButton from '@/components/CopyButton';
+import { StatusDot } from '../ds';
+import { serverStatusKind } from './serverDetailData';
+
+// Header meta: [status dot] [(i) with endpoint + ID] [star]. Everything the
+// header used to spell out lives behind the (i).
+export default function ServerHeaderMeta({ server }) {
+ const { t } = useTranslation();
+ const status = server.status || 'pending';
+ const endpoint = server.hostname || server.ip_address || t('app.serverDetail.noEndpointConfigured', 'No endpoint configured');
+ return (
+ <>
+
+
+
+ - {t('common.labels.status', 'Status')}
+ - {status}
+ - {t('app.serverDetail.endpoint', 'Endpoint')}
+ - {endpoint}
+ - {t('app.serverDetail.serverId', 'ID')}
+ -
+
{server.id}
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/components/serverdetail/serverDetailData.js b/frontend/src/components/serverdetail/serverDetailData.js
index 258dc345..e9a8c8fa 100644
--- a/frontend/src/components/serverdetail/serverDetailData.js
+++ b/frontend/src/components/serverdetail/serverDetailData.js
@@ -5,6 +5,11 @@ import { statusKind } from '../ds/status.js';
// Server connection states ride the shared vocabulary.
export const serverStatusKind = (status) => statusKind(status);
+// The backend names an unpaired server "Pending pairing ()"
+// (api/servers.py _placeholder_name); the id suffix is noise in a heading.
+const PLACEHOLDER_NAME = /^Pending pairing \(.*\)$/;
+export const isPlaceholderServerName = (name) => PLACEHOLDER_NAME.test(name ?? '');
+
export const PRESET_LABELS = {
'* * * * *': 'Every minute',
'*/5 * * * *': 'Every 5 minutes',
diff --git a/frontend/src/components/serverdetail/serverDetailData.test.mjs b/frontend/src/components/serverdetail/serverDetailData.test.mjs
new file mode 100644
index 00000000..68dc32b6
--- /dev/null
+++ b/frontend/src/components/serverdetail/serverDetailData.test.mjs
@@ -0,0 +1,14 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+
+import { isPlaceholderServerName } from './serverDetailData.js';
+
+test('backend placeholder names are detected', () => {
+ assert.equal(isPlaceholderServerName('Pending pairing (342ade51)'), true);
+});
+
+test('real names are left alone', () => {
+ assert.equal(isPlaceholderServerName('web-01'), false);
+ assert.equal(isPlaceholderServerName('Pending pairing'), false);
+ assert.equal(isPlaceholderServerName(undefined), false);
+});
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index c9298e20..0d65f546 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -5387,7 +5387,10 @@
"alerts": "Alerts",
"areYouSureYouWantTo": "Remove this server? This can't be undone.",
"backToServers": "Back to servers",
+ "copyId": "Copy ID",
"cron": "Cron",
+ "details": "Server details",
+ "endpoint": "Endpoint",
"errorLoadingServer": "Couldn't load the server.",
"failedToAcknowledgeAlert": "Couldn't acknowledge the alert.",
"failedToPingServer": "Couldn't ping the server.",
@@ -5398,6 +5401,7 @@
"metrics": "Metrics",
"noEndpointConfigured": "No endpoint configured",
"packages": "Packages",
+ "pendingPairing": "Pending pairing",
"ping": "Ping",
"proxy": "Proxy",
"remoteAccess": "Remote access",
@@ -5405,6 +5409,7 @@
"restorePoints": "Restore points",
"sections": "Server sections",
"serverDidNotRespond": "Server did not respond",
+ "serverId": "ID",
"serverNotFound": "Server not found",
"serverRemovedSuccessfully": "Server removed",
"serverRespondedInMs": "Server responded in {{latency}}ms",
diff --git a/frontend/src/pages/ServerDetail.jsx b/frontend/src/pages/ServerDetail.jsx
index 8d9debbc..4b8f252a 100644
--- a/frontend/src/pages/ServerDetail.jsx
+++ b/frontend/src/pages/ServerDetail.jsx
@@ -1,13 +1,12 @@
-import { serverStatusKind } from '../components/serverdetail/serverDetailData';
+import { isPlaceholderServerName } from '../components/serverdetail/serverDetailData';
+import ServerHeaderMeta from '../components/serverdetail/ServerHeaderMeta';
import { useState, useEffect, useCallback } from 'react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import api from '../services/api';
import { useToast } from '../contexts/useToast.js';
import { useConfirm } from '../hooks/useConfirm';
import { useRecordVisit } from '@/hooks/useRecordVisit';
-import FavoriteStar from '@/components/FavoriteStar';
import { Button } from '@/components/ui/button';
-import { Pill } from '../components/ds';
import { Tabs, TabsContent } from '@/components/ui/tabs';
import { Tip } from '@/components/ui/tooltip';
import { Server, ArrowLeft } from 'lucide-react';
@@ -276,17 +275,9 @@ const ServerDetail = () => {
}
- title={server.name}
+ title={isPlaceholderServerName(server.name) ? t('app.serverDetail.pendingPairing', 'Pending pairing') : server.name}
navLabel={t('app.serverDetail.sections', 'Server sections')}
- meta={(
- <>
-
- {server.status || 'pending'}
-
- {server.hostname || server.ip_address || t('app.serverDetail.noEndpointConfigured', 'No endpoint configured')}
-
- >
- )}
+ meta={}
tabs={tabs.map((item) => ({
id: item.id,
label: item.badge ? <>{item.label}{item.badge}> : item.label,
diff --git a/frontend/src/styles/components/_design-system.scss b/frontend/src/styles/components/_design-system.scss
index 2a836513..357b4eae 100644
--- a/frontend/src/styles/components/_design-system.scss
+++ b/frontend/src/styles/components/_design-system.scss
@@ -14,6 +14,22 @@ $sk-tones: ('accent', 'green', 'cyan', 'amber', 'red', 'violet');
// ------------------------------------------------------------
// Pill — status badge (dot + label)
// ------------------------------------------------------------
+// StatusDot: the Pill's dot on its own, for headers that want colour without a label.
+.sk-statusdot {
+ display: inline-block;
+ flex: none;
+ width: 8px; // geometry: status dot
+ height: 8px; // geometry: status dot
+ border-radius: 50%;
+
+ &--green { background: var(--green); }
+ &--amber { background: var(--amber); }
+ &--red { background: var(--red); }
+ &--cyan { background: var(--cyan); }
+ &--violet { background: var(--violet); }
+ &--gray { background: var(--text-faint); }
+}
+
.sk-pill {
// Badge spec: 20px tall, 4px radius, 12px/500, tinted fill, flat dot.
display: inline-flex;
diff --git a/frontend/src/styles/pages/_servers.scss b/frontend/src/styles/pages/_servers.scss
index 68ffeac9..a6d83bdf 100644
--- a/frontend/src/styles/pages/_servers.scss
+++ b/frontend/src/styles/pages/_servers.scss
@@ -2554,3 +2554,37 @@
flex-direction: column;
gap: $space-3;
}
+
+// Header: dot / (i) / star share one centered row beside the title; on narrow
+// screens the title block and actions stay on ONE row (title truncates), tabs below.
+.server-detail-page .sk-topbar {
+ &__titles { align-items: center; }
+ &__meta { display: inline-flex; align-items: center; gap: $space-2; }
+
+ .fav-star {
+ width: 24px; // geometry: matches the InfoTip md footprint
+ height: 24px; // geometry: matches the InfoTip md footprint
+ svg { width: 14px; height: 14px; } // geometry: inline icon
+ opacity: 0.6;
+ &:hover, &.is-on { opacity: 1; }
+ }
+
+ @media (max-width: 768px) {
+ &__titles { flex: 1 1 0; min-width: 0; flex-wrap: nowrap; }
+ &__title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal; }
+ &__spacer { display: none; }
+ &__actions { order: 0; width: auto; margin-left: auto; justify-content: flex-end; overflow: visible; flex-wrap: nowrap; }
+ }
+}
+
+.server-detail-info {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: $space-1 $space-3;
+ margin: 0;
+
+ dt { color: var(--text-faint); }
+ dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
+
+ &__id { display: flex; align-items: center; gap: $space-1; code { font-family: $font-mono; } }
+}