From 634c264f26a836c2d4c371cd28ab6049d7149b54 Mon Sep 17 00:00:00 2001 From: diogo464 Date: Wed, 13 Aug 2025 12:07:07 +0100 Subject: Implement real-time storage usage UI with two-color progress bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add /api/storage endpoint that fetches live data from fctdrive drive-size and df commands - Implement 10-second caching to avoid excessive system calls - Create StorageUsage component with two-color bar showing active drive usage vs total disk usage - Update drive pages to fetch and pass storage data server-side - Replace hardcoded storage values with real system data - Display active drive usage (blue), other disk usage (gray), and available space 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- frontend/app/api/storage/route.ts | 80 ++++++++++++++++++++++ frontend/app/drive/[...path]/page.tsx | 29 +++++++- frontend/app/drive/page.tsx | 29 +++++++- frontend/components/drive/DriveDirectoryClient.tsx | 30 +++----- frontend/components/drive/DriveDirectoryView.tsx | 11 ++- frontend/components/drive/StorageUsage.tsx | 78 +++++++++++++++++++++ 6 files changed, 233 insertions(+), 24 deletions(-) create mode 100644 frontend/app/api/storage/route.ts create mode 100644 frontend/components/drive/StorageUsage.tsx diff --git a/frontend/app/api/storage/route.ts b/frontend/app/api/storage/route.ts new file mode 100644 index 0000000..66f2817 --- /dev/null +++ b/frontend/app/api/storage/route.ts @@ -0,0 +1,80 @@ +import { NextResponse } from 'next/server' +import { exec } from 'child_process' +import { promisify } from 'util' + +const execAsync = promisify(exec) + +// Cache for 10 seconds +let cacheTimestamp = 0 +let cachedData: StorageData | null = null + +interface StorageData { + activeDriveUsage: number + totalDiskCapacity: number + totalDiskUsed: number + availableDisk: number +} + +async function fetchStorageData(): Promise { + const now = Date.now() + + // Return cached data if less than 10 seconds old + if (cachedData && now - cacheTimestamp < 10000) { + return cachedData + } + + try { + // Get active drive usage in bytes + const { stdout: driveSize } = await execAsync('fctdrive drive-size') + const activeDriveUsage = parseInt(driveSize.trim()) || 0 + + // Get disk usage in bytes + const fctdrivePath = process.env.FCTDRIVE_PATH || '/home/diogo464/dev/fctdrive' + const { stdout: dfOutput } = await execAsync(`df -B1 "${fctdrivePath}"`) + + // Parse df output - second line contains the data + const lines = dfOutput.trim().split('\n') + const dataLine = lines[1] // Skip header line + const columns = dataLine.split(/\s+/) + + const totalDiskCapacity = parseInt(columns[1]) || 0 + const totalDiskUsed = parseInt(columns[2]) || 0 + const availableDisk = parseInt(columns[3]) || 0 + + const data: StorageData = { + activeDriveUsage, + totalDiskCapacity, + totalDiskUsed, + availableDisk + } + + // Update cache + cachedData = data + cacheTimestamp = now + + return data + } catch (error) { + console.error('Failed to fetch storage data:', error) + + // Return zeros on error as requested + return { + activeDriveUsage: 0, + totalDiskCapacity: 0, + totalDiskUsed: 0, + availableDisk: 0 + } + } +} + +export async function GET() { + try { + const data = await fetchStorageData() + return NextResponse.json(data) + } catch (error) { + console.error('Storage API error:', error) + return NextResponse.json( + { error: 'Failed to fetch storage data' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/frontend/app/drive/[...path]/page.tsx b/frontend/app/drive/[...path]/page.tsx index 8380a30..bfb65ad 100644 --- a/frontend/app/drive/[...path]/page.tsx +++ b/frontend/app/drive/[...path]/page.tsx @@ -1,6 +1,27 @@ import { DriveDirectoryView } from "@/components/drive/DriveDirectoryView" import { Drive_ls } from "@/lib/drive_server" +async function fetchStorageData() { + try { + const response = await fetch(`${process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:3000' : ''}/api/storage`, { + cache: 'no-store' + }) + if (!response.ok) { + throw new Error('Failed to fetch storage data') + } + return await response.json() + } catch (error) { + console.error('Failed to fetch storage data:', error) + // Return zeros on error as requested + return { + activeDriveUsage: 0, + totalDiskCapacity: 0, + totalDiskUsed: 0, + availableDisk: 0 + } + } +} + export default async function DriveDirectoryPage({ params, }: { @@ -11,6 +32,10 @@ export default async function DriveDirectoryPage({ const decodedSegments = pathSegments?.map(segment => decodeURIComponent(segment)) || [] const currentPath = '/' + decodedSegments.join('/') - const files = await Drive_ls(currentPath, false) - return + const [files, storageData] = await Promise.all([ + Drive_ls(currentPath, false), + fetchStorageData() + ]) + + return } \ No newline at end of file diff --git a/frontend/app/drive/page.tsx b/frontend/app/drive/page.tsx index 0e3fd0c..ea5846e 100644 --- a/frontend/app/drive/page.tsx +++ b/frontend/app/drive/page.tsx @@ -1,7 +1,32 @@ import { DriveDirectoryView } from "@/components/drive/DriveDirectoryView" import { Drive_ls } from "@/lib/drive_server" +async function fetchStorageData() { + try { + const response = await fetch(`${process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:3000' : ''}/api/storage`, { + cache: 'no-store' + }) + if (!response.ok) { + throw new Error('Failed to fetch storage data') + } + return await response.json() + } catch (error) { + console.error('Failed to fetch storage data:', error) + // Return zeros on error as requested + return { + activeDriveUsage: 0, + totalDiskCapacity: 0, + totalDiskUsed: 0, + availableDisk: 0 + } + } +} + export default async function DriveRootPage() { - const files = await Drive_ls("/", false) - return + const [files, storageData] = await Promise.all([ + Drive_ls("/", false), + fetchStorageData() + ]) + + return } \ No newline at end of file diff --git a/frontend/components/drive/DriveDirectoryClient.tsx b/frontend/components/drive/DriveDirectoryClient.tsx index c405341..6877b9b 100644 --- a/frontend/components/drive/DriveDirectoryClient.tsx +++ b/frontend/components/drive/DriveDirectoryClient.tsx @@ -21,7 +21,6 @@ import { } from "lucide-react" import { Button } from "@/components/ui/button" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { Progress } from "@/components/ui/progress" import { DropdownMenu, DropdownMenuContent, @@ -37,6 +36,7 @@ import { toast } from "@/hooks/use-toast" import { DriveLsEntry } from "@/lib/drive_types" import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants" import { DriveMoveDialog } from "./DriveMoveDialog" +import { StorageUsage } from "./StorageUsage" function formatFileSize(bytes: number): string { if (bytes === 0) return "0 Bytes" @@ -67,13 +67,21 @@ interface Breadcrumb { path: string } +interface StorageData { + activeDriveUsage: number + totalDiskCapacity: number + totalDiskUsed: number + availableDisk: number +} + interface DriveDirectoryClientProps { path: string files: DriveLsEntry[] breadcrumbs: Breadcrumb[] + storageData: StorageData } -export function DriveDirectoryClient({ path, files, breadcrumbs }: DriveDirectoryClientProps) { +export function DriveDirectoryClient({ path, files, breadcrumbs, storageData }: DriveDirectoryClientProps) { const [selectedFiles, setSelectedFiles] = useState>(new Set()) const [renameDialogOpen, setRenameDialogOpen] = useState(false) const [infoDialogOpen, setInfoDialogOpen] = useState(false) @@ -87,10 +95,6 @@ export function DriveDirectoryClient({ path, files, breadcrumbs }: DriveDirector const [isLoggedIn, setIsLoggedIn] = useState(true) // Mock logged in state - const maxStorage = 1073741824 // 1GB - const usedStorage = 0 // TODO: Calculate from files if needed - const storagePercentage = (usedStorage / maxStorage) * 100 - const toggleFileSelection = (filePath: string) => { const newSelected = new Set(selectedFiles) if (newSelected.has(filePath)) { @@ -438,19 +442,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs }: DriveDirector {/* Storage Info */} -
-
- Storage Usage - - {formatFileSize(usedStorage)} of {formatFileSize(maxStorage)} used - -
- -
- {storagePercentage.toFixed(1)}% used - {formatFileSize(maxStorage - usedStorage)} available -
-
+ {/* Bulk Actions */} {selectedFiles.size > 0 && ( diff --git a/frontend/components/drive/DriveDirectoryView.tsx b/frontend/components/drive/DriveDirectoryView.tsx index 20ed9d8..d93dce8 100644 --- a/frontend/components/drive/DriveDirectoryView.tsx +++ b/frontend/components/drive/DriveDirectoryView.tsx @@ -1,9 +1,17 @@ import { DriveLsEntry } from "@/lib/drive_types" import { DriveDirectoryClient } from "./DriveDirectoryClient" +interface StorageData { + activeDriveUsage: number + totalDiskCapacity: number + totalDiskUsed: number + availableDisk: number +} + interface DriveDirectoryViewProps { path: string files: DriveLsEntry[] + storageData: StorageData } // Generate breadcrumbs from path @@ -41,7 +49,7 @@ function sortFiles(files: DriveLsEntry[]): DriveLsEntry[] { }); } -export function DriveDirectoryView({ path, files }: DriveDirectoryViewProps) { +export function DriveDirectoryView({ path, files, storageData }: DriveDirectoryViewProps) { const sortedFiles = sortFiles(files) const breadcrumbs = generateBreadcrumbs(path) @@ -50,6 +58,7 @@ export function DriveDirectoryView({ path, files }: DriveDirectoryViewProps) { path={path} files={sortedFiles} breadcrumbs={breadcrumbs} + storageData={storageData} /> ) } \ No newline at end of file diff --git a/frontend/components/drive/StorageUsage.tsx b/frontend/components/drive/StorageUsage.tsx new file mode 100644 index 0000000..a5a1d68 --- /dev/null +++ b/frontend/components/drive/StorageUsage.tsx @@ -0,0 +1,78 @@ +interface StorageData { + activeDriveUsage: number + totalDiskCapacity: number + totalDiskUsed: number + availableDisk: number +} + +interface StorageUsageProps { + data: StorageData +} + +function formatFileSize(bytes: number): string { + if (bytes === 0) return "0 Bytes" + const k = 1024 + const sizes = ["Bytes", "KB", "MB", "GB", "TB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] +} + +export function StorageUsage({ data }: StorageUsageProps) { + // Safety check for undefined data + if (!data) { + return ( +
+
+ Storage Usage + Loading... +
+
+
+ ) + } + + const { activeDriveUsage, totalDiskCapacity, totalDiskUsed } = data + + // Calculate percentages based on total disk capacity + const activeDrivePercentage = totalDiskCapacity > 0 ? (activeDriveUsage / totalDiskCapacity) * 100 : 0 + const totalUsedPercentage = totalDiskCapacity > 0 ? (totalDiskUsed / totalDiskCapacity) * 100 : 0 + + return ( +
+
+ Storage Usage + + {formatFileSize(activeDriveUsage)} active / {formatFileSize(totalDiskUsed)} total of {formatFileSize(totalDiskCapacity)} + +
+ + {/* Custom two-color progress bar */} +
+ {/* Total disk usage (gray background layer) */} +
+ {/* Active drive usage (blue foreground layer) */} +
+
+ +
+
+
+
+ Active Drive: {formatFileSize(activeDriveUsage)} +
+
+
+ Other Usage: {formatFileSize(totalDiskUsed - activeDriveUsage)} +
+
+ {formatFileSize(totalDiskCapacity - totalDiskUsed)} available +
+
+ ) +} \ No newline at end of file -- cgit