mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
feat(system): adicionar notificações de espaço em disco
Adiciona monitoramento de disco com alertas globais quando o uso ultrapassa thresholds configuráveis (85% warning, 95% critical). - Novo endpoint GET /api/system/disk-status - getDiskStatus() no SystemService com thresholds via KVStore - Hook useDiskAlert com polling de 45s e dismiss via localStorage - DiskAlertBanner no AppLayout visível em todas as páginas - Banner reaparece se o nível de alerta piorar Closes #485
This commit is contained in:
parent
f004c002a7
commit
f9583ff3b8
|
|
@ -113,6 +113,10 @@ export default class SystemController {
|
||||||
return await this.systemService.subscribeToReleaseNotes(reqData.email);
|
return await this.systemService.subscribeToReleaseNotes(reqData.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDiskStatus({}: HttpContext) {
|
||||||
|
return await this.systemService.getDiskStatus();
|
||||||
|
}
|
||||||
|
|
||||||
async getDebugInfo({}: HttpContext) {
|
async getDebugInfo({}: HttpContext) {
|
||||||
const debugInfo = await this.systemService.getDebugInfo()
|
const debugInfo = await this.systemService.getDebugInfo()
|
||||||
return { debugInfo }
|
return { debugInfo }
|
||||||
|
|
|
||||||
|
|
@ -521,6 +521,59 @@ export class SystemService {
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i]
|
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<void> {
|
async updateSetting(key: KVStoreKey, value: any): Promise<void> {
|
||||||
if ((value === '' || value === undefined || value === null) && KV_STORE_SCHEMA[key] === 'string') {
|
if ((value === '' || value === undefined || value === null) && KV_STORE_SCHEMA[key] === 'string') {
|
||||||
await KVStore.clearValue(key)
|
await KVStore.clearValue(key)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
import { KVStoreKey } from "../types/kv_store.js";
|
import { KVStoreKey } from "../types/kv_store.js";
|
||||||
|
|
||||||
export const SETTINGS_KEYS: KVStoreKey[] = ['chat.suggestionsEnabled', 'chat.lastModel', 'ui.hasVisitedEasySetup', 'ui.theme', 'system.earlyAccess', 'ai.assistantCustomName'];
|
export const SETTINGS_KEYS: KVStoreKey[] = ['chat.suggestionsEnabled', 'chat.lastModel', 'ui.hasVisitedEasySetup', 'ui.theme', 'system.earlyAccess', 'ai.assistantCustomName', 'disk.warningThreshold', 'disk.criticalThreshold'];
|
||||||
23
admin/inertia/components/DiskAlertBanner.tsx
Normal file
23
admin/inertia/components/DiskAlertBanner.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div className="px-4 pt-4">
|
||||||
|
<Alert
|
||||||
|
type={isWarning ? 'warning' : 'error'}
|
||||||
|
variant="bordered"
|
||||||
|
title={isWarning ? 'Disk Space Running Low' : 'Disk Space Critically Low'}
|
||||||
|
message={`Disk "${diskStatus.diskName}" is ${diskStatus.highestUsage.toFixed(1)}% full. Free up space to avoid issues with downloads and services.`}
|
||||||
|
dismissible={true}
|
||||||
|
onDismiss={dismiss}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
53
admin/inertia/hooks/useDiskAlert.ts
Normal file
53
admin/inertia/hooks/useDiskAlert.ts
Normal file
|
|
@ -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<DiskAlertLevel | null>(() => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(DISMISSED_KEY) as DiskAlertLevel | null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: diskStatus } = useQuery<DiskStatusResponse | undefined>({
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import { SERVICE_NAMES } from '../../constants/service_names'
|
||||||
import { Link } from '@inertiajs/react'
|
import { Link } from '@inertiajs/react'
|
||||||
import { IconArrowLeft } from '@tabler/icons-react'
|
import { IconArrowLeft } from '@tabler/icons-react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import DiskAlertBanner from '~/components/DiskAlertBanner'
|
||||||
|
|
||||||
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||||
const [isChatOpen, setIsChatOpen] = useState(false)
|
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",
|
"text-desert-green font-semibold h-[1.5px] bg-desert-green border-none",
|
||||||
window.location.pathname !== '/home' ? "mt-12 md:mt-0" : "mt-0"
|
window.location.pathname !== '/home' ? "mt-12 md:mt-0" : "mt-0"
|
||||||
)} />
|
)} />
|
||||||
|
<DiskAlertBanner />
|
||||||
<div className="flex-1 w-full bg-desert">{children}</div>
|
<div className="flex-1 w-full bg-desert">{children}</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,18 @@ class API {
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDiskStatus() {
|
||||||
|
return catchInternal(async () => {
|
||||||
|
const response = await this.client.get<{
|
||||||
|
level: 'none' | 'warning' | 'critical'
|
||||||
|
threshold: number
|
||||||
|
highestUsage: number
|
||||||
|
diskName: string
|
||||||
|
}>('/system/disk-status')
|
||||||
|
return response.data
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
async getDebugInfo() {
|
async getDebugInfo() {
|
||||||
return catchInternal(async () => {
|
return catchInternal(async () => {
|
||||||
const response = await this.client.get<{ debugInfo: string }>('/system/debug-info')
|
const response = await this.client.get<{ debugInfo: string }>('/system/debug-info')
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ router
|
||||||
|
|
||||||
router
|
router
|
||||||
.group(() => {
|
.group(() => {
|
||||||
|
router.get('/disk-status', [SystemController, 'getDiskStatus'])
|
||||||
router.get('/debug-info', [SystemController, 'getDebugInfo'])
|
router.get('/debug-info', [SystemController, 'getDebugInfo'])
|
||||||
router.get('/info', [SystemController, 'getSystemInfo'])
|
router.get('/info', [SystemController, 'getSystemInfo'])
|
||||||
router.get('/internet-status', [SystemController, 'getInternetStatus'])
|
router.get('/internet-status', [SystemController, 'getInternetStatus'])
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ export const KV_STORE_SCHEMA = {
|
||||||
'ui.theme': 'string',
|
'ui.theme': 'string',
|
||||||
'ai.assistantCustomName': 'string',
|
'ai.assistantCustomName': 'string',
|
||||||
'gpu.type': 'string',
|
'gpu.type': 'string',
|
||||||
|
'disk.warningThreshold': 'string',
|
||||||
|
'disk.criticalThreshold': 'string',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
type KVTagToType<T extends string> = T extends 'boolean' ? boolean : string
|
type KVTagToType<T extends string> = T extends 'boolean' ? boolean : string
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user