From 622c1f9262800bf315e244db93265076bb1c2ba8 Mon Sep 17 00:00:00 2001 From: Anso Date: Sat, 11 Apr 2026 21:46:14 -0400 Subject: [PATCH] feat: home dashboard and Settings Hub polish (#506) * feat(dashboard): drop CPU column and relative timestamp from Stack Health and status bar The Stack Health table's CPU column duplicated data already surfaced in the top ResourceGauges and the CPU Usage historical chart. The health status bar's 'just now' timestamp was cosmetic: no consumer relied on lastUpdated state for polling, staleness detection, or conditional rendering. Removing both tightens the dashboard and eliminates a dead prop chain through useDashboardData. * refactor: remove dead admin_email field from setup flow The Setup form captured an admin email under 'Used for license recovery. Never shared with third parties.' but the value was written to global_settings and read nowhere: no license recovery, SMTP, or support contact flow consumed it. Rather than building UI on top of the dead field, delete the input, the payload key, and the backend persistence. Any orphaned row from prior setups is harmless and the frontend ignores unknown settings keys. * feat(settings): use Radix ScrollArea with per-section scroll memory Settings Hub used a native-scroll div that snapped to the top every time the user switched subsections and exposed the default browser scrollbar. Wrap the nav and content panes with the shadcn ScrollArea (Radix under the hood, type='hover') and expose a viewportRef so the modal can stash each section's scrollTop in a ref and restore it via useLayoutEffect on switch. Style the thumb with translucent foreground tokens so it reads as glass against popovers and dialogs. Replaces a hand-rolled scroll hook and ad-hoc CSS utility. --- backend/src/index.ts | 6 +-- docs/features/dashboard.mdx | 5 +- frontend/src/components/HomeDashboard.tsx | 2 - frontend/src/components/SettingsModal.tsx | 46 +++++++++++++++---- frontend/src/components/Setup.tsx | 17 ------- .../components/dashboard/HealthStatusBar.tsx | 16 +------ .../components/dashboard/StackHealthTable.tsx | 19 ++------ frontend/src/components/dashboard/types.ts | 1 - .../components/dashboard/useDashboardData.ts | 5 +- frontend/src/components/ui/scroll-area.tsx | 21 ++++++--- 10 files changed, 59 insertions(+), 79 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 5909683b..456015de 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -537,7 +537,7 @@ app.post('/api/auth/setup', authRateLimiter, async (req: Request, res: Response) return; } - const { username, password, confirmPassword, admin_email } = req.body; + const { username, password, confirmPassword } = req.body; // Validation if (!username || !password || !confirmPassword) { @@ -567,10 +567,6 @@ app.post('/api/auth/setup', authRateLimiter, async (req: Request, res: Response) dbSvc.updateGlobalSetting('auth_password_hash', passwordHash); dbSvc.updateGlobalSetting('auth_jwt_secret', jwtSecret); - if (admin_email && typeof admin_email === 'string') { - dbSvc.updateGlobalSetting('admin_email', admin_email.trim()); - } - // Create admin user in users table dbSvc.addUser({ username, password_hash: passwordHash, role: 'admin' }); diff --git a/docs/features/dashboard.mdx b/docs/features/dashboard.mdx index 49e2047f..80ca16ae 100644 --- a/docs/features/dashboard.mdx +++ b/docs/features/dashboard.mdx @@ -19,7 +19,7 @@ The top bar provides an at-a-glance health assessment for the active node. Sench | **Degraded** | At least one resource is above 80%, or there are unread error alerts. | | **Critical** | At least one resource is above 90%, or there are exited containers with unread errors. | -The bar also shows the active node name, the number of running containers, the current alert count, and a relative timestamp for the last data refresh. +The bar also shows the active node name, the number of running containers, and the current alert count. ## Resource gauges @@ -43,10 +43,9 @@ A table listing every stack in your `COMPOSE_DIR` with live status and resource |--------|-------------| | **Stack** | Stack name (derived from the directory name) | | **Status** | `UP` (running) or `DN` (exited) | -| **CPU** | Aggregate CPU usage across all containers in the stack, normalized over host cores | | **Memory** | Total memory allocated by the stack's containers | -Click any row to navigate directly to that stack's editor. Stacks are sorted with running stacks first, then alphabetically. If you have more than 8 stacks, the table paginates automatically. +Click any row to navigate directly to that stack's editor. Stacks are sorted with running stacks first, then alphabetically. If you have more than 8 stacks, the table paginates automatically. For fleet-wide CPU usage, see the **CPU** resource gauge card at the top of the dashboard and the **CPU Usage** historical chart below the table. ## Historical metrics charts diff --git a/frontend/src/components/HomeDashboard.tsx b/frontend/src/components/HomeDashboard.tsx index 7dc347ea..97225280 100644 --- a/frontend/src/components/HomeDashboard.tsx +++ b/frontend/src/components/HomeDashboard.tsx @@ -26,7 +26,6 @@ export default function HomeDashboard({ onNavigateToStack, notifications, onClea systemStats={data.systemStats} notifications={notifications} activeNodeName={activeNode?.name || 'Local'} - lastUpdated={data.lastUpdated} /> {})} /> diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 9784dc8a..34d7925b 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -1,10 +1,11 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useLayoutEffect, useRef } from 'react'; import { Dialog, DialogContent, DialogTitle, DialogDescription, } from '@/components/ui/dialog'; +import { ScrollArea } from '@/components/ui/scroll-area'; import { VisuallyHidden } from '@radix-ui/react-visually-hidden'; import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; @@ -38,6 +39,11 @@ import { } from './settings'; import type { PatchableSettings, SectionId } from './settings'; +const GLOBAL_ONLY_SECTIONS: ReadonlySet = new Set([ + 'account', 'license', 'users', 'sso', 'api-tokens', 'registries', + 'labels', 'notifications', 'notification-routing', 'webhooks', 'nodes', 'appstore', +]); + interface SettingsModalProps { isOpen: boolean; onClose: () => void; @@ -51,14 +57,30 @@ export function SettingsModal({ isOpen, onClose, initialSection }: SettingsModal const isRemote = activeNode?.type === 'remote'; const [activeSection, setActiveSection] = useState(initialSection || 'account'); + const contentViewportRef = useRef(null); + const scrollPositionsRef = useRef>>({}); + + const switchSection = (next: SectionId) => { + if (contentViewportRef.current) { + scrollPositionsRef.current[activeSection] = contentViewportRef.current.scrollTop; + } + setActiveSection(next); + }; + + useLayoutEffect(() => { + if (contentViewportRef.current) { + contentViewportRef.current.scrollTop = scrollPositionsRef.current[activeSection] ?? 0; + } + }, [activeSection]); + useEffect(() => { if (isOpen && initialSection) setActiveSection(initialSection); }, [isOpen, initialSection]); - // When switching to a remote node, reset to a node-scoped section if on a global-only one + // Remote nodes don't expose global-only sections, so bounce to a node-scoped one. useEffect(() => { - if (isRemote && (activeSection === 'account' || activeSection === 'license' || activeSection === 'users' || activeSection === 'sso' || activeSection === 'api-tokens' || activeSection === 'registries' || activeSection === 'labels' || activeSection === 'notifications' || activeSection === 'notification-routing' || activeSection === 'webhooks' || activeSection === 'nodes' || activeSection === 'appstore')) { - setActiveSection('system'); + if (isRemote && GLOBAL_ONLY_SECTIONS.has(activeSection)) { + switchSection('system'); } }, [isRemote]); // eslint-disable-line react-hooks/exhaustive-deps @@ -221,7 +243,7 @@ export function SettingsModal({ isOpen, onClose, initialSection }: SettingsModal