diff --git a/admin/app/controllers/system_controller.ts b/admin/app/controllers/system_controller.ts index 0c3e1ad..f22f125 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..b1b6d67 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', 'ui.language']; \ 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..44ff5bd --- /dev/null +++ b/admin/inertia/components/DiskAlertBanner.tsx @@ -0,0 +1,28 @@ +import { useTranslation } from 'react-i18next' +import Alert from './Alert' +import { useDiskAlert } from '~/hooks/useDiskAlert' + +export default function DiskAlertBanner() { + const { t } = useTranslation('common') + 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..d384762 100644 --- a/admin/inertia/layouts/AppLayout.tsx +++ b/admin/inertia/layouts/AppLayout.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { useTranslation } from 'react-i18next' import Footer from '~/components/Footer' import ChatButton from '~/components/chat/ChatButton' import ChatModal from '~/components/chat/ChatModal' @@ -7,8 +8,10 @@ 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 { t } = useTranslation('layout') const [isChatOpen, setIsChatOpen] = useState(false) const aiAssistantInstalled = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA) @@ -18,7 +21,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) { window.location.pathname !== '/home' && ( -

Back to Home

+

{t('nav.backToHome')}

)}
(window.location.href = '/home')} > Project Nomad Logo -

Command Center

+

{t('header.commandCenter')}


+
{children}