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