diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx index 985d8fb..b1056ce 100644 --- a/frontend-modern/src/App.tsx +++ b/frontend-modern/src/App.tsx @@ -21,6 +21,7 @@ import Settings from './components/Settings/Settings'; import { Alerts } from './pages/Alerts'; import { DockerHosts } from './components/Docker/DockerHosts'; import { ToastContainer } from './components/Toast/Toast'; +import NotificationContainer from './components/NotificationContainer'; import { ErrorBoundary } from './components/ErrorBoundary'; import { SecurityWarning } from './components/SecurityWarning'; import { Login } from './components/Login'; @@ -584,6 +585,7 @@ function App() { + @@ -784,12 +786,34 @@ function AppLayout(props: { navigate(tab.route); }; + type PlatformTab = ReturnType[number]; + type UtilityTab = ReturnType[number]; + + const mobileTabs = createMemo< + Array<(PlatformTab & { type: 'platform' }) | (UtilityTab & { type: 'utility' })> + >(() => { + const platformEntries = platformTabs().map((tab) => ({ ...tab, type: 'platform' as const })); + const utilityEntries = utilityTabs().map((tab) => ({ ...tab, type: 'utility' as const })); + return [...platformEntries, ...utilityEntries]; + }); + + const handleMobileTabChange = (tabId: PlatformTab['id'] | UtilityTab['id']) => { + const platform = platformTabs().find((tab) => tab.id === tabId); + if (platform) { + handlePlatformClick(platform); + return; + } + const utility = utilityTabs().find((tab) => tab.id === tabId); + if (utility) { + handleUtilityClick(utility); + } + }; + return (
{/* Header */} -
- -
+
+
-
-
+
+
+ {/* Mobile Tab Switcher */} +
+ +
+ +
+ +
+
+
+ {/* Tabs */}
@@ -1008,7 +1064,10 @@ function AppLayout(props: { })()} - + + Update available + +
); diff --git a/frontend-modern/src/components/Backups/UnifiedBackups.tsx b/frontend-modern/src/components/Backups/UnifiedBackups.tsx index e848be1..146508f 100644 --- a/frontend-modern/src/components/Backups/UnifiedBackups.tsx +++ b/frontend-modern/src/components/Backups/UnifiedBackups.tsx @@ -1353,7 +1353,7 @@ const UnifiedBackups: Component = () => { {/* Available Backups chart - keep visible while backups exist or a date filter is active */} -
+
@@ -1416,7 +1416,7 @@ const UnifiedBackups: Component = () => {
)} -
+
-
+ Last {chartTimeRange()} days
-
+
0} fallback={ @@ -1511,7 +1511,9 @@ const UnifiedBackups: Component = () => { > { // Use createEffect to reactively update the chart createEffect(() => { diff --git a/frontend-modern/src/components/Docker/DockerHostSummaryTable.tsx b/frontend-modern/src/components/Docker/DockerHostSummaryTable.tsx index 09134db..37f5dac 100644 --- a/frontend-modern/src/components/Docker/DockerHostSummaryTable.tsx +++ b/frontend-modern/src/components/Docker/DockerHostSummaryTable.tsx @@ -4,6 +4,7 @@ import { Card } from '@/components/shared/Card'; import { MetricBar } from '@/components/Dashboard/MetricBar'; import { renderDockerStatusBadge } from './DockerStatusBadge'; import { formatUptime } from '@/utils/format'; +import { ScrollableTable } from '@/components/shared/ScrollableTable'; export interface DockerHostSummary { host: DockerHost; @@ -114,15 +115,15 @@ export const DockerHostSummaryTable: Component = (p return ( -
- + +
props.onSelect(summary.host.id)} > - - - - - - - -
handleSort('name')} onKeyDown={(e) => e.key === 'Enter' && handleSort('name')} - tabindex="0" + tabIndex={0} role="button" aria-label={`Sort by host ${sortKey() === 'name' ? (sortDirection() === 'asc' ? 'ascending' : 'descending') : ''}`} > @@ -150,19 +151,19 @@ export const DockerHostSummaryTable: Component = (p Containers {renderSortIndicator('running')} handleSort('uptime')} > Uptime {renderSortIndicator('uptime')} handleSort('lastSeen')} > Last Update {renderSortIndicator('lastSeen')} handleSort('agent')} > Agent {renderSortIndicator('agent')} @@ -174,6 +175,8 @@ export const DockerHostSummaryTable: Component = (p {(summary) => { const selected = props.selectedHostId() === summary.host.id; const online = isHostOnline(summary.host); + const uptimeLabel = summary.uptimeSeconds ? formatUptime(summary.uptimeSeconds) : '—'; + const agentLabel = summary.host.agentVersion ? `v${summary.host.agentVersion}` : '—'; const rowStyle = () => { const styles: Record = {}; @@ -206,14 +209,16 @@ export const DockerHostSummaryTable: Component = (p return className; }; + const agentOutdated = isAgentOutdated(summary.host.agentVersion); + return (
-
+
+
{summary.host.displayName} @@ -228,11 +233,51 @@ export const DockerHostSummaryTable: Component = (p
+
+
+ Uptime: + {uptimeLabel} +
+
+ Last: + + {summary.lastSeenRelative} + + + {summary.lastSeenAbsolute} + + + +
+
+ Agent: + + {agentLabel} + + + + Update recommended + + +
+ +
+ Interval: + {summary.host.intervalSeconds}s +
+
+
+ {renderDockerStatusBadge(summary.host.status)} + —}> = (p /> + —}> = (p /> -
+
+
= (p />
+ + +
-
+ ); }; diff --git a/frontend-modern/src/components/shared/ScrollableTable.tsx b/frontend-modern/src/components/shared/ScrollableTable.tsx index 4100d9b..387cff7 100644 --- a/frontend-modern/src/components/shared/ScrollableTable.tsx +++ b/frontend-modern/src/components/shared/ScrollableTable.tsx @@ -1,4 +1,4 @@ -import { Component, JSX, createSignal, createEffect, onMount } from 'solid-js'; +import { Component, JSX, createSignal, createEffect, onMount, onCleanup } from 'solid-js'; import { Show } from 'solid-js'; interface ScrollableTableProps { @@ -23,6 +23,9 @@ export const ScrollableTable: Component = (props) => { onMount(() => { checkScroll(); window.addEventListener('resize', checkScroll); + onCleanup(() => { + window.removeEventListener('resize', checkScroll); + }); }); createEffect(() => { @@ -43,7 +46,7 @@ export const ScrollableTable: Component = (props) => {