feat(ui): Phase 57 - remote context UX (Option A) + network layer fixes

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
This commit is contained in:
SaelixCode
2026-03-19 20:39:57 -04:00
parent e027a94492
commit 04c770c198
7 changed files with 117 additions and 40 deletions
@@ -5,6 +5,7 @@ import { Input } from '@/components/ui/input';
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { RefreshCw, Download, Trash2, Search, Filter } from 'lucide-react';
import { apiFetch } from '@/lib/api';
import { useNodes } from '@/context/NodeContext';
interface LogEntry {
@@ -17,6 +18,7 @@ interface LogEntry {
}
export function GlobalObservabilityView() {
const { activeNode } = useNodes();
const [logs, setLogs] = useState<LogEntry[]>([]);
const [loading, setLoading] = useState(true);
const [allStacks, setAllStacks] = useState<string[]>([]);
@@ -57,7 +59,7 @@ export function GlobalObservabilityView() {
useEffect(() => {
const fetchStacks = async () => {
try {
const res = await fetch('/api/stacks');
const res = await apiFetch('/stacks');
if (res.ok) {
const stacks: string[] = await res.json();
setAllStacks(stacks.sort());
@@ -111,7 +113,7 @@ export function GlobalObservabilityView() {
const fetchData = async () => {
setLoading(true);
try {
const logsRes = await fetch('/api/logs/global');
const logsRes = await apiFetch('/logs/global');
if (logsRes.ok) {
setLogs(await logsRes.json());
}
@@ -180,6 +182,13 @@ export function GlobalObservabilityView() {
return (
<div className="flex flex-col h-full w-full relative group bg-[#0A0A0A] text-gray-300">
{/* Node Context Indicator */}
{activeNode?.type === 'remote' && (
<div className="absolute top-2 left-4 z-10 flex items-center gap-1.5 bg-background/90 backdrop-blur-sm border border-border shadow-md rounded-md px-2.5 py-1 text-xs text-muted-foreground">
<span className="w-1.5 h-1.5 rounded-full bg-blue-400 shrink-0" />
{activeNode.name}
</div>
)}
{/* Floating Action Bar */}
<div className="absolute top-2 right-6 z-10 flex gap-2 transition-opacity duration-200 opacity-0 group-hover:opacity-100 focus-within:opacity-100 bg-background/90 backdrop-blur-sm border border-border shadow-md rounded-md p-1 pr-1">
<div className="relative flex items-center">