summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-08-13 12:07:07 +0100
committerdiogo464 <[email protected]>2025-08-13 12:07:07 +0100
commit634c264f26a836c2d4c371cd28ab6049d7149b54 (patch)
treec3aa606bdd8a55df9dfcba590aca285c70078ab3
parent1badf419bff94c475b3402c5d598112fa2d685f1 (diff)
Implement real-time storage usage UI with two-color progress bar
- 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 <[email protected]>
-rw-r--r--frontend/app/api/storage/route.ts80
-rw-r--r--frontend/app/drive/[...path]/page.tsx29
-rw-r--r--frontend/app/drive/page.tsx29
-rw-r--r--frontend/components/drive/DriveDirectoryClient.tsx30
-rw-r--r--frontend/components/drive/DriveDirectoryView.tsx11
-rw-r--r--frontend/components/drive/StorageUsage.tsx78
6 files changed, 233 insertions, 24 deletions
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 @@
1import { NextResponse } from 'next/server'
2import { exec } from 'child_process'
3import { promisify } from 'util'
4
5const execAsync = promisify(exec)
6
7// Cache for 10 seconds
8let cacheTimestamp = 0
9let cachedData: StorageData | null = null
10
11interface StorageData {
12 activeDriveUsage: number
13 totalDiskCapacity: number
14 totalDiskUsed: number
15 availableDisk: number
16}
17
18async function fetchStorageData(): Promise<StorageData> {
19 const now = Date.now()
20
21 // Return cached data if less than 10 seconds old
22 if (cachedData && now - cacheTimestamp < 10000) {
23 return cachedData
24 }
25
26 try {
27 // Get active drive usage in bytes
28 const { stdout: driveSize } = await execAsync('fctdrive drive-size')
29 const activeDriveUsage = parseInt(driveSize.trim()) || 0
30
31 // Get disk usage in bytes
32 const fctdrivePath = process.env.FCTDRIVE_PATH || '/home/diogo464/dev/fctdrive'
33 const { stdout: dfOutput } = await execAsync(`df -B1 "${fctdrivePath}"`)
34
35 // Parse df output - second line contains the data
36 const lines = dfOutput.trim().split('\n')
37 const dataLine = lines[1] // Skip header line
38 const columns = dataLine.split(/\s+/)
39
40 const totalDiskCapacity = parseInt(columns[1]) || 0
41 const totalDiskUsed = parseInt(columns[2]) || 0
42 const availableDisk = parseInt(columns[3]) || 0
43
44 const data: StorageData = {
45 activeDriveUsage,
46 totalDiskCapacity,
47 totalDiskUsed,
48 availableDisk
49 }
50
51 // Update cache
52 cachedData = data
53 cacheTimestamp = now
54
55 return data
56 } catch (error) {
57 console.error('Failed to fetch storage data:', error)
58
59 // Return zeros on error as requested
60 return {
61 activeDriveUsage: 0,
62 totalDiskCapacity: 0,
63 totalDiskUsed: 0,
64 availableDisk: 0
65 }
66 }
67}
68
69export async function GET() {
70 try {
71 const data = await fetchStorageData()
72 return NextResponse.json(data)
73 } catch (error) {
74 console.error('Storage API error:', error)
75 return NextResponse.json(
76 { error: 'Failed to fetch storage data' },
77 { status: 500 }
78 )
79 }
80} \ 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 @@
1import { DriveDirectoryView } from "@/components/drive/DriveDirectoryView" 1import { DriveDirectoryView } from "@/components/drive/DriveDirectoryView"
2import { Drive_ls } from "@/lib/drive_server" 2import { Drive_ls } from "@/lib/drive_server"
3 3
4async function fetchStorageData() {
5 try {
6 const response = await fetch(`${process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:3000' : ''}/api/storage`, {
7 cache: 'no-store'
8 })
9 if (!response.ok) {
10 throw new Error('Failed to fetch storage data')
11 }
12 return await response.json()
13 } catch (error) {
14 console.error('Failed to fetch storage data:', error)
15 // Return zeros on error as requested
16 return {
17 activeDriveUsage: 0,
18 totalDiskCapacity: 0,
19 totalDiskUsed: 0,
20 availableDisk: 0
21 }
22 }
23}
24
4export default async function DriveDirectoryPage({ 25export default async function DriveDirectoryPage({
5 params, 26 params,
6}: { 27}: {
@@ -11,6 +32,10 @@ export default async function DriveDirectoryPage({
11 const decodedSegments = pathSegments?.map(segment => decodeURIComponent(segment)) || [] 32 const decodedSegments = pathSegments?.map(segment => decodeURIComponent(segment)) || []
12 const currentPath = '/' + decodedSegments.join('/') 33 const currentPath = '/' + decodedSegments.join('/')
13 34
14 const files = await Drive_ls(currentPath, false) 35 const [files, storageData] = await Promise.all([
15 return <DriveDirectoryView path={currentPath} files={files} /> 36 Drive_ls(currentPath, false),
37 fetchStorageData()
38 ])
39
40 return <DriveDirectoryView path={currentPath} files={files} storageData={storageData} />
16} \ No newline at end of file 41} \ 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 @@
1import { DriveDirectoryView } from "@/components/drive/DriveDirectoryView" 1import { DriveDirectoryView } from "@/components/drive/DriveDirectoryView"
2import { Drive_ls } from "@/lib/drive_server" 2import { Drive_ls } from "@/lib/drive_server"
3 3
4async function fetchStorageData() {
5 try {
6 const response = await fetch(`${process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:3000' : ''}/api/storage`, {
7 cache: 'no-store'
8 })
9 if (!response.ok) {
10 throw new Error('Failed to fetch storage data')
11 }
12 return await response.json()
13 } catch (error) {
14 console.error('Failed to fetch storage data:', error)
15 // Return zeros on error as requested
16 return {
17 activeDriveUsage: 0,
18 totalDiskCapacity: 0,
19 totalDiskUsed: 0,
20 availableDisk: 0
21 }
22 }
23}
24
4export default async function DriveRootPage() { 25export default async function DriveRootPage() {
5 const files = await Drive_ls("/", false) 26 const [files, storageData] = await Promise.all([
6 return <DriveDirectoryView path="/" files={files} /> 27 Drive_ls("/", false),
28 fetchStorageData()
29 ])
30
31 return <DriveDirectoryView path="/" files={files} storageData={storageData} />
7} \ No newline at end of file 32} \ 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 {
21} from "lucide-react" 21} from "lucide-react"
22import { Button } from "@/components/ui/button" 22import { Button } from "@/components/ui/button"
23import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" 23import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
24import { Progress } from "@/components/ui/progress"
25import { 24import {
26 DropdownMenu, 25 DropdownMenu,
27 DropdownMenuContent, 26 DropdownMenuContent,
@@ -37,6 +36,7 @@ import { toast } from "@/hooks/use-toast"
37import { DriveLsEntry } from "@/lib/drive_types" 36import { DriveLsEntry } from "@/lib/drive_types"
38import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants" 37import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants"
39import { DriveMoveDialog } from "./DriveMoveDialog" 38import { DriveMoveDialog } from "./DriveMoveDialog"
39import { StorageUsage } from "./StorageUsage"
40 40
41function formatFileSize(bytes: number): string { 41function formatFileSize(bytes: number): string {
42 if (bytes === 0) return "0 Bytes" 42 if (bytes === 0) return "0 Bytes"
@@ -67,13 +67,21 @@ interface Breadcrumb {
67 path: string 67 path: string
68} 68}
69 69
70interface StorageData {
71 activeDriveUsage: number
72 totalDiskCapacity: number
73 totalDiskUsed: number
74 availableDisk: number
75}
76
70interface DriveDirectoryClientProps { 77interface DriveDirectoryClientProps {
71 path: string 78 path: string
72 files: DriveLsEntry[] 79 files: DriveLsEntry[]
73 breadcrumbs: Breadcrumb[] 80 breadcrumbs: Breadcrumb[]
81 storageData: StorageData
74} 82}
75 83
76export function DriveDirectoryClient({ path, files, breadcrumbs }: DriveDirectoryClientProps) { 84export function DriveDirectoryClient({ path, files, breadcrumbs, storageData }: DriveDirectoryClientProps) {
77 const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set()) 85 const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set())
78 const [renameDialogOpen, setRenameDialogOpen] = useState(false) 86 const [renameDialogOpen, setRenameDialogOpen] = useState(false)
79 const [infoDialogOpen, setInfoDialogOpen] = useState(false) 87 const [infoDialogOpen, setInfoDialogOpen] = useState(false)
@@ -87,10 +95,6 @@ export function DriveDirectoryClient({ path, files, breadcrumbs }: DriveDirector
87 95
88 const [isLoggedIn, setIsLoggedIn] = useState(true) // Mock logged in state 96 const [isLoggedIn, setIsLoggedIn] = useState(true) // Mock logged in state
89 97
90 const maxStorage = 1073741824 // 1GB
91 const usedStorage = 0 // TODO: Calculate from files if needed
92 const storagePercentage = (usedStorage / maxStorage) * 100
93
94 const toggleFileSelection = (filePath: string) => { 98 const toggleFileSelection = (filePath: string) => {
95 const newSelected = new Set(selectedFiles) 99 const newSelected = new Set(selectedFiles)
96 if (newSelected.has(filePath)) { 100 if (newSelected.has(filePath)) {
@@ -438,19 +442,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs }: DriveDirector
438 </div> 442 </div>
439 443
440 {/* Storage Info */} 444 {/* Storage Info */}
441 <div className="bg-card rounded-lg border p-4"> 445 <StorageUsage data={storageData} />
442 <div className="flex items-center justify-between mb-2">
443 <span className="text-sm font-medium">Storage Usage</span>
444 <span className="text-sm text-muted-foreground">
445 {formatFileSize(usedStorage)} of {formatFileSize(maxStorage)} used
446 </span>
447 </div>
448 <Progress value={storagePercentage} className="h-2" />
449 <div className="flex justify-between text-xs text-muted-foreground mt-1">
450 <span>{storagePercentage.toFixed(1)}% used</span>
451 <span>{formatFileSize(maxStorage - usedStorage)} available</span>
452 </div>
453 </div>
454 446
455 {/* Bulk Actions */} 447 {/* Bulk Actions */}
456 {selectedFiles.size > 0 && ( 448 {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 @@
1import { DriveLsEntry } from "@/lib/drive_types" 1import { DriveLsEntry } from "@/lib/drive_types"
2import { DriveDirectoryClient } from "./DriveDirectoryClient" 2import { DriveDirectoryClient } from "./DriveDirectoryClient"
3 3
4interface StorageData {
5 activeDriveUsage: number
6 totalDiskCapacity: number
7 totalDiskUsed: number
8 availableDisk: number
9}
10
4interface DriveDirectoryViewProps { 11interface DriveDirectoryViewProps {
5 path: string 12 path: string
6 files: DriveLsEntry[] 13 files: DriveLsEntry[]
14 storageData: StorageData
7} 15}
8 16
9// Generate breadcrumbs from path 17// Generate breadcrumbs from path
@@ -41,7 +49,7 @@ function sortFiles(files: DriveLsEntry[]): DriveLsEntry[] {
41 }); 49 });
42} 50}
43 51
44export function DriveDirectoryView({ path, files }: DriveDirectoryViewProps) { 52export function DriveDirectoryView({ path, files, storageData }: DriveDirectoryViewProps) {
45 const sortedFiles = sortFiles(files) 53 const sortedFiles = sortFiles(files)
46 const breadcrumbs = generateBreadcrumbs(path) 54 const breadcrumbs = generateBreadcrumbs(path)
47 55
@@ -50,6 +58,7 @@ export function DriveDirectoryView({ path, files }: DriveDirectoryViewProps) {
50 path={path} 58 path={path}
51 files={sortedFiles} 59 files={sortedFiles}
52 breadcrumbs={breadcrumbs} 60 breadcrumbs={breadcrumbs}
61 storageData={storageData}
53 /> 62 />
54 ) 63 )
55} \ No newline at end of file 64} \ 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 @@
1interface StorageData {
2 activeDriveUsage: number
3 totalDiskCapacity: number
4 totalDiskUsed: number
5 availableDisk: number
6}
7
8interface StorageUsageProps {
9 data: StorageData
10}
11
12function formatFileSize(bytes: number): string {
13 if (bytes === 0) return "0 Bytes"
14 const k = 1024
15 const sizes = ["Bytes", "KB", "MB", "GB", "TB"]
16 const i = Math.floor(Math.log(bytes) / Math.log(k))
17 return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
18}
19
20export function StorageUsage({ data }: StorageUsageProps) {
21 // Safety check for undefined data
22 if (!data) {
23 return (
24 <div className="bg-card rounded-lg border p-4">
25 <div className="flex items-center justify-between mb-2">
26 <span className="text-sm font-medium">Storage Usage</span>
27 <span className="text-sm text-muted-foreground">Loading...</span>
28 </div>
29 <div className="w-full h-2 bg-gray-200 rounded-full" />
30 </div>
31 )
32 }
33
34 const { activeDriveUsage, totalDiskCapacity, totalDiskUsed } = data
35
36 // Calculate percentages based on total disk capacity
37 const activeDrivePercentage = totalDiskCapacity > 0 ? (activeDriveUsage / totalDiskCapacity) * 100 : 0
38 const totalUsedPercentage = totalDiskCapacity > 0 ? (totalDiskUsed / totalDiskCapacity) * 100 : 0
39
40 return (
41 <div className="bg-card rounded-lg border p-4">
42 <div className="flex items-center justify-between mb-2">
43 <span className="text-sm font-medium">Storage Usage</span>
44 <span className="text-sm text-muted-foreground">
45 {formatFileSize(activeDriveUsage)} active / {formatFileSize(totalDiskUsed)} total of {formatFileSize(totalDiskCapacity)}
46 </span>
47 </div>
48
49 {/* Custom two-color progress bar */}
50 <div className="relative w-full h-2 bg-gray-200 rounded-full overflow-hidden">
51 {/* Total disk usage (gray background layer) */}
52 <div
53 className="absolute top-0 left-0 h-full bg-gray-400 rounded-full transition-all duration-300"
54 style={{ width: `${Math.min(totalUsedPercentage, 100)}%` }}
55 />
56 {/* Active drive usage (blue foreground layer) */}
57 <div
58 className="absolute top-0 left-0 h-full bg-blue-600 rounded-full transition-all duration-300"
59 style={{ width: `${Math.min(activeDrivePercentage, 100)}%` }}
60 />
61 </div>
62
63 <div className="flex justify-between text-xs text-muted-foreground mt-2">
64 <div className="flex items-center gap-4">
65 <div className="flex items-center gap-1">
66 <div className="w-2 h-2 bg-blue-600 rounded-full" />
67 <span>Active Drive: {formatFileSize(activeDriveUsage)}</span>
68 </div>
69 <div className="flex items-center gap-1">
70 <div className="w-2 h-2 bg-gray-400 rounded-full" />
71 <span>Other Usage: {formatFileSize(totalDiskUsed - activeDriveUsage)}</span>
72 </div>
73 </div>
74 <span>{formatFileSize(totalDiskCapacity - totalDiskUsed)} available</span>
75 </div>
76 </div>
77 )
78} \ No newline at end of file