diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index 231cf9c..b7ade0f 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -63,6 +63,9 @@ Phase 1: Jellyfin media index, SSH-based remote filesystem inspection, server mo - The Users tab may open a read-only detail drawer for a selected user, but any communication actions in that drawer should remain clearly disabled/placeholders until the workflow is implemented. - The frontend shell should use a polished two-row header with branding on the left, user/logout controls on the right, and primary navigation in a dedicated tab row beneath. - The frontend shell and primary pages should remain responsive and mobile-safe, with compact navigation, stacked controls on narrow screens, and reduced table column density where needed. +- Dense management surfaces such as Actions and Settings should prefer compact tabbed or split-pane layouts over long single-column forms when that improves scanning and editing speed. +- Tab rails and subtabs should feel like a polished admin console: compact, clearly selected, and visually consistent across the app. +- When a setting subsection is not applicable to the currently selected mode or service, the UI should hide those fields rather than leaving them visible but disabled. - The frontend should hydrate the API bearer token from persisted OIDC user storage immediately on reload so early requests do not race the auth provider lifecycle. - The Media tab should persist its search/filter/sort/pagination state across reloads and tab switches. - The File Browser should persist its current directory and selected file across reloads and tab switches. @@ -229,4 +232,20 @@ Phase 1: Jellyfin media index, SSH-based remote filesystem inspection, server mo - 2026-05-06: Saved SSH keys now surface a derived public key, fingerprint, and per-key machine usage count in the Settings UI for easier auditing. - 2026-05-06: The dev Compose stack now starts without any SSH key material at all unless a user later configures remote SSH machines. - 2026-05-06: The Settings page now exposes a protected local-database reset flow that requires several explicit acknowledgements and a typed confirmation phrase before it can delete the cached app databases. +- 2026-05-06: The Actions page was redesigned into a compact tabbed workspace with a left tab rail of saved actions, and both new-action creation and editing now open in popups instead of inline forms. +- 2026-05-06: Closing an edited Action popup now warns before discarding unsaved changes. +- 2026-05-06: The Settings page was reworked into a compact tabbed layout with separate Machines, SSH Keys, and Danger Zone sections, and irrelevant machine/service fields now hide when that mode or service is not selected. +- 2026-05-06: The Settings machine list was converted into a compact table with popup editing so the page no longer repeats the full machine form for every entry. +- 2026-05-06: The Monitoring page was reworked into an overview tab plus one tab per configured machine, so each machine gets its own monitoring page. +- 2026-05-06: The Actions page now shows a visible empty-state container when no action is selected, and the Monitoring tabs were returned to the app's standard tab styling. +- 2026-05-06: The Monitoring tab spacing was tightened and the Applications page was updated to match the app's standard card-and-tab design. +- 2026-05-06: The dashboard library stats were moved into the Jellyfin Applications tab above the media table, and the dashboard activity panel was relabeled as Jellyfin activity. +- 2026-05-06: The dashboard, Monitoring overview, Applications, and File Browser layouts were normalized around shared titled containers, with Monitoring showing the fleet overview before machine tabs and File Browser using machine tabs with Browser/Media info/Jobs stacked in each machine view. +- 2026-05-06: The Applications and Monitoring tab containers were refactored into a reusable bordered tab-card pattern so the active tab's content sits inside the same card as the tab rail. +- 2026-05-06: The Settings Machines tab was changed from a repeated form layout to a left-hand machine list with a right-hand details panel and popup editing. +- 2026-05-06: The File Browser was reworked into Browser / Media info / Jobs subtabs. +- 2026-05-06: The app shell received a small density pass that tightened container padding and tab widths to make the whole site feel more compact. +- 2026-05-06: The tab rails across Actions, Monitoring, Settings, and Files were restyled to be more enterprise-console-like with compact pills, clearer active states, and reduced visual noise. - 2026-05-06: Added an Actions tab for saved server tasks, with backend persistence, per-task run history, and support for shell/Python task types on either local or SSH machines. +- 2026-05-06: Reusable dialog footers now keep cancel on the left and confirm on the right, and hover edit buttons now appear on the right edge of editable list rows in Actions and Settings. +- 2026-05-06: Library stats, Jellyfin activity, and Monitoring overview now use shared section-container patterns so subcontainers stay consistent across the app. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 46f4f7c..d577de3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -161,7 +161,7 @@ function Shell({ minHeight: 44, py: 1, px: 1.5, - minWidth: { xs: 96, md: 120 }, + minWidth: { xs: 88, md: 108 }, textTransform: "none", fontWeight: 600, }, @@ -199,7 +199,7 @@ function Shell({ } /> diff --git a/frontend/src/components/NowPlaying.tsx b/frontend/src/components/NowPlaying.tsx index 58ac31c..13fe39f 100644 --- a/frontend/src/components/NowPlaying.tsx +++ b/frontend/src/components/NowPlaying.tsx @@ -1,4 +1,3 @@ -import { Card, CardContent } from "@mui/material"; import type { NowPlayingSession } from "../types"; import { SessionActivityPanel } from "./SessionActivityPanel"; @@ -9,14 +8,10 @@ interface Props { export function NowPlaying({ sessions, onSelectSession }: Props) { return ( - - - - - + ); } diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 016e844..00fb465 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,88 +1,37 @@ -import { Box, Divider, Grid, Stack, Typography } from "@mui/material"; +import { Stack } from "@mui/material"; import { useNavigate } from "react-router-dom"; -import { - useCounts, - useLibraries, - useActivity, - useMonitoringOverview, -} from "../hooks/useDashboard"; +import { useActivity, useMonitoringOverview } from "../hooks/useDashboard"; import { NowPlaying } from "../components/NowPlaying"; -import { MetricCard } from "../components/MetricCard"; -import { LibraryOverview } from "../components/LibraryOverview"; import { MonitoringOverviewTable } from "../components/MonitoringOverviewTable"; +import { SectionCard } from "../components/SectionCard"; export function Dashboard() { const navigate = useNavigate(); - const { data: counts } = useCounts(); - const { data: libraries } = useLibraries(); const { data: activity } = useActivity(); const { data: monitoringOverview } = useMonitoringOverview(); return ( - - - - Activity - - {activity && ( + + + {activity ? ( navigate(`/users?user=${encodeURIComponent(session.user)}`) } /> - )} - + ) : null} + - - - - - Monitoring Overview - - - - - - - - - Library Stats - - {counts && ( - - - - - - - - - - - - - - - )} - {libraries && } - + + + ); } diff --git a/frontend/src/pages/FileBrowser.impl.tsx b/frontend/src/pages/FileBrowser.impl.tsx index 8a7253a..c9abfaf 100644 --- a/frontend/src/pages/FileBrowser.impl.tsx +++ b/frontend/src/pages/FileBrowser.impl.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { useSearchParams } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { DataGrid } from "@mui/x-data-grid"; import type { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import { @@ -15,6 +15,7 @@ import { MenuItem, Select, Stack, + Tab, TextField, Typography, useMediaQuery, @@ -27,6 +28,8 @@ import { } from "../hooks/useFiles"; import { usePersistentState } from "../hooks/usePersistentState"; import { useMonitoringSettings } from "../hooks/useSettings"; +import { SectionCard } from "../components/SectionCard"; +import { TabbedCard } from "../components/TabbedCard"; interface DisplayRow { id: string; @@ -596,6 +599,7 @@ export function FileBrowser() { ); const { currentDir, pathInput, selectedPath, selectedJob } = browserState; const selectedMachineId = searchParams.get("machine_id") || initialMachineId; + const navigateToSettings = useNavigate(); const updateBrowserState = (patch: Partial) => setBrowserState((current) => ({ ...current, ...patch })); @@ -685,194 +689,237 @@ export function FileBrowser() { const selectedTemplate = templates?.find((t) => t.key === selectedJob); return ( - + File Browser - - Machine - - - - - - updateBrowserState({ pathInput: e.target.value })} - onKeyDown={handlePathSubmit} - /> - - - - - - Current: {currentDir}{" "} - {selectedPath ? `| Selected: ${selectedPath}` : ""}{" "} - {listing ? `| Entries: ${listing.count}` : ""} - - - {error && {String(error)}} - - - { - const row = params.row as DisplayRow; - if (row.type === "dir" || row.type === "up") navigate(row.path); - else - updateBrowserState({ - selectedPath: row.path, - currentDir, - pathInput: row.path, - }); - }} + variant="outlined" + size="small" /> - + - {selectedPath && isVideoFile(selectedPath) && ( - - - {ffprobeError ? ( - - {String(ffprobeError)} - - ) : ffprobeLoading && !ffprobeData ? ( - - Loading ffprobe data... - - ) : ffprobeData ? ( - - ) : ( - - No ffprobe data available. - - )} - - - )} - - {selectedPath && templates && templates.length > 0 && ( - - - - Jobs - - - - - Job template - - - - + 0 ? selectedMachineId : ""} + onChange={setMachine} + tabs={fileMachines.map((machine) => ( + + ))} + > + {fileMachines.length > 0 ? ( + + + - - {selectedTemplate && ( - refetch()} + > + Refresh + + + + Current: {currentDir}{" "} + {selectedPath ? `| Selected: ${selectedPath}` : ""}{" "} + {listing ? `| Entries: ${listing.count}` : ""} + + {error && {String(error)}} + + { + const row = params.row as DisplayRow; + if (row.type === "dir" || row.type === "up") + navigate(row.path); + else + updateBrowserState({ + selectedPath: row.path, + currentDir, + pathInput: row.path, + }); + }} + /> + + + + + + {selectedPath ? ( + isVideoFile(selectedPath) ? ( + ffprobeError ? ( + {String(ffprobeError)} + ) : ffprobeLoading && !ffprobeData ? ( + Loading ffprobe data... + ) : ffprobeData ? ( + + ) : ( + No ffprobe data available. + ) + ) : ( + + Select a video file to view ffprobe details. + + ) + ) : ( + + Select a file in Browser to view ffprobe details. + + )} + + + + {selectedPath && templates && templates.length > 0 ? ( + + + + + Job template + + + + + + + {selectedTemplate && ( + + {selectedTemplate.description} + + )} + + + + {runJob.data && ( + - {selectedTemplate.description} - + Exit: {runJob.data.exit_status} + {"\n"} + {runJob.data.stdout} + {runJob.data.stderr && `\nSTDERR: ${runJob.data.stderr}`} + )} - - - - {runJob.data && ( - + Select a file in Browser to run jobs. + + )} + + + ) : ( + navigateToSettings("/settings")} > - Exit: {runJob.data.exit_status} - {"\n"} - {runJob.data.stdout} - {runJob.data.stderr && `\nSTDERR: ${runJob.data.stderr}`} - - )} - - - )} + Open Settings + + } + > + No file-capable machines are configured yet. + + )} + ); } diff --git a/frontend/src/pages/Monitoring.tsx b/frontend/src/pages/Monitoring.tsx index 830192e..626c836 100644 --- a/frontend/src/pages/Monitoring.tsx +++ b/frontend/src/pages/Monitoring.tsx @@ -1,80 +1,108 @@ -import { Alert, Button, Chip, Stack, Typography } from "@mui/material"; +import { useMemo, useState } from "react"; +import { Alert, Button, Stack, Tab, Typography } from "@mui/material"; +import { useNavigate } from "react-router-dom"; import { useMonitoringMachines, useMonitoringPoller, } from "../hooks/useMonitoring"; +import { useMonitoringOverview } from "../hooks/useDashboard"; import { MachineMonitoringSection } from "../components/MachineMonitoringSection"; -import { useNavigate } from "react-router-dom"; +import { MonitoringOverviewTable } from "../components/MonitoringOverviewTable"; +import { SectionCard } from "../components/SectionCard"; +import { TabbedCard } from "../components/TabbedCard"; + +type MonitoringTab = "overview" | string; export function Monitoring() { - const { data: machines, isLoading, error } = useMonitoringMachines(); + const { data: machines = [], isLoading, error } = useMonitoringMachines(); const { data: poller } = useMonitoringPoller(); + const { data: overview } = useMonitoringOverview(); const navigate = useNavigate(); + const [tab, setTab] = useState("overview"); + + const selectedMachine = useMemo( + () => machines.find((machine) => machine.id === tab) ?? null, + [machines, tab], + ); return ( - + - - Monitoring - {poller && ( - - )} - + + Monitoring + - Each machine below is monitored with its own settings and collector - state, and recent activity is gathered automatically by the backend. + Review fleet health first, then switch to a machine-specific tab. - {error && {String(error)}} - {isLoading && ( + {error ? {String(error)} : null} + {isLoading ? ( Loading monitoring machines... - )} - {!isLoading && (machines?.length ?? 0) === 0 && ( - navigate("/settings")} - > - Open Settings - - } - > - No monitoring machines are configured yet. - - )} + ) : null} - {machines?.map((machine) => ( - - - - ))} + , + ...machines.map((machine) => ( + + )), + ]} + contentSx={{ p: 1.5 }} + > + {tab === "overview" ? ( + + + + Fleet overview + + + {poller?.worker_running + ? `Poller running · ${poller.interval_seconds}s` + : "Poller stopped"} + + + + {!isLoading && machines.length === 0 ? ( + navigate("/settings")} + > + Open Settings + + } + > + No monitoring machines are configured yet. + + ) : overview ? ( + + ) : ( + Loading fleet overview... + )} + + ) : selectedMachine ? ( + + + + ) : null} + ); }