Playwright investigation revealed the Logs view (GlobalObservabilityView)
rendered 1,859+ log entries as real DOM nodes (9,616 total DOM nodes) with
no virtualization. Combined with a 5-second polling cycle replacing all
React elements each time and smooth-scroll animations stacking on every
update, the renderer process grew rapidly on a host running at 97% RAM
usage, crashing the browser tab with Out of Memory within minutes.
Fixes:
- Cap rendered DOM rows to MAX_DISPLAY_ROWS (300) via .slice(-300) so the
browser only ever holds ~1,500 log-related DOM nodes regardless of how
many entries are in state
- Add a truncation notice when log count exceeds the display cap
- Reduce SSE-mode in-memory log cap from 10,000 to MAX_LOG_ENTRIES (2,000)
- Switch auto-scroll from behavior:'smooth' to behavior:'instant' to stop
stacking layout animations on every 5-second poll
- Reduce /api/logs/global response limit from 2,000 to 500 lines since the
client renders at most 300 rows, making the extra payload wasteful
Task 1 - Network layer fixes:
- AppStoreView: replace raw fetch() with apiFetch() on /templates and
/templates/deploy so x-node-id header is injected for proxy routing
- GlobalObservabilityView: replace raw fetch() with apiFetch() on
/stacks and /logs/global
- HostConsole: append ?nodeId= to WebSocket URL so the upgrade handler
correctly routes the PTY session to the active remote node
Task 2 - Scoped context two-tier UX (Option A / Portainer-style):
- EditorLayout: add a context pill in the top header bar showing the
active node name (pulsing blue for remote, green for local)
- HostConsole: header now reads "Host Console - [Node Name]"
- ResourcesView: title now reads "Resources Hub - [Node Name]" for remote
- GlobalObservabilityView: floating node badge in top-left corner for remote
- AppStoreView: deploy sheet shows "Deploying to: [Node Name]" badge
- SettingsModal: remote nodes only see System Limits + Developer tabs;
global-only tabs (Account, Appearance, Notifications, Nodes) are hidden;
header subtitle shows remote node name
- Cap GlobalObservabilityView SSE log array at 10,000 entries to prevent
unbounded memory growth during long dev-mode sessions
- Break NodeContext infinite re-fetch loop by replacing the activeNode
closure dependency in refreshNodes with a useRef read, making the
callback stable and preventing the useEffect -> setState -> useCallback
-> useEffect cycle