diff --git a/core/application/p2p.go b/core/application/p2p.go
index 451e381214c2..dbd6f74aa9b2 100644
--- a/core/application/p2p.go
+++ b/core/application/p2p.go
@@ -53,9 +53,21 @@ func (a *Application) StartP2P() error {
return err
}
+ // modelsFn reports the model names this instance currently serves so the
+ // federation proxy can route a request only to peers that have the
+ // requested model. It is re-evaluated on every announce tick.
+ modelsFn := func() []string {
+ cfgs := a.ModelConfigLoader().GetAllModelsConfigs()
+ names := make([]string, 0, len(cfgs))
+ for _, c := range cfgs {
+ names = append(names, c.Name)
+ }
+ return names
+ }
+
// Here a new node is created and started
// and a service is exposed by the node
- node, err := p2p.ExposeService(ctx, "localhost", port, a.applicationConfig.P2PToken, p2p.NetworkID(networkID, p2p.FederatedID))
+ node, err := p2p.ExposeService(ctx, "localhost", port, a.applicationConfig.P2PToken, p2p.NetworkID(networkID, p2p.FederatedID), modelsFn)
if err != nil {
return err
}
diff --git a/core/cli/federated.go b/core/cli/federated.go
index c61adab0f072..a19e11df0965 100644
--- a/core/cli/federated.go
+++ b/core/cli/federated.go
@@ -14,12 +14,14 @@ type FederatedCLI struct {
RandomWorker bool `env:"LOCALAI_RANDOM_WORKER,RANDOM_WORKER" default:"false" help:"Select a random worker from the pool" group:"p2p"`
Peer2PeerNetworkID string `env:"LOCALAI_P2P_NETWORK_ID,P2P_NETWORK_ID" help:"Network ID for P2P mode, can be set arbitrarly by the user for grouping a set of instances." group:"p2p"`
TargetWorker string `env:"LOCALAI_TARGET_WORKER,TARGET_WORKER" help:"Target worker to run the federated server on" group:"p2p"`
+ UploadLimit int `env:"LOCALAI_UPLOAD_LIMIT,UPLOAD_LIMIT" default:"15" help:"Default upload-size limit in megabytes" group:"api"`
+ AffinitySync bool `env:"LOCALAI_FEDERATED_AFFINITY_SYNC,FEDERATED_AFFINITY_SYNC" default:"false" help:"Broadcast prefix-cache affinity observations to other federation servers over the p2p generic channel (enable on every federation server that should cohere)" group:"p2p"`
}
func (f *FederatedCLI) Run(ctx *cliContext.Context) error {
warnDeprecatedFlags()
- fs := p2p.NewFederatedServer(f.Address, p2p.NetworkID(f.Peer2PeerNetworkID, p2p.FederatedID), f.Peer2PeerToken, !f.RandomWorker, f.TargetWorker)
+ fs := p2p.NewFederatedServer(f.Address, p2p.NetworkID(f.Peer2PeerNetworkID, p2p.FederatedID), f.Peer2PeerToken, !f.RandomWorker, f.TargetWorker, int64(f.UploadLimit)*1024*1024, f.AffinitySync)
c, cancel := context.WithCancel(context.Background())
diff --git a/core/cli/worker/worker_p2p.go b/core/cli/worker/worker_p2p.go
index c7ff254ea451..223f4f90889a 100644
--- a/core/cli/worker/worker_p2p.go
+++ b/core/cli/worker/worker_p2p.go
@@ -62,7 +62,7 @@ func (r *P2P) Run(ctx *cliContext.Context) error {
p = r.RunnerPort
}
- _, err = p2p.ExposeService(c, address, p, r.Token, p2p.NetworkID(r.Peer2PeerNetworkID, p2p.LlamaCPPWorkerID))
+ _, err = p2p.ExposeService(c, address, p, r.Token, p2p.NetworkID(r.Peer2PeerNetworkID, p2p.LlamaCPPWorkerID), nil)
if err != nil {
return err
}
@@ -104,7 +104,7 @@ func (r *P2P) Run(ctx *cliContext.Context) error {
}
}()
- _, err = p2p.ExposeService(c, address, fmt.Sprint(port), r.Token, p2p.NetworkID(r.Peer2PeerNetworkID, p2p.LlamaCPPWorkerID))
+ _, err = p2p.ExposeService(c, address, fmt.Sprint(port), r.Token, p2p.NetworkID(r.Peer2PeerNetworkID, p2p.LlamaCPPWorkerID), nil)
if err != nil {
return err
}
diff --git a/core/cli/worker/worker_p2p_mlx.go b/core/cli/worker/worker_p2p_mlx.go
index 7edd1673def2..77a95394eece 100644
--- a/core/cli/worker/worker_p2p_mlx.go
+++ b/core/cli/worker/worker_p2p_mlx.go
@@ -81,7 +81,7 @@ func (r *P2PMLX) Run(ctx *cliContext.Context) error {
}
}()
- _, err = p2p.ExposeService(c, address, fmt.Sprint(port), r.Token, p2p.NetworkID(r.Peer2PeerNetworkID, p2p.MLXWorkerID))
+ _, err = p2p.ExposeService(c, address, fmt.Sprint(port), r.Token, p2p.NetworkID(r.Peer2PeerNetworkID, p2p.MLXWorkerID), nil)
if err != nil {
return err
}
diff --git a/core/http/react-ui/e2e/cluster.spec.js b/core/http/react-ui/e2e/cluster.spec.js
new file mode 100644
index 000000000000..523e0f60d50a
--- /dev/null
+++ b/core/http/react-ui/e2e/cluster.spec.js
@@ -0,0 +1,37 @@
+import { test, expect } from './coverage-fixtures.js'
+
+// The Cluster page composes two capability sections: "Distributed (NATS)" (the
+// former Nodes page) and "Swarm (p2p)" (the former P2P page). Each section only
+// mounts when its mode is enabled — distributed when /api/nodes answers OK, swarm
+// when a non-empty p2p network token is present. We mock those probes so the page
+// renders against the standalone ui-test-server without NATS / p2p running.
+
+async function mockDistributedOnly(page) {
+ await page.route('**/api/nodes', (route) => {
+ route.fulfill({ status: 200, contentType: 'application/json', body: '[]' })
+ })
+ await page.route('**/api/nodes/scheduling', (route) => {
+ route.fulfill({ status: 200, contentType: 'application/json', body: '[]' })
+ })
+ // Swarm disabled: token probe fails, so the swarm section stays hidden.
+ await page.route('**/api/p2p/token', (route) => {
+ route.fulfill({ status: 503, contentType: 'text/plain', body: '' })
+ })
+}
+
+test.describe('Cluster page', () => {
+ test('shows the page title', async ({ page }) => {
+ await mockDistributedOnly(page)
+ await page.goto('/app/cluster')
+ await expect(page).toHaveURL(/\/app\/cluster$/)
+ await expect(page.getByRole('heading', { name: /Cluster/i })).toBeVisible()
+ })
+
+ test('shows the distributed section when /api/nodes responds', async ({ page }) => {
+ await mockDistributedOnly(page)
+ await page.goto('/app/cluster')
+ await expect(page).toHaveURL(/\/app\/cluster$/)
+ // The distributed capability section is titled "Distributed (NATS)".
+ await expect(page.getByText(/Distributed \(NATS\)/i)).toBeVisible()
+ })
+})
diff --git a/core/http/react-ui/e2e/navigation.spec.js b/core/http/react-ui/e2e/navigation.spec.js
index d22dbe0a6c7f..cf2cf0a9a719 100644
--- a/core/http/react-ui/e2e/navigation.spec.js
+++ b/core/http/react-ui/e2e/navigation.spec.js
@@ -23,4 +23,11 @@ test.describe('Navigation', () => {
await expect(page).toHaveURL(/\/app\/traces/)
await expect(page.getByRole('heading', { name: 'Traces', exact: true })).toBeVisible()
})
+
+ test('old cluster routes redirect to /app/cluster', async ({ page }) => {
+ await page.goto('/app/p2p')
+ await expect(page).toHaveURL(/\/app\/cluster$/)
+ await page.goto('/app/nodes')
+ await expect(page).toHaveURL(/\/app\/cluster$/)
+ })
})
diff --git a/core/http/react-ui/e2e/nodes-per-node-backend-actions.spec.js b/core/http/react-ui/e2e/nodes-per-node-backend-actions.spec.js
index 76855437f15e..784a815f8202 100644
--- a/core/http/react-ui/e2e/nodes-per-node-backend-actions.spec.js
+++ b/core/http/react-ui/e2e/nodes-per-node-backend-actions.spec.js
@@ -81,7 +81,7 @@ async function mockDistributedNodes(page, { onDelete } = {}) {
}
async function expandNodeAndWaitForBackends(page) {
- await page.goto('/app/nodes')
+ await page.goto('/app/cluster')
// Click the row to expand it. The chevron toggle and the row both work,
// but clicking the name cell is the most user-like.
await page.getByText(NODE_NAME).first().click()
diff --git a/core/http/react-ui/e2e/p2p.spec.js b/core/http/react-ui/e2e/p2p.spec.js
index 8ea92faf85d9..70d0e8f397e3 100644
--- a/core/http/react-ui/e2e/p2p.spec.js
+++ b/core/http/react-ui/e2e/p2p.spec.js
@@ -1,26 +1,65 @@
import { test, expect } from './coverage-fixtures.js'
-// P2P (Swarm) admin page — renders in the no-auth test harness (isAdmin).
-test.describe('P2P page', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto('/app/p2p')
+// The standalone P2P (Swarm) page was merged into the Cluster page: /app/p2p now
+// redirects to /app/cluster, and the p2p content lives under the "Swarm (p2p)"
+// section. That section only mounts when p2p is enabled (a network token is
+// present), so we mock /api/p2p/token to return a non-empty token and assert the
+// swarm content renders under the cluster page.
+const P2P_TOKEN = 'test-network-token'
+
+async function mockSwarmEnabled(page) {
+ await page.route('**/api/p2p/token', (route) => {
+ route.fulfill({
+ status: 200,
+ contentType: 'text/plain',
+ body: P2P_TOKEN,
+ })
+ })
+ await page.route('**/api/p2p/workers', (route) => {
+ route.fulfill({ status: 200, contentType: 'application/json', body: '{"nodes":[]}' })
+ })
+ await page.route('**/api/p2p/federation', (route) => {
+ route.fulfill({ status: 200, contentType: 'application/json', body: '{"nodes":[]}' })
+ })
+ await page.route('**/api/p2p/stats', (route) => {
+ route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({
+ llama_cpp_workers: { online: 0, total: 0 },
+ federated: { online: 0, total: 0 },
+ mlx_workers: { online: 0, total: 0 },
+ }),
+ })
})
+ // The cluster page also probes /api/nodes for the distributed section; keep it
+ // failing (distributed disabled) so only the swarm section renders here.
+ await page.route('**/api/nodes', (route) => {
+ route.fulfill({ status: 503, contentType: 'application/json', body: '{}' })
+ })
+}
- test('renders the P2P distribution overview and capability cards', async ({ page }) => {
- await expect(page).toHaveURL(/\/app\/p2p$/)
- await expect(page.getByRole('heading', { name: /P2P Distribution Not Enabled/i })).toBeVisible()
- await expect(page.getByRole('heading', { name: 'Instance Federation' })).toBeVisible()
- await expect(page.getByRole('heading', { name: 'Model Sharding' })).toBeVisible()
- await expect(page.getByRole('heading', { name: 'Resource Sharing' })).toBeVisible()
- await expect(page.getByRole('heading', { name: /How to Enable P2P/i })).toBeVisible()
+test.describe('P2P (Swarm) section on the Cluster page', () => {
+ test('the old /app/p2p route lands on the cluster page', async ({ page }) => {
+ await mockSwarmEnabled(page)
+ // /app/p2p redirects to /app/cluster.
+ await page.goto('/app/p2p')
+ await expect(page).toHaveURL(/\/app\/cluster$/)
+ await expect(page.getByRole('heading', { name: /Cluster/i })).toBeVisible()
})
- test('hardware selector offers build targets and responds to selection', async ({ page }) => {
- const cpu = page.getByRole('button').filter({ hasText: /^CPU$/ })
- const cuda = page.getByRole('button').filter({ hasText: /^CUDA 12$/ })
- await expect(cpu).toBeVisible()
- await expect(cuda).toBeVisible()
- await cuda.click() // selecting a build target must not break the page
- await expect(page.getByRole('heading', { name: /How to Enable P2P/i })).toBeVisible()
+ test('renders the Swarm (p2p) section when p2p is enabled', async ({ page }) => {
+ await mockSwarmEnabled(page)
+ await page.goto('/app/cluster')
+ await expect(page).toHaveURL(/\/app\/cluster$/)
+
+ // The collapsible swarm section is titled "Swarm (p2p)".
+ await expect(page.getByText(/Swarm \(p2p\)/i)).toBeVisible()
+
+ // The enabled p2p content (Network Token panel + the federation / sharding
+ // tabs) is rendered inside the swarm section.
+ await expect(page.getByRole('heading', { name: /Network Token/i })).toBeVisible()
+ await expect(page.getByText('Federation', { exact: true })).toBeVisible()
+ await expect(page.getByText('Model Sharding', { exact: true })).toBeVisible()
})
})
diff --git a/core/http/react-ui/public/locales/de/admin.json b/core/http/react-ui/public/locales/de/admin.json
index 88582b5a20f3..767131bea32e 100644
--- a/core/http/react-ui/public/locales/de/admin.json
+++ b/core/http/react-ui/public/locales/de/admin.json
@@ -58,5 +58,21 @@
"explorer": {
"title": "Explorer",
"subtitle": "Dateien und Konfiguration durchsuchen"
+ },
+ "cluster": {
+ "title": "Cluster",
+ "subtitle": "Verteilte und Peer-to-Peer-Knoten, die diese Instanz bedienen",
+ "empty": "Es ist kein verteiltes oder p2p-Clustering aktiviert. Starte LocalAI im verteilten oder föderierten/p2p-Modus, um hier Cluster-Knoten zu verwalten.",
+ "distributed": {
+ "title": "Verteilt (NATS)"
+ },
+ "swarm": {
+ "title": "Swarm (p2p)"
+ },
+ "summary": {
+ "nodes": "Verteilte Knoten",
+ "inFlight": "Laufende Anfragen",
+ "peers": "Swarm-Peers online"
+ }
}
}
diff --git a/core/http/react-ui/public/locales/de/nav.json b/core/http/react-ui/public/locales/de/nav.json
index 891a15cae7dc..ddc1520a913e 100644
--- a/core/http/react-ui/public/locales/de/nav.json
+++ b/core/http/react-ui/public/locales/de/nav.json
@@ -40,6 +40,7 @@
"traces": "Traces",
"nodes": "Knoten",
"swarm": "Swarm",
+ "cluster": "Cluster",
"system": "System",
"settings": "Einstellungen",
"api": "API"
diff --git a/core/http/react-ui/public/locales/en/admin.json b/core/http/react-ui/public/locales/en/admin.json
index f4a380ae33ba..c88e4c225fa3 100644
--- a/core/http/react-ui/public/locales/en/admin.json
+++ b/core/http/react-ui/public/locales/en/admin.json
@@ -81,5 +81,21 @@
"explorer": {
"title": "Explorer",
"subtitle": "Browse files and configuration"
+ },
+ "cluster": {
+ "title": "Cluster",
+ "subtitle": "Distributed and peer-to-peer nodes serving this instance",
+ "empty": "No distributed or p2p clustering is enabled. Start LocalAI in distributed or federated/p2p mode to manage cluster nodes here.",
+ "distributed": {
+ "title": "Distributed (NATS)"
+ },
+ "swarm": {
+ "title": "Swarm (p2p)"
+ },
+ "summary": {
+ "nodes": "Distributed nodes",
+ "inFlight": "In-flight requests",
+ "peers": "Swarm peers online"
+ }
}
}
diff --git a/core/http/react-ui/public/locales/en/nav.json b/core/http/react-ui/public/locales/en/nav.json
index ac85d49794db..6d6f314925f2 100644
--- a/core/http/react-ui/public/locales/en/nav.json
+++ b/core/http/react-ui/public/locales/en/nav.json
@@ -41,6 +41,7 @@
"traces": "Traces",
"nodes": "Nodes",
"swarm": "Swarm",
+ "cluster": "Cluster",
"system": "System",
"settings": "Settings",
"api": "API"
diff --git a/core/http/react-ui/public/locales/es/admin.json b/core/http/react-ui/public/locales/es/admin.json
index fee37c1ab959..85141ef112d1 100644
--- a/core/http/react-ui/public/locales/es/admin.json
+++ b/core/http/react-ui/public/locales/es/admin.json
@@ -58,5 +58,21 @@
"explorer": {
"title": "Explorador",
"subtitle": "Explora archivos y configuración"
+ },
+ "cluster": {
+ "title": "Clúster",
+ "subtitle": "Nodos distribuidos y entre pares que sirven a esta instancia",
+ "empty": "No hay clustering distribuido ni p2p habilitado. Inicia LocalAI en modo distribuido o federado/p2p para gestionar aquí los nodos del clúster.",
+ "distributed": {
+ "title": "Distribuido (NATS)"
+ },
+ "swarm": {
+ "title": "Swarm (p2p)"
+ },
+ "summary": {
+ "nodes": "Nodos distribuidos",
+ "inFlight": "Solicitudes en curso",
+ "peers": "Pares de Swarm en línea"
+ }
}
}
diff --git a/core/http/react-ui/public/locales/es/nav.json b/core/http/react-ui/public/locales/es/nav.json
index 0c831a599af4..ab91f2072afb 100644
--- a/core/http/react-ui/public/locales/es/nav.json
+++ b/core/http/react-ui/public/locales/es/nav.json
@@ -40,6 +40,7 @@
"traces": "Trazas",
"nodes": "Nodos",
"swarm": "Swarm",
+ "cluster": "Clúster",
"system": "Sistema",
"settings": "Configuración",
"api": "API"
diff --git a/core/http/react-ui/public/locales/it/admin.json b/core/http/react-ui/public/locales/it/admin.json
index 2bd575b661be..90b05e8cc719 100644
--- a/core/http/react-ui/public/locales/it/admin.json
+++ b/core/http/react-ui/public/locales/it/admin.json
@@ -58,5 +58,21 @@
"explorer": {
"title": "Esplora risorse",
"subtitle": "Sfoglia file e configurazioni"
+ },
+ "cluster": {
+ "title": "Cluster",
+ "subtitle": "Nodi distribuiti e peer-to-peer al servizio di questa istanza",
+ "empty": "Nessun clustering distribuito o p2p è abilitato. Avvia LocalAI in modalità distribuita o federata/p2p per gestire qui i nodi del cluster.",
+ "distributed": {
+ "title": "Distribuito (NATS)"
+ },
+ "swarm": {
+ "title": "Swarm (p2p)"
+ },
+ "summary": {
+ "nodes": "Nodi distribuiti",
+ "inFlight": "Richieste in corso",
+ "peers": "Peer Swarm online"
+ }
}
}
diff --git a/core/http/react-ui/public/locales/it/nav.json b/core/http/react-ui/public/locales/it/nav.json
index e3d3ec434295..f9ef6f11c0bc 100644
--- a/core/http/react-ui/public/locales/it/nav.json
+++ b/core/http/react-ui/public/locales/it/nav.json
@@ -40,6 +40,7 @@
"traces": "Tracce",
"nodes": "Nodi",
"swarm": "Swarm",
+ "cluster": "Cluster",
"system": "Sistema",
"settings": "Impostazioni",
"api": "API"
diff --git a/core/http/react-ui/public/locales/zh-CN/admin.json b/core/http/react-ui/public/locales/zh-CN/admin.json
index d55487e69e88..1e938c950c70 100644
--- a/core/http/react-ui/public/locales/zh-CN/admin.json
+++ b/core/http/react-ui/public/locales/zh-CN/admin.json
@@ -58,5 +58,21 @@
"explorer": {
"title": "资源浏览器",
"subtitle": "浏览文件和配置"
+ },
+ "cluster": {
+ "title": "集群",
+ "subtitle": "为此实例提供服务的分布式和点对点节点",
+ "empty": "未启用分布式或 p2p 集群。请以分布式或联邦/p2p 模式启动 LocalAI,以便在此管理集群节点。",
+ "distributed": {
+ "title": "分布式 (NATS)"
+ },
+ "swarm": {
+ "title": "Swarm (p2p)"
+ },
+ "summary": {
+ "nodes": "分布式节点",
+ "inFlight": "进行中的请求",
+ "peers": "在线 Swarm 节点"
+ }
}
}
diff --git a/core/http/react-ui/public/locales/zh-CN/nav.json b/core/http/react-ui/public/locales/zh-CN/nav.json
index 84fff7c91942..9ff29d370748 100644
--- a/core/http/react-ui/public/locales/zh-CN/nav.json
+++ b/core/http/react-ui/public/locales/zh-CN/nav.json
@@ -40,6 +40,7 @@
"traces": "追踪",
"nodes": "节点",
"swarm": "Swarm",
+ "cluster": "集群",
"system": "系统",
"settings": "设置",
"api": "API"
diff --git a/core/http/react-ui/src/components/ClusterSection.jsx b/core/http/react-ui/src/components/ClusterSection.jsx
new file mode 100644
index 000000000000..9ff7ce1d7232
--- /dev/null
+++ b/core/http/react-ui/src/components/ClusterSection.jsx
@@ -0,0 +1,27 @@
+import { useState } from 'react'
+
+// ClusterSection is a collapsible, titled container for one capability area of
+// the Cluster page (Distributed / Swarm). Default expanded.
+export default function ClusterSection({ icon, title, subtitle, defaultOpen = true, children }) {
+ const [open, setOpen] = useState(defaultOpen)
+ return (
+
- +{pendingCount} awaiting approval — approve from Nodes. + +{pendingCount} awaiting approval — approve from Nodes.
)} diff --git a/core/http/react-ui/src/components/Sidebar.jsx b/core/http/react-ui/src/components/Sidebar.jsx index c75ed377d1e0..453b7bada510 100644 --- a/core/http/react-ui/src/components/Sidebar.jsx +++ b/core/http/react-ui/src/components/Sidebar.jsx @@ -75,8 +75,7 @@ const sections = [ { path: '/app/middleware', icon: 'fas fa-shield-halved', labelKey: 'items.middleware', adminOnly: true }, { path: '/app/backends', icon: 'fas fa-server', labelKey: 'items.backends', adminOnly: true }, { path: '/app/traces', icon: 'fas fa-chart-line', labelKey: 'items.traces', adminOnly: true }, - { path: '/app/nodes', icon: 'fas fa-network-wired', labelKey: 'items.nodes', adminOnly: true, feature: 'distributed' }, - { path: '/app/p2p', icon: 'fas fa-circle-nodes', labelKey: 'items.swarm', adminOnly: true }, + { path: '/app/cluster', icon: 'fas fa-network-wired', labelKey: 'items.cluster', adminOnly: true }, { path: '/app/manage', icon: 'fas fa-desktop', labelKey: 'items.system', adminOnly: true }, { path: '/app/settings', icon: 'fas fa-cog', labelKey: 'items.settings', adminOnly: true }, ], diff --git a/core/http/react-ui/src/hooks/useP2PMode.js b/core/http/react-ui/src/hooks/useP2PMode.js new file mode 100644 index 000000000000..98676df9f0a2 --- /dev/null +++ b/core/http/react-ui/src/hooks/useP2PMode.js @@ -0,0 +1,31 @@ +import { useState, useEffect, useCallback } from 'react' +import { p2pApi } from '../utils/api' + +// useP2PMode reports whether p2p / swarm mode is available, mirroring +// useDistributedMode. Availability is "a network token exists" (the same signal +// the standalone P2P page used). One-shot probe on mount plus a manual refetch. +// +// Returns: +// enabled — true when a non-empty network token is present +// loading — true until the first probe completes +// refetch — manual trigger to re-run the probe +export function useP2PMode() { + const [enabled, setEnabled] = useState(false) + const [loading, setLoading] = useState(true) + + const probe = useCallback(async () => { + setLoading(true) + try { + const token = await p2pApi.getToken() + setEnabled(!!(token && String(token).trim())) + } catch { + setEnabled(false) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { probe() }, [probe]) + + return { enabled, loading, refetch: probe } +} diff --git a/core/http/react-ui/src/pages/BackendLogs.jsx b/core/http/react-ui/src/pages/BackendLogs.jsx index 3f5216dbc5e9..83c042137f2e 100644 --- a/core/http/react-ui/src/pages/BackendLogs.jsx +++ b/core/http/react-ui/src/pages/BackendLogs.jsx @@ -339,7 +339,7 @@ function DistributedBackendLogsResolver({ modelId, fromTimestamp }) {{modelId} isn't currently loaded on any node in the cluster. - Check the Nodes page to see which models are running where. + Check the Nodes page to see which models are running where.
diff --git a/core/http/react-ui/src/pages/Backends.jsx b/core/http/react-ui/src/pages/Backends.jsx index 53f1ef547c44..56d249b82fc4 100644 --- a/core/http/react-ui/src/pages/Backends.jsx +++ b/core/http/react-ui/src/pages/Backends.jsx @@ -49,7 +49,7 @@ export default function Backends() { // whenever splitMenuFor changes to a different row index. const splitMenuAnchorRef = useRef(null) - // Target-node mode: set when navigated from /app/nodes via "+ Add backend". + // Target-node mode: set when navigated from /app/cluster via "+ Add backend". // The gallery page header banners the scope; rows collapse their split-button // to a single Install-on-this-node action; manual install posts to the // per-node endpoint. @@ -323,7 +323,7 @@ export default function Backends() { return ({t('cluster.subtitle', 'Distributed and peer-to-peer nodes serving this instance')}
+View backend logs from the{' '} - Nodes page. + Nodes page.
Backend logs from node {nodeName || nodeId} - {' '}(back to nodes) + {' '}(back to nodes)
diff --git a/core/http/react-ui/src/pages/Nodes.jsx b/core/http/react-ui/src/pages/Nodes.jsx index f2eb9d955770..7ddcf94e83c3 100644 --- a/core/http/react-ui/src/pages/Nodes.jsx +++ b/core/http/react-ui/src/pages/Nodes.jsx @@ -689,7 +689,7 @@ function SchedulingForm({ onSave, onCancel }) { ) } -export default function Nodes() { +export default function Nodes({ embedded = false }) { const { addToast } = useOutletContext() const navigate = useNavigate() const { t } = useTranslation('admin') @@ -983,16 +983,18 @@ export default function Nodes() { const pending = filteredNodes.filter(n => n.status === 'pending').length return ( -- {t('nodes.subtitle')} -
-+ {t('nodes.subtitle')} +
+- {t('p2p.subtitle')} - {' '} - - - -
-+ {t('p2p.subtitle')} + {' '} + + + +
+