diff --git a/admin/app/controllers/system_controller.ts b/admin/app/controllers/system_controller.ts index fbc872a..309301a 100644 --- a/admin/app/controllers/system_controller.ts +++ b/admin/app/controllers/system_controller.ts @@ -113,6 +113,10 @@ export default class SystemController { return await this.systemService.subscribeToReleaseNotes(reqData.email); } + async getDiskStatus({}: HttpContext) { + return await this.systemService.getDiskStatus(); + } + async getDebugInfo({}: HttpContext) { const debugInfo = await this.systemService.getDebugInfo() return { debugInfo } diff --git a/admin/app/services/system_service.ts b/admin/app/services/system_service.ts index 84157af..ee9977f 100644 --- a/admin/app/services/system_service.ts +++ b/admin/app/services/system_service.ts @@ -521,6 +521,59 @@ export class SystemService { return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i] } + async getDiskStatus(): Promise<{ + level: 'none' | 'warning' | 'critical' + threshold: number + highestUsage: number + diskName: string + }> { + try { + const diskInfoRawString = await getFile( + path.join(process.cwd(), SystemService.diskInfoFile), + 'string' + ) + + const diskInfo = ( + diskInfoRawString + ? JSON.parse(diskInfoRawString.toString()) + : { diskLayout: { blockdevices: [] }, fsSize: [] } + ) as NomadDiskInfoRaw + + const disks = this.calculateDiskUsage(diskInfo) + + const warningStr = await KVStore.getValue('disk.warningThreshold') + const criticalStr = await KVStore.getValue('disk.criticalThreshold') + const warningThreshold = warningStr ? Number(warningStr) : 85 + const criticalThreshold = criticalStr ? Number(criticalStr) : 95 + + let highestUsage = 0 + let diskName = '' + + for (const disk of disks) { + if (disk.percentUsed > highestUsage) { + highestUsage = disk.percentUsed + diskName = disk.name + } + } + + let level: 'none' | 'warning' | 'critical' = 'none' + let threshold = 0 + + if (highestUsage >= criticalThreshold) { + level = 'critical' + threshold = criticalThreshold + } else if (highestUsage >= warningThreshold) { + level = 'warning' + threshold = warningThreshold + } + + return { level, threshold, highestUsage, diskName } + } catch (error) { + logger.error('Error getting disk status:', error) + return { level: 'none', threshold: 0, highestUsage: 0, diskName: '' } + } + } + async updateSetting(key: KVStoreKey, value: any): Promise { if ((value === '' || value === undefined || value === null) && KV_STORE_SCHEMA[key] === 'string') { await KVStore.clearValue(key) diff --git a/admin/constants/kv_store.ts b/admin/constants/kv_store.ts index 69872ff..1012d24 100644 --- a/admin/constants/kv_store.ts +++ b/admin/constants/kv_store.ts @@ -1,3 +1,3 @@ import { KVStoreKey } from "../types/kv_store.js"; -export const SETTINGS_KEYS: KVStoreKey[] = ['chat.suggestionsEnabled', 'chat.lastModel', 'ui.hasVisitedEasySetup', 'ui.theme', 'system.earlyAccess', 'ai.assistantCustomName']; \ No newline at end of file +export const SETTINGS_KEYS: KVStoreKey[] = ['chat.suggestionsEnabled', 'chat.lastModel', 'ui.hasVisitedEasySetup', 'ui.theme', 'system.earlyAccess', 'ai.assistantCustomName', 'disk.warningThreshold', 'disk.criticalThreshold']; \ No newline at end of file diff --git a/admin/inertia/components/DiskAlertBanner.tsx b/admin/inertia/components/DiskAlertBanner.tsx new file mode 100644 index 0000000..6eeb925 --- /dev/null +++ b/admin/inertia/components/DiskAlertBanner.tsx @@ -0,0 +1,23 @@ +import Alert from './Alert' +import { useDiskAlert } from '~/hooks/useDiskAlert' + +export default function DiskAlertBanner() { + const { diskStatus, shouldShow, dismiss } = useDiskAlert() + + if (!shouldShow || !diskStatus) return null + + const isWarning = diskStatus.level === 'warning' + + return ( +
+ +
+ ) +} diff --git a/admin/inertia/hooks/useDiskAlert.ts b/admin/inertia/hooks/useDiskAlert.ts new file mode 100644 index 0000000..799214d --- /dev/null +++ b/admin/inertia/hooks/useDiskAlert.ts @@ -0,0 +1,53 @@ +import { useQuery } from '@tanstack/react-query' +import { useState, useMemo } from 'react' +import api from '~/lib/api' + +type DiskAlertLevel = 'none' | 'warning' | 'critical' + +interface DiskStatusResponse { + level: DiskAlertLevel + threshold: number + highestUsage: number + diskName: string +} + +const DISMISSED_KEY = 'nomad:disk-alert-dismissed-level' + +export function useDiskAlert() { + const [dismissedLevel, setDismissedLevel] = useState(() => { + try { + return localStorage.getItem(DISMISSED_KEY) as DiskAlertLevel | null + } catch { + return null + } + }) + + const { data: diskStatus } = useQuery({ + queryKey: ['disk-status'], + queryFn: async () => await api.getDiskStatus(), + refetchInterval: 45000, + }) + + const shouldShow = useMemo(() => { + if (!diskStatus || diskStatus.level === 'none') return false + if (!dismissedLevel) return true + // Exibe novamente se o nĂ­vel piorou + if (dismissedLevel === 'warning' && diskStatus.level === 'critical') return true + return false + }, [diskStatus, dismissedLevel]) + + const dismiss = () => { + if (diskStatus) { + setDismissedLevel(diskStatus.level) + try { + localStorage.setItem(DISMISSED_KEY, diskStatus.level) + } catch {} + } + } + + return { + diskStatus, + shouldShow, + dismiss, + } +} diff --git a/admin/inertia/layouts/AppLayout.tsx b/admin/inertia/layouts/AppLayout.tsx index 1d6a4ed..41ff434 100644 --- a/admin/inertia/layouts/AppLayout.tsx +++ b/admin/inertia/layouts/AppLayout.tsx @@ -7,6 +7,7 @@ import { SERVICE_NAMES } from '../../constants/service_names' import { Link } from '@inertiajs/react' import { IconArrowLeft } from '@tabler/icons-react' import classNames from 'classnames' +import DiskAlertBanner from '~/components/DiskAlertBanner' export default function AppLayout({ children }: { children: React.ReactNode }) { const [isChatOpen, setIsChatOpen] = useState(false) @@ -33,6 +34,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) { "text-desert-green font-semibold h-[1.5px] bg-desert-green border-none", window.location.pathname !== '/home' ? "mt-12 md:mt-0" : "mt-0" )} /> +
{children}