diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index fe690700..27f25497 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,4 +1,5 @@
import { Suspense, lazy } from 'react'
+import { Navigate } from 'react-router-dom'
import { BrowserRouter as Router, Routes, Route, Outlet } from 'react-router-dom'
import Layout from './components/Layout'
import { ToastContainer } from './components/Toast'
@@ -54,7 +55,7 @@ export default function App() {
} />
} />
} />
- } />
+ } />
{/* Settings Routes */}
}>
diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx
index 2f05ff4e..cf56fd40 100644
--- a/frontend/src/components/Layout.tsx
+++ b/frontend/src/components/Layout.tsx
@@ -13,6 +13,13 @@ interface LayoutProps {
children: ReactNode
}
+type NavItem = {
+ name: string
+ path?: string
+ icon?: string
+ children?: NavItem[]
+}
+
export default function Layout({ children }: LayoutProps) {
const location = useLocation()
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
@@ -41,7 +48,7 @@ export default function Layout({ children }: LayoutProps) {
staleTime: 1000 * 60 * 60, // 1 hour
})
- const navigation = [
+ const navigation: NavItem[] = [
{ name: 'Dashboard', path: '/', icon: '📊' },
{ name: 'Proxy Hosts', path: '/proxy-hosts', icon: '🌐' },
{ name: 'Remote Servers', path: '/remote-servers', icon: '🖥️' },
@@ -162,9 +169,9 @@ export default function Layout({ children }: LayoutProps) {
{isExpanded && (
- {item.children.map((child) => {
+ {item.children.map((child: NavItem) => {
// If this child has its own children, render a nested accordion
- if ((child as any).children) {
+ if (child.children && child.children.length > 0) {
const nestedExpandedKey = `${item.name}:${child.name}`
const isNestedOpen = expandedMenus.includes(nestedExpandedKey)
@@ -190,10 +197,10 @@ export default function Layout({ children }: LayoutProps) {
{isNestedOpen && (
- {(child as any).children.map((sub: any) => (
+ {child.children.map((sub: NavItem) => (
setMobileSidebarOpen(false)}
className={`block py-2 px-3 rounded-md text-sm transition-colors ${
location.pathname === sub.path
diff --git a/frontend/src/pages/__tests__/Uptime.spec.tsx b/frontend/src/pages/__tests__/Uptime.spec.tsx
index 74585351..b86ed566 100644
--- a/frontend/src/pages/__tests__/Uptime.spec.tsx
+++ b/frontend/src/pages/__tests__/Uptime.spec.tsx
@@ -100,6 +100,37 @@ describe('Uptime page', () => {
expect(barTitles.some(el => (el.getAttribute('title') || '').includes('Status: DOWN'))).toBeTruthy()
})
+ it('pause button is yellow and appears before delete in settings menu', async () => {
+ const monitor = {
+ id: 'm12', name: 'OrderTest', url: 'http://example.com', type: 'http', interval: 60, enabled: true,
+ status: 'up', last_check: new Date().toISOString(), latency: 10, max_retries: 3,
+ }
+ vi.mocked(uptimeApi.getMonitors).mockResolvedValue([monitor])
+ vi.mocked(uptimeApi.getMonitorHistory).mockResolvedValue([])
+
+ renderWithProviders()
+ await waitFor(() => expect(screen.getByText('OrderTest')).toBeInTheDocument())
+ const card = screen.getByText('OrderTest').closest('div') as HTMLElement
+ await userEvent.click(within(card).getByTitle('Monitor settings'))
+
+ const configureBtn = within(card).getByText('Configure')
+ // Find the menu container by traversing up until the absolute positioned menu is found
+ let menuContainer: HTMLElement | null = configureBtn.parentElement
+ while (menuContainer && !menuContainer.className.includes('absolute')) {
+ menuContainer = menuContainer.parentElement
+ }
+ expect(menuContainer).toBeTruthy()
+ const buttons = Array.from(menuContainer!.querySelectorAll('button'))
+ const pauseBtn = buttons.find(b => b.textContent?.trim() === 'Pause')
+ const deleteBtn = buttons.find(b => b.textContent?.trim() === 'Delete')
+ expect(pauseBtn).toBeTruthy()
+ expect(deleteBtn).toBeTruthy()
+ // Ensure Pause appears before Delete
+ expect(buttons.indexOf(pauseBtn!)).toBeLessThan(buttons.indexOf(deleteBtn!))
+ // Ensure Pause has yellow styling class
+ expect(pauseBtn!.className).toContain('text-yellow-600')
+ })
+
it('deletes monitor when delete confirmed and shows toast', async () => {
const monitor = {
id: 'm5', name: 'DeleteMe', url: 'http://example.com', type: 'http', interval: 60, enabled: true,