init
This commit is contained in:
6
src/components/__init__.py
Normal file
6
src/components/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .header import Header
|
||||
from .sidebar import Sidebar
|
||||
from .bottom_nav import BottomNav
|
||||
from .circular_progress import MetricCircle, LargeMetricCircle, ColorfulMetricCard
|
||||
|
||||
__all__ = ['Header', 'Sidebar', 'BottomNav', 'MetricCircle', 'LargeMetricCircle', 'ColorfulMetricCard']
|
||||
19
src/components/bottom_nav.py
Normal file
19
src/components/bottom_nav.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from nicegui import ui
|
||||
|
||||
|
||||
class BottomNav:
|
||||
def __init__(self):
|
||||
with ui.footer().classes('bottom-nav fixed-bottom') as footer:
|
||||
with ui.row().classes('w-full justify-around items-center py-2'):
|
||||
self._nav_item('dashboard', 'Dashboard', '/', active=True)
|
||||
self._nav_item('smart_toy', 'Ollama', '/ollama')
|
||||
self._nav_item('terminal', 'Processes', '/processes')
|
||||
self._nav_item('settings', 'Settings', '/settings')
|
||||
|
||||
def _nav_item(self, icon: str, label: str, route: str, active: bool = False):
|
||||
def navigate():
|
||||
ui.navigate.to(route)
|
||||
|
||||
with ui.column().classes('items-center cursor-pointer').on('click', navigate):
|
||||
ui.icon(icon, size='md').props(f'color={"cyan" if active else "grey-5"}')
|
||||
ui.label(label).classes(f'text-xs {"text-cyan" if active else "text-grey-5"} mt-1')
|
||||
49
src/components/circular_progress.py
Normal file
49
src/components/circular_progress.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from nicegui import ui
|
||||
|
||||
|
||||
class MetricCircle:
|
||||
def __init__(self, title: str, value: str, percentage: float, color: str, icon: str):
|
||||
with ui.card().classes('metric-card p-4 text-center'):
|
||||
with ui.column().classes('items-center gap-2'):
|
||||
# Icon at top
|
||||
ui.icon(icon, size='md', color=color)
|
||||
|
||||
# Title
|
||||
ui.label(title).classes('text-sm text-grey-5 font-medium')
|
||||
|
||||
# Circular progress - simplified
|
||||
ui.circular_progress(
|
||||
value=percentage,
|
||||
size='60px',
|
||||
color=color
|
||||
)
|
||||
|
||||
# Value
|
||||
ui.label(value).classes('text-lg font-bold text-white')
|
||||
|
||||
|
||||
class LargeMetricCircle:
|
||||
def __init__(self, title: str, value: str, percentage: float, color: str):
|
||||
with ui.card().classes('metric-card p-6 text-center'):
|
||||
with ui.column().classes('items-center gap-3'):
|
||||
# Title
|
||||
ui.label(title).classes('text-sm text-grey-5 font-medium uppercase tracking-wide')
|
||||
|
||||
# Large circular progress - simplified
|
||||
ui.circular_progress(
|
||||
value=percentage,
|
||||
size='120px',
|
||||
color=color
|
||||
)
|
||||
|
||||
# Value below
|
||||
ui.label(f'{int(percentage * 100)}%').classes('text-2xl font-bold text-white')
|
||||
ui.label(value).classes('text-xs text-grey-5')
|
||||
|
||||
|
||||
class ColorfulMetricCard:
|
||||
def __init__(self, title: str, icon: str, color: str):
|
||||
with ui.card().classes(f'p-4 text-center animate-fade-in').style(f'background: linear-gradient(135deg, {color}20 0%, {color}10 100%); border: 1px solid {color}40'):
|
||||
with ui.column().classes('items-center gap-2'):
|
||||
ui.icon(icon, size='xl').style(f'color: {color}')
|
||||
ui.label(title).classes('text-sm font-medium text-white')
|
||||
38
src/components/header.py
Normal file
38
src/components/header.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from nicegui import ui
|
||||
from utils import data_manager
|
||||
|
||||
|
||||
class Header(ui.header):
|
||||
def __init__(self):
|
||||
super().__init__(fixed=True, elevated=False)
|
||||
|
||||
with self.classes('bg-transparent'):
|
||||
with ui.row().classes('w-full items-center justify-between px-6 py-3'):
|
||||
# Left side - minimal branding
|
||||
with ui.row().classes('items-center gap-3'):
|
||||
ui.label('ArchGPU Frontend').classes('text-xl font-bold text-white')
|
||||
ui.chip('Live', icon='circle', color='green').props('size=sm outline')
|
||||
|
||||
# Right side - system status only
|
||||
with ui.row().classes('items-center gap-4'):
|
||||
# Get real-time data
|
||||
dashboard_data = data_manager.get_dashboard_data()
|
||||
|
||||
# System load indicator
|
||||
with ui.row().classes('items-center gap-2'):
|
||||
ui.icon('memory', size='sm', color='cyan')
|
||||
ui.label(f'CPU: {dashboard_data["cpu"]["percent"]}%').classes('text-sm text-white')
|
||||
|
||||
with ui.row().classes('items-center gap-2'):
|
||||
ui.icon('gpu_on', size='sm', color='orange')
|
||||
if dashboard_data['gpu']['available']:
|
||||
ui.label(f'GPU: {dashboard_data["gpu"]["percent"]}%').classes('text-sm text-white')
|
||||
else:
|
||||
ui.label('GPU: N/A').classes('text-sm text-white')
|
||||
|
||||
with ui.row().classes('items-center gap-2'):
|
||||
ui.icon('thermostat', size='sm', color='red')
|
||||
if dashboard_data['gpu']['available'] and dashboard_data['gpu']['temperature'] > 0:
|
||||
ui.label(f'{dashboard_data["gpu"]["temperature"]}°C').classes('text-sm text-white')
|
||||
else:
|
||||
ui.label('--°C').classes('text-sm text-white')
|
||||
48
src/components/sidebar.py
Normal file
48
src/components/sidebar.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from nicegui import ui
|
||||
|
||||
|
||||
class Sidebar:
|
||||
def __init__(self, current_route='/'):
|
||||
with ui.left_drawer(value=True, bordered=True, fixed=True).classes('w-64') as drawer:
|
||||
drawer.style('background: #252837; border-right: 1px solid #374151;')
|
||||
|
||||
with ui.column().classes('w-full h-full p-4'):
|
||||
|
||||
# Navigation sections
|
||||
ui.label('MAIN').classes('text-xs text-grey-5 font-bold tracking-wide mb-2')
|
||||
|
||||
with ui.column().classes('gap-1 mb-6'):
|
||||
self._nav_item('Dashboard', 'dashboard', '/', active=(current_route == '/'))
|
||||
self._nav_item('System Overview', 'monitor', '/system', active=(current_route == '/system'))
|
||||
|
||||
ui.label('MANAGEMENT').classes('text-xs text-grey-5 font-bold tracking-wide mb-2')
|
||||
|
||||
with ui.column().classes('gap-1 mb-6'):
|
||||
self._nav_item('Ollama Manager', 'smart_toy', '/ollama', active=(current_route == '/ollama'))
|
||||
self._nav_item('Process Manager', 'terminal', '/processes', active=(current_route == '/processes'))
|
||||
self._nav_item('Network Monitor', 'router', '/network', active=(current_route == '/network'))
|
||||
self._nav_item('Package Manager', 'inventory_2', '/packages', active=(current_route == '/packages'))
|
||||
|
||||
ui.label('TOOLS').classes('text-xs text-grey-5 font-bold tracking-wide mb-2')
|
||||
|
||||
with ui.column().classes('gap-1 mb-6'):
|
||||
self._nav_item('Log Viewer', 'description', '/logs', active=(current_route == '/logs'))
|
||||
self._nav_item('System Info', 'info', '/info', active=(current_route == '/info'))
|
||||
|
||||
ui.space()
|
||||
|
||||
# Bottom section
|
||||
ui.separator().classes('my-4')
|
||||
self._nav_item('Settings', 'settings', '/settings', active=(current_route == '/settings'))
|
||||
|
||||
def _nav_item(self, label: str, icon: str, route: str, active: bool = False):
|
||||
def navigate():
|
||||
ui.navigate.to(route)
|
||||
|
||||
bg_class = 'bg-cyan-600 bg-opacity-20' if active else ''
|
||||
text_color = 'text-cyan' if active else 'text-grey-5 hover:text-white'
|
||||
icon_color = 'cyan' if active else 'grey-5'
|
||||
|
||||
with ui.row().classes(f'w-full items-center gap-3 px-3 py-2 rounded-lg cursor-pointer {bg_class}').on('click', navigate):
|
||||
ui.icon(icon, size='sm', color=icon_color)
|
||||
ui.label(label).classes(f'text-sm {text_color}')
|
||||
193
src/main.py
Normal file
193
src/main.py
Normal file
@@ -0,0 +1,193 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from nicegui import ui, app
|
||||
|
||||
from components import Header, Sidebar
|
||||
from pages import DashboardPage, OllamaManagerPage
|
||||
from utils import data_manager
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logging.getLogger('watchfiles').setLevel(logging.WARNING)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app.add_static_files('/static', 'src/static')
|
||||
|
||||
# Start global data collection
|
||||
@app.on_startup
|
||||
async def startup():
|
||||
data_manager.start()
|
||||
|
||||
@app.on_shutdown
|
||||
async def shutdown():
|
||||
data_manager.stop()
|
||||
|
||||
|
||||
def create_layout(current_route='/'):
|
||||
# Force dark mode
|
||||
ui.dark_mode(True)
|
||||
ui.query('.nicegui-content').classes('p-0 m-0')
|
||||
|
||||
# Add custom CSS
|
||||
ui.add_head_html('<link rel="stylesheet" type="text/css" href="/static/style.css">')
|
||||
|
||||
Header()
|
||||
Sidebar(current_route)
|
||||
|
||||
|
||||
@ui.page('/')
|
||||
async def index_page():
|
||||
create_layout('/')
|
||||
DashboardPage()
|
||||
|
||||
|
||||
@ui.page('/system')
|
||||
async def system_page():
|
||||
create_layout('/system')
|
||||
with ui.element('div').classes('main-content w-full'):
|
||||
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
|
||||
ui.label('System Overview').classes('text-2xl font-bold text-white mb-4')
|
||||
with ui.card().classes('metric-card p-6'):
|
||||
ui.label('Detailed system information will be displayed here...').classes('text-grey-5')
|
||||
|
||||
|
||||
@ui.page('/ollama')
|
||||
async def ollama_page():
|
||||
create_layout('/ollama')
|
||||
with ui.element('div').classes('main-content w-full'):
|
||||
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
|
||||
ui.label('Ollama Manager').classes('text-2xl font-bold text-white mb-4')
|
||||
|
||||
# Status cards
|
||||
with ui.row().classes('w-full gap-4 mb-6'):
|
||||
with ui.card().classes('metric-card flex-grow p-4'):
|
||||
with ui.row().classes('items-center gap-2'):
|
||||
ui.icon('check_circle', color='green')
|
||||
ui.label('Status: Online').classes('font-medium text-white')
|
||||
|
||||
with ui.card().classes('metric-card flex-grow p-4'):
|
||||
ui.label('Version: 0.11.11').classes('font-medium text-white')
|
||||
|
||||
# Models list
|
||||
with ui.card().classes('metric-card p-6'):
|
||||
ui.label('Installed Models').classes('text-lg font-bold text-white mb-4')
|
||||
|
||||
models = [
|
||||
('llama3.2:3b', '2.0 GB', 'Q4_0'),
|
||||
('mistral:7b', '4.1 GB', 'Q4_0'),
|
||||
('codellama:13b', '7.4 GB', 'Q4_K_M'),
|
||||
('phi3:mini', '2.3 GB', 'Q4_0'),
|
||||
]
|
||||
|
||||
for name, size, quant in models:
|
||||
with ui.card().classes('metric-card p-4 mb-2'):
|
||||
with ui.row().classes('w-full items-center'):
|
||||
with ui.column().classes('gap-1'):
|
||||
ui.label(name).classes('font-bold text-white')
|
||||
with ui.row().classes('gap-2'):
|
||||
ui.chip(size, icon='storage').props('outline dense color=cyan')
|
||||
ui.chip(quant, icon='memory').props('outline dense color=orange')
|
||||
|
||||
ui.space()
|
||||
|
||||
with ui.row().classes('gap-2'):
|
||||
ui.button(icon='play_arrow').props('round flat color=green').tooltip('Run')
|
||||
ui.button(icon='info').props('round flat color=blue').tooltip('Info')
|
||||
ui.button(icon='delete').props('round flat color=red').tooltip('Delete')
|
||||
|
||||
|
||||
@ui.page('/processes')
|
||||
async def processes_page():
|
||||
create_layout('/processes')
|
||||
with ui.element('div').classes('main-content w-full'):
|
||||
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
|
||||
ui.label('Process Manager').classes('text-2xl font-bold text-white')
|
||||
with ui.card().classes('metric-card p-6'):
|
||||
ui.label('Process management coming soon...').classes('text-grey-5')
|
||||
|
||||
|
||||
@ui.page('/network')
|
||||
async def network_page():
|
||||
create_layout('/network')
|
||||
with ui.element('div').classes('main-content w-full'):
|
||||
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
|
||||
ui.label('Network Monitor').classes('text-2xl font-bold text-white')
|
||||
with ui.card().classes('metric-card p-6'):
|
||||
ui.label('Network monitoring coming soon...').classes('text-grey-5')
|
||||
|
||||
|
||||
@ui.page('/packages')
|
||||
async def packages_page():
|
||||
create_layout('/packages')
|
||||
with ui.element('div').classes('main-content w-full'):
|
||||
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
|
||||
ui.label('Package Manager').classes('text-2xl font-bold text-white')
|
||||
with ui.card().classes('metric-card p-6'):
|
||||
ui.label('Package management coming soon...').classes('text-grey-5')
|
||||
|
||||
|
||||
@ui.page('/logs')
|
||||
async def logs_page():
|
||||
create_layout('/logs')
|
||||
with ui.element('div').classes('main-content w-full'):
|
||||
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
|
||||
ui.label('Log Viewer').classes('text-2xl font-bold text-white')
|
||||
with ui.card().classes('metric-card p-6'):
|
||||
ui.label('Log viewing coming soon...').classes('text-grey-5')
|
||||
|
||||
|
||||
@ui.page('/info')
|
||||
async def info_page():
|
||||
create_layout('/info')
|
||||
with ui.element('div').classes('main-content w-full'):
|
||||
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
|
||||
ui.label('System Information').classes('text-2xl font-bold text-white')
|
||||
with ui.card().classes('metric-card p-6'):
|
||||
ui.label('Detailed system information coming soon...').classes('text-grey-5')
|
||||
|
||||
|
||||
@ui.page('/settings')
|
||||
async def settings_page():
|
||||
create_layout('/settings')
|
||||
with ui.element('div').classes('main-content w-full'):
|
||||
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
|
||||
ui.label('Settings').classes('text-2xl font-bold text-white')
|
||||
|
||||
with ui.card().classes('metric-card p-6'):
|
||||
ui.label('Refresh Intervals').classes('text-lg font-bold text-white mb-4')
|
||||
|
||||
with ui.column().classes('gap-4'):
|
||||
with ui.row().classes('items-center justify-between'):
|
||||
ui.label('System Stats').classes('text-white')
|
||||
ui.select(['1s', '2s', '5s', '10s'], value='2s').props('outlined dense color=cyan')
|
||||
|
||||
with ui.row().classes('items-center justify-between'):
|
||||
ui.label('GPU Temperature').classes('text-white')
|
||||
ui.select(['1s', '2s', '5s', '10s'], value='5s').props('outlined dense color=cyan')
|
||||
|
||||
with ui.card().classes('metric-card p-6 mt-4'):
|
||||
ui.label('About').classes('text-lg font-bold text-white mb-4')
|
||||
|
||||
with ui.column().classes('gap-2'):
|
||||
with ui.row().classes('items-center gap-2'):
|
||||
ui.icon('computer', color='cyan')
|
||||
ui.label('ArchGPU Frontend v0.1.0').classes('text-white')
|
||||
|
||||
with ui.row().classes('items-center gap-2'):
|
||||
ui.icon('smart_toy', color='orange')
|
||||
ui.label('Ollama v0.11.11').classes('text-white')
|
||||
|
||||
|
||||
if __name__ in {"__main__", "__mp_main__"}:
|
||||
ui.run(
|
||||
title=os.getenv('APP_TITLE', 'ArchGPU Frontend'),
|
||||
storage_secret=os.getenv('APP_STORAGE_SECRET'),
|
||||
port=int(os.getenv('APP_PORT', '8080')),
|
||||
show=os.getenv("APP_SHOW", 'false').lower() == "true",
|
||||
dark=True
|
||||
)
|
||||
6
src/pages/__init__.py
Normal file
6
src/pages/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .welcome import WelcomePage
|
||||
from .system_overview import SystemOverviewPage
|
||||
from .ollama_manager import OllamaManagerPage
|
||||
from .dashboard import DashboardPage
|
||||
|
||||
__all__ = ['WelcomePage', 'SystemOverviewPage', 'OllamaManagerPage', 'DashboardPage']
|
||||
148
src/pages/dashboard.py
Normal file
148
src/pages/dashboard.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from nicegui import ui
|
||||
from components.circular_progress import MetricCircle, LargeMetricCircle, ColorfulMetricCard
|
||||
from utils import data_manager
|
||||
|
||||
|
||||
class DashboardPage:
|
||||
def __init__(self):
|
||||
# Get real-time data
|
||||
dashboard_data = data_manager.get_dashboard_data()
|
||||
system_info = data_manager.get_system_info()
|
||||
|
||||
# Main content area with proper viewport handling
|
||||
with ui.element('div').classes('main-content w-full'):
|
||||
with ui.column().classes('w-full max-w-6xl mx-auto p-6 gap-6'):
|
||||
# Top stats grid
|
||||
with ui.grid(columns=4).classes('w-full gap-4'):
|
||||
MetricCircle('CPU', f"{dashboard_data['cpu']['percent']}%",
|
||||
dashboard_data['cpu']['percent'] / 100, '#e879f9', 'memory')
|
||||
MetricCircle('Memory', f"{dashboard_data['memory']['used_gb']}GB",
|
||||
dashboard_data['memory']['percent'] / 100, '#10b981', 'storage')
|
||||
|
||||
if dashboard_data['gpu']['available']:
|
||||
MetricCircle('GPU', f"{dashboard_data['gpu']['percent']}%",
|
||||
dashboard_data['gpu']['percent'] / 100, '#f97316', 'gpu_on')
|
||||
MetricCircle('Temp', f"{dashboard_data['gpu']['temperature']}°C",
|
||||
dashboard_data['gpu']['temperature'] / 100, '#06b6d4', 'thermostat')
|
||||
else:
|
||||
MetricCircle('GPU', 'N/A', 0, '#f97316', 'gpu_on')
|
||||
MetricCircle('Temp', 'N/A', 0, '#06b6d4', 'thermostat')
|
||||
|
||||
# Main dashboard content
|
||||
with ui.row().classes('w-full gap-6'):
|
||||
# Left column - charts and graphs
|
||||
with ui.column().classes('flex-grow gap-4'):
|
||||
# Performance chart card
|
||||
with ui.card().classes('chart-area p-6'):
|
||||
ui.label('System Performance').classes('text-lg font-bold text-white mb-4')
|
||||
|
||||
# Simulated chart area
|
||||
with ui.element('div').classes('h-48 w-full relative').style('background: linear-gradient(45deg, #1a1d2e 0%, #2a2d3e 100%); border-radius: 8px'):
|
||||
# Chart lines simulation
|
||||
with ui.element('svg').classes('absolute inset-0 w-full h-full'):
|
||||
ui.html('''
|
||||
<svg viewBox="0 0 400 200" class="w-full h-full">
|
||||
<defs>
|
||||
<linearGradient id="gradient1" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#e879f9;stop-opacity:0.3" />
|
||||
<stop offset="100%" style="stop-color:#e879f9;stop-opacity:0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gradient2" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#10b981;stop-opacity:0.3" />
|
||||
<stop offset="100%" style="stop-color:#10b981;stop-opacity:0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M 20 100 Q 100 50 200 80 T 380 60" stroke="#e879f9" stroke-width="2" fill="none"/>
|
||||
<path d="M 20 100 Q 100 50 200 80 T 380 60 L 380 180 L 20 180 Z" fill="url(#gradient1)"/>
|
||||
<path d="M 20 120 Q 100 90 200 110 T 380 100" stroke="#10b981" stroke-width="2" fill="none"/>
|
||||
<path d="M 20 120 Q 100 90 200 110 T 380 100 L 380 180 L 20 180 Z" fill="url(#gradient2)"/>
|
||||
</svg>
|
||||
''')
|
||||
|
||||
# Chart legend
|
||||
with ui.row().classes('gap-6 mt-4'):
|
||||
with ui.row().classes('items-center gap-2'):
|
||||
ui.element('div').classes('w-3 h-3 rounded-full').style('background: #e879f9')
|
||||
ui.label('CPU Usage').classes('text-sm text-grey-5')
|
||||
with ui.row().classes('items-center gap-2'):
|
||||
ui.element('div').classes('w-3 h-3 rounded-full').style('background: #10b981')
|
||||
ui.label('Memory Usage').classes('text-sm text-grey-5')
|
||||
|
||||
# Quick actions
|
||||
with ui.row().classes('w-full gap-4'):
|
||||
ColorfulMetricCard('Process Manager', 'terminal', '#e879f9')
|
||||
ColorfulMetricCard('Network Monitor', 'router', '#10b981')
|
||||
ColorfulMetricCard('Log Viewer', 'description', '#f97316')
|
||||
|
||||
# Right column - system info and GPU details
|
||||
with ui.column().classes('w-80 gap-4'):
|
||||
# Large GPU usage circle
|
||||
if dashboard_data['gpu']['available']:
|
||||
gpu_info = data_manager.get_gpu_info()
|
||||
gpu_name = 'Unknown GPU'
|
||||
if gpu_info.get('cards') and len(gpu_info['cards']) > 0:
|
||||
gpu_name = gpu_info['cards'][0].get('name', 'Unknown GPU')
|
||||
LargeMetricCircle('GPU Usage', gpu_name,
|
||||
dashboard_data['gpu']['percent'] / 100, '#f97316')
|
||||
else:
|
||||
LargeMetricCircle('GPU Usage', 'No GPU Detected', 0, '#f97316')
|
||||
|
||||
# System info card
|
||||
with ui.card().classes('metric-card p-4'):
|
||||
ui.label('System Info').classes('text-sm font-bold text-white mb-3')
|
||||
|
||||
with ui.column().classes('gap-2'):
|
||||
self._info_row('OS', system_info.get('os', 'Unknown'))
|
||||
self._info_row('Kernel', system_info.get('kernel', 'Unknown'))
|
||||
self._info_row('CPU', system_info.get('cpu', 'Unknown'))
|
||||
# Get first GPU name for display
|
||||
gpu_info = data_manager.get_gpu_info()
|
||||
gpu_display = 'No GPU'
|
||||
if gpu_info.get('cards') and len(gpu_info['cards']) > 0:
|
||||
gpu_display = gpu_info['cards'][0].get('name', 'Unknown GPU')
|
||||
self._info_row('GPU', gpu_display)
|
||||
self._info_row('Uptime', system_info.get('uptime', 'Unknown'))
|
||||
|
||||
# Ollama status card
|
||||
with ui.card().classes('metric-card p-4'):
|
||||
ui.label('Ollama Status').classes('text-sm font-bold text-white mb-3')
|
||||
|
||||
with ui.row().classes('items-center gap-2 mb-2'):
|
||||
ui.icon('check_circle', color='green', size='sm')
|
||||
ui.label('Online').classes('text-sm text-white')
|
||||
|
||||
with ui.column().classes('gap-1'):
|
||||
ui.label('4 models active').classes('text-xs text-grey-5')
|
||||
ui.label('llama3.2:3b, mistral:7b...').classes('text-xs text-grey-6')
|
||||
|
||||
# Bottom metrics row
|
||||
with ui.grid(columns=5).classes('w-full gap-4 mt-4'):
|
||||
self._bottom_metric(str(dashboard_data['processes']['count']), 'Processes', 'dashboard')
|
||||
|
||||
# Format network data (bytes to human readable)
|
||||
network_mb = (dashboard_data['network']['bytes_recv'] + dashboard_data['network']['bytes_sent']) / (1024 * 1024)
|
||||
if network_mb > 1024:
|
||||
network_display = f"{network_mb/1024:.1f}GB"
|
||||
else:
|
||||
network_display = f"{network_mb:.0f}MB"
|
||||
self._bottom_metric(network_display, 'Network', 'wifi')
|
||||
|
||||
self._bottom_metric(f"{dashboard_data['disk']['percent']:.0f}%", 'Disk', 'storage')
|
||||
|
||||
# CPU core count as services
|
||||
self._bottom_metric(str(dashboard_data['cpu']['count']), 'CPU Cores', 'settings')
|
||||
|
||||
# Memory total
|
||||
self._bottom_metric(f"{dashboard_data['memory']['total_gb']:.0f}GB", 'Total RAM', 'memory')
|
||||
|
||||
def _info_row(self, label: str, value: str):
|
||||
with ui.row().classes('w-full justify-between'):
|
||||
ui.label(label).classes('text-xs text-grey-5')
|
||||
ui.label(value).classes('text-xs text-white font-medium')
|
||||
|
||||
def _bottom_metric(self, value: str, label: str, icon: str):
|
||||
with ui.card().classes('metric-card p-3 text-center'):
|
||||
with ui.column().classes('items-center gap-1'):
|
||||
ui.icon(icon, size='sm', color='grey-5')
|
||||
ui.label(value).classes('text-lg font-bold text-white')
|
||||
ui.label(label).classes('text-xs text-grey-5')
|
||||
70
src/pages/ollama_manager.py
Normal file
70
src/pages/ollama_manager.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from nicegui import ui
|
||||
|
||||
|
||||
class OllamaManagerPage(ui.column):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
with self.classes('w-full gap-6 p-6'):
|
||||
ui.label('Ollama Manager').classes('text-h4 font-bold')
|
||||
|
||||
# Status cards
|
||||
with ui.row().classes('w-full gap-4'):
|
||||
with ui.card().classes('flex-grow'):
|
||||
with ui.row().classes('items-center gap-2'):
|
||||
ui.icon('check_circle').props('color=positive')
|
||||
ui.label('Ollama Status: Online').classes('font-medium')
|
||||
|
||||
with ui.card().classes('flex-grow'):
|
||||
ui.label('Version: 0.11.11').classes('font-medium')
|
||||
|
||||
# Models management
|
||||
with ui.card().classes('w-full'):
|
||||
with ui.row().classes('w-full items-center mb-4'):
|
||||
ui.label('Installed Models').classes('text-h6 font-bold')
|
||||
ui.space()
|
||||
ui.button('Pull New Model', icon='download').props('color=primary')
|
||||
|
||||
with ui.column().classes('w-full gap-2'):
|
||||
self._create_model_item('llama3.2:3b', '2.0 GB', 'Q4_0')
|
||||
self._create_model_item('mistral:7b', '4.1 GB', 'Q4_0')
|
||||
self._create_model_item('codellama:13b', '7.4 GB', 'Q4_K_M')
|
||||
self._create_model_item('phi3:mini', '2.3 GB', 'Q4_0')
|
||||
|
||||
# Quick test
|
||||
with ui.card().classes('w-full'):
|
||||
ui.label('Quick Chat Test').classes('text-h6 font-bold mb-4')
|
||||
|
||||
with ui.row().classes('w-full gap-2 mb-2'):
|
||||
ui.select(
|
||||
['llama3.2:3b', 'mistral:7b', 'codellama:13b', 'phi3:mini'],
|
||||
value='llama3.2:3b',
|
||||
label='Model'
|
||||
).classes('flex-grow').props('outlined')
|
||||
|
||||
ui.textarea(
|
||||
label='Prompt',
|
||||
placeholder='Enter your prompt here...',
|
||||
value='Hello! Tell me a fun fact about AMD GPUs.'
|
||||
).classes('w-full').props('outlined')
|
||||
|
||||
ui.button('Send', icon='send').props('color=primary')
|
||||
|
||||
with ui.expansion('Response', icon='message').classes('w-full mt-4').props('default-opened'):
|
||||
ui.label('Response will appear here...').classes('text-grey-7')
|
||||
|
||||
def _create_model_item(self, name: str, size: str, quantization: str):
|
||||
with ui.card().classes('w-full'):
|
||||
with ui.row().classes('w-full items-center'):
|
||||
with ui.column().classes('gap-1'):
|
||||
ui.label(name).classes('font-bold text-h6')
|
||||
with ui.row().classes('gap-4'):
|
||||
ui.chip(size, icon='storage').props('outline dense')
|
||||
ui.chip(quantization, icon='memory').props('outline dense')
|
||||
|
||||
ui.space()
|
||||
|
||||
with ui.row().classes('gap-2'):
|
||||
ui.button(icon='play_arrow').props('round flat color=primary').tooltip('Run Model')
|
||||
ui.button(icon='info').props('round flat').tooltip('Model Info')
|
||||
ui.button(icon='delete').props('round flat color=negative').tooltip('Delete Model')
|
||||
45
src/pages/system_overview.py
Normal file
45
src/pages/system_overview.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from nicegui import ui
|
||||
|
||||
|
||||
class SystemOverviewPage(ui.column):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
with self.classes('w-full gap-6 p-6'):
|
||||
ui.label('System Overview').classes('text-h4 font-bold')
|
||||
|
||||
with ui.row().classes('w-full gap-4 flex-wrap'):
|
||||
self._create_stat_card('CPU Usage', '45%', 'memory', 'blue')
|
||||
self._create_stat_card('Memory', '8.2 / 16 GB', 'storage', 'green')
|
||||
self._create_stat_card('GPU Usage', '78%', 'gpu_on', 'orange')
|
||||
self._create_stat_card('GPU Memory', '6.1 / 8 GB', 'memory_alt', 'purple')
|
||||
|
||||
with ui.card().classes('w-full'):
|
||||
ui.label('System Information').classes('text-h6 font-bold mb-4')
|
||||
with ui.grid(columns=2).classes('w-full gap-4'):
|
||||
self._info_row('Operating System', 'Arch Linux')
|
||||
self._info_row('Kernel', '6.16.7-arch1-1')
|
||||
self._info_row('GPU', 'AMD Radeon RX 6700 XT')
|
||||
self._info_row('Driver', 'amdgpu')
|
||||
self._info_row('CPU', 'AMD Ryzen 7 5800X')
|
||||
self._info_row('Uptime', '2 days, 14:32:15')
|
||||
|
||||
with ui.card().classes('w-full'):
|
||||
ui.label('GPU Temperature').classes('text-h6 font-bold mb-4')
|
||||
with ui.row().classes('items-center gap-4'):
|
||||
ui.icon('thermostat', size='lg')
|
||||
ui.label('65°C').classes('text-h5')
|
||||
ui.linear_progress(value=0.65, show_value=False).classes('flex-grow')
|
||||
|
||||
def _create_stat_card(self, title: str, value: str, icon: str, color: str):
|
||||
with ui.card().classes(f'flex-grow min-w-[200px]'):
|
||||
with ui.row().classes('items-center gap-4'):
|
||||
ui.icon(icon, size='lg').classes(f'text-{color}')
|
||||
with ui.column().classes('gap-1'):
|
||||
ui.label(title).classes('text-caption text-gray-500')
|
||||
ui.label(value).classes('text-h6 font-bold')
|
||||
|
||||
def _info_row(self, label: str, value: str):
|
||||
with ui.row().classes('w-full'):
|
||||
ui.label(label).classes('font-medium')
|
||||
ui.label(value).classes('text-gray-600 dark:text-gray-400')
|
||||
10
src/pages/welcome.py
Normal file
10
src/pages/welcome.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from typing import Literal
|
||||
from nicegui import ui
|
||||
|
||||
|
||||
class WelcomePage(ui.column):
|
||||
def __init__(self, *, wrap: bool = False, align_items: None | Literal['start'] | Literal['end'] | Literal['center'] | Literal['baseline'] | Literal['stretch'] = None) -> None:
|
||||
super().__init__(wrap=wrap, align_items=align_items)
|
||||
|
||||
with self:
|
||||
ui.label('something')
|
||||
163
src/static/style.css
Normal file
163
src/static/style.css
Normal file
@@ -0,0 +1,163 @@
|
||||
/* Dark theme inspired by mockup */
|
||||
:root {
|
||||
--bg-primary: #1a1d2e;
|
||||
--bg-secondary: #252837;
|
||||
--bg-card: #2a2d3e;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #9ca3af;
|
||||
--accent-pink: #e879f9;
|
||||
--accent-green: #10b981;
|
||||
--accent-orange: #f97316;
|
||||
--accent-cyan: #06b6d4;
|
||||
--accent-purple: #8b5cf6;
|
||||
--border-color: #374151;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-primary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
|
||||
}
|
||||
|
||||
.nicegui-content {
|
||||
background-color: var(--bg-primary) !important;
|
||||
}
|
||||
|
||||
/* Card styling */
|
||||
.q-card {
|
||||
background-color: var(--bg-card) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
border-radius: 16px !important;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
.q-header {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
border-bottom: 1px solid var(--border-color) !important;
|
||||
}
|
||||
|
||||
/* Drawer styling */
|
||||
.q-drawer {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
border-right: 1px solid var(--border-color) !important;
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
.q-btn {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.q-btn--flat {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Progress circle styling */
|
||||
.progress-circle {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.progress-circle .q-circular-progress {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.progress-circle .progress-text {
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Metric card styling */
|
||||
.metric-card {
|
||||
background: linear-gradient(135deg, var(--bg-card) 0%, rgba(42, 45, 62, 0.8) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Chart area styling */
|
||||
.chart-area {
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Bottom navigation */
|
||||
.bottom-nav {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
border-top: 1px solid var(--border-color) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.bottom-nav .q-btn {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
.bottom-nav .q-btn--active {
|
||||
color: var(--accent-cyan) !important;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Glassmorphism effect */
|
||||
.glass {
|
||||
background: rgba(42, 45, 62, 0.6);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Layout and viewport fixes */
|
||||
body, .nicegui-content, .q-page, .q-page-container {
|
||||
background: #1a1d2e !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.q-header, .q-drawer, .q-footer {
|
||||
z-index: 2000 !important;
|
||||
}
|
||||
|
||||
.q-drawer {
|
||||
width: 256px !important;
|
||||
}
|
||||
|
||||
.q-header {
|
||||
height: 64px !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
height: calc(100vh - 64px) !important;
|
||||
overflow-y: auto !important;
|
||||
margin-left: 256px !important;
|
||||
padding-top: 64px !important;
|
||||
}
|
||||
5
src/utils/__init__.py
Normal file
5
src/utils/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .system_monitor import SystemMonitor
|
||||
from .gpu_monitor import GPUMonitor
|
||||
from .data_manager import data_manager
|
||||
|
||||
__all__ = ['SystemMonitor', 'GPUMonitor', 'data_manager']
|
||||
142
src/utils/data_manager.py
Normal file
142
src/utils/data_manager.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
from .system_monitor import SystemMonitor
|
||||
from .gpu_monitor import GPUMonitor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DataManager:
|
||||
"""Global data manager that collects system information in the background"""
|
||||
|
||||
def __init__(self, update_interval: float = 1.0):
|
||||
self.update_interval = update_interval
|
||||
self.system_monitor = SystemMonitor()
|
||||
self.gpu_monitor = GPUMonitor()
|
||||
|
||||
self._data = {
|
||||
'system_info': {},
|
||||
'system_stats': {},
|
||||
'gpu_info': {},
|
||||
'gpu_stats': {},
|
||||
'last_update': 0
|
||||
}
|
||||
|
||||
self._running = False
|
||||
self._thread = None
|
||||
self._lock = threading.RLock()
|
||||
|
||||
def start(self):
|
||||
"""Start the background data collection"""
|
||||
if self._running:
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._update_loop, daemon=True)
|
||||
self._thread.start()
|
||||
logger.info("DataManager started")
|
||||
|
||||
def stop(self):
|
||||
"""Stop the background data collection"""
|
||||
self._running = False
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(timeout=5)
|
||||
logger.info("DataManager stopped")
|
||||
|
||||
def _update_loop(self):
|
||||
"""Background loop that updates system data"""
|
||||
while self._running:
|
||||
try:
|
||||
start_time = time.time()
|
||||
|
||||
# Collect system information
|
||||
with self._lock:
|
||||
# Static info (cached internally by monitors)
|
||||
self._data['system_info'] = self.system_monitor.get_system_info()
|
||||
self._data['gpu_info'] = self.gpu_monitor.get_gpu_info()
|
||||
|
||||
# Dynamic stats
|
||||
self._data['system_stats'] = self.system_monitor.get_system_stats()
|
||||
self._data['gpu_stats'] = self.gpu_monitor.get_primary_gpu_stats()
|
||||
self._data['last_update'] = time.time()
|
||||
|
||||
# Calculate sleep time to maintain consistent intervals
|
||||
elapsed = time.time() - start_time
|
||||
sleep_time = max(0, self.update_interval - elapsed)
|
||||
|
||||
if sleep_time > 0:
|
||||
time.sleep(sleep_time)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in data update loop: {e}")
|
||||
time.sleep(1) # Brief pause before retrying
|
||||
|
||||
def get_dashboard_data(self) -> Dict[str, Any]:
|
||||
"""Get all data needed for the dashboard"""
|
||||
with self._lock:
|
||||
stats = self._data['system_stats']
|
||||
gpu_stats = self._data['gpu_stats']
|
||||
|
||||
# Format data for dashboard consumption
|
||||
return {
|
||||
'cpu': {
|
||||
'percent': round(stats.get('cpu', {}).get('percent', 0), 1),
|
||||
'count': stats.get('cpu', {}).get('count', 0)
|
||||
},
|
||||
'memory': {
|
||||
'percent': round(stats.get('memory', {}).get('percent', 0), 1),
|
||||
'used_gb': round(stats.get('memory', {}).get('used', 0) / (1024**3), 1),
|
||||
'total_gb': round(stats.get('memory', {}).get('total', 0) / (1024**3), 1)
|
||||
},
|
||||
'gpu': {
|
||||
'percent': round(gpu_stats.get('usage', 0), 1),
|
||||
'temperature': round(gpu_stats.get('temperature', 0), 1),
|
||||
'available': gpu_stats.get('available', False)
|
||||
},
|
||||
'processes': {
|
||||
'count': stats.get('processes', {}).get('count', 0)
|
||||
},
|
||||
'disk': {
|
||||
'percent': round(stats.get('disk', {}).get('percent', 0), 1)
|
||||
},
|
||||
'network': {
|
||||
'bytes_sent': stats.get('network', {}).get('bytes_sent', 0),
|
||||
'bytes_recv': stats.get('network', {}).get('bytes_recv', 0)
|
||||
},
|
||||
'last_update': self._data['last_update']
|
||||
}
|
||||
|
||||
def get_system_info(self) -> Dict[str, Any]:
|
||||
"""Get static system information"""
|
||||
with self._lock:
|
||||
return self._data['system_info'].copy()
|
||||
|
||||
def get_system_stats(self) -> Dict[str, Any]:
|
||||
"""Get current system statistics"""
|
||||
with self._lock:
|
||||
return self._data['system_stats'].copy()
|
||||
|
||||
def get_gpu_info(self) -> Dict[str, Any]:
|
||||
"""Get static GPU information"""
|
||||
with self._lock:
|
||||
return self._data['gpu_info'].copy()
|
||||
|
||||
def get_gpu_stats(self) -> Dict[str, Any]:
|
||||
"""Get current GPU statistics"""
|
||||
with self._lock:
|
||||
return self._data['gpu_stats'].copy()
|
||||
|
||||
def get_processes(self, limit: int = 10) -> list:
|
||||
"""Get top processes (fetched on demand to avoid overhead)"""
|
||||
return self.system_monitor.get_processes(limit)
|
||||
|
||||
def format_bytes(self, bytes_value: int) -> str:
|
||||
"""Format bytes to human readable format"""
|
||||
return self.system_monitor.format_bytes(bytes_value)
|
||||
|
||||
|
||||
# Global instance
|
||||
data_manager = DataManager()
|
||||
308
src/utils/gpu_monitor.py
Normal file
308
src/utils/gpu_monitor.py
Normal file
@@ -0,0 +1,308 @@
|
||||
import subprocess
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GPUMonitor:
|
||||
def __init__(self):
|
||||
self.last_update = None
|
||||
self.cache_duration = 2 # seconds
|
||||
self._cached_data = {}
|
||||
self.gpu_available = self._check_gpu_availability()
|
||||
|
||||
def _check_gpu_availability(self) -> bool:
|
||||
"""Check if AMD GPU monitoring tools are available"""
|
||||
try:
|
||||
# Check for rocm-smi (AMD)
|
||||
result = subprocess.run(['rocm-smi', '--help'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
pass
|
||||
|
||||
try:
|
||||
# Check for radeontop
|
||||
result = subprocess.run(['radeontop', '--help'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# Check for GPU in /sys/class/drm
|
||||
try:
|
||||
import os
|
||||
gpu_dirs = [d for d in os.listdir('/sys/class/drm') if d.startswith('card')]
|
||||
return len(gpu_dirs) > 0
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
def get_gpu_info(self) -> Dict[str, Any]:
|
||||
"""Get static GPU information"""
|
||||
if not self.gpu_available:
|
||||
return {'available': False, 'message': 'No GPU monitoring tools found'}
|
||||
|
||||
if not self._cached_data.get('gpu_info'):
|
||||
try:
|
||||
gpu_info = self._get_rocm_info()
|
||||
if not gpu_info:
|
||||
gpu_info = self._get_sys_gpu_info()
|
||||
|
||||
self._cached_data['gpu_info'] = gpu_info
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting GPU info: {e}")
|
||||
self._cached_data['gpu_info'] = {'available': False, 'error': str(e)}
|
||||
|
||||
return self._cached_data['gpu_info']
|
||||
|
||||
def get_gpu_stats(self) -> Dict[str, Any]:
|
||||
"""Get real-time GPU statistics"""
|
||||
if not self.gpu_available:
|
||||
return {'available': False}
|
||||
|
||||
now = time.time()
|
||||
if (self.last_update is None or
|
||||
now - self.last_update > self.cache_duration):
|
||||
|
||||
try:
|
||||
stats = self._get_rocm_stats()
|
||||
if not stats:
|
||||
stats = self._get_fallback_stats()
|
||||
|
||||
stats['timestamp'] = now
|
||||
self._cached_data['stats'] = stats
|
||||
self.last_update = now
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting GPU stats: {e}")
|
||||
self._cached_data['stats'] = {'available': False, 'error': str(e)}
|
||||
|
||||
return self._cached_data.get('stats', {'available': False})
|
||||
|
||||
def _get_rocm_info(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get GPU info using rocm-smi"""
|
||||
try:
|
||||
result = subprocess.run(['rocm-smi', '--showid', '--showproductname'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode == 0:
|
||||
lines = result.stdout.strip().split('\n')
|
||||
gpu_info = {'available': True, 'driver': 'rocm-smi', 'cards': []}
|
||||
|
||||
for line in lines:
|
||||
if 'GPU[' in line and ':' in line:
|
||||
# Parse GPU ID and name
|
||||
parts = line.split(':')
|
||||
if len(parts) >= 2:
|
||||
gpu_id = parts[0].strip()
|
||||
gpu_name = parts[1].strip()
|
||||
gpu_info['cards'].append({
|
||||
'id': gpu_id,
|
||||
'name': gpu_name
|
||||
})
|
||||
|
||||
return gpu_info if gpu_info['cards'] else None
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"rocm-smi not available: {e}")
|
||||
return None
|
||||
|
||||
def _get_rocm_stats(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get GPU stats using rocm-smi"""
|
||||
try:
|
||||
# Get temperature, usage, and memory info
|
||||
result = subprocess.run(['rocm-smi', '--showtemp', '--showuse', '--showmeminfo'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode == 0:
|
||||
stats = {'available': True, 'cards': []}
|
||||
|
||||
lines = result.stdout.strip().split('\n')
|
||||
current_gpu = None
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
|
||||
# Parse GPU identifier
|
||||
if line.startswith('GPU['):
|
||||
gpu_match = re.search(r'GPU\[(\d+)\]', line)
|
||||
if gpu_match:
|
||||
current_gpu = {
|
||||
'id': int(gpu_match.group(1)),
|
||||
'temperature': None,
|
||||
'usage': None,
|
||||
'memory_used': None,
|
||||
'memory_total': None
|
||||
}
|
||||
stats['cards'].append(current_gpu)
|
||||
|
||||
# Parse temperature
|
||||
elif 'Temperature' in line and current_gpu is not None:
|
||||
temp_match = re.search(r'(\d+\.\d+)°C', line)
|
||||
if temp_match:
|
||||
current_gpu['temperature'] = float(temp_match.group(1))
|
||||
|
||||
# Parse GPU usage
|
||||
elif 'GPU use' in line and current_gpu is not None:
|
||||
usage_match = re.search(r'(\d+)%', line)
|
||||
if usage_match:
|
||||
current_gpu['usage'] = int(usage_match.group(1))
|
||||
|
||||
# Parse memory info
|
||||
elif 'Memory' in line and current_gpu is not None:
|
||||
mem_match = re.search(r'(\d+)MB / (\d+)MB', line)
|
||||
if mem_match:
|
||||
current_gpu['memory_used'] = int(mem_match.group(1))
|
||||
current_gpu['memory_total'] = int(mem_match.group(2))
|
||||
|
||||
return stats if stats['cards'] else None
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"rocm-smi stats not available: {e}")
|
||||
return None
|
||||
|
||||
def _get_sys_gpu_info(self) -> Dict[str, Any]:
|
||||
"""Get GPU info from /sys filesystem"""
|
||||
try:
|
||||
import os
|
||||
gpu_info = {'available': True, 'driver': 'sysfs', 'cards': []}
|
||||
|
||||
drm_path = '/sys/class/drm'
|
||||
if os.path.exists(drm_path):
|
||||
card_dirs = [d for d in os.listdir(drm_path) if d.startswith('card') and not d.endswith('-')]
|
||||
|
||||
for card_dir in sorted(card_dirs):
|
||||
card_path = os.path.join(drm_path, card_dir)
|
||||
device_path = os.path.join(card_path, 'device')
|
||||
|
||||
gpu_name = "Unknown GPU"
|
||||
|
||||
# Try to get GPU name from various sources
|
||||
for name_file in ['product_name', 'device/product_name']:
|
||||
name_path = os.path.join(card_path, name_file)
|
||||
if os.path.exists(name_path):
|
||||
try:
|
||||
with open(name_path, 'r') as f:
|
||||
gpu_name = f.read().strip()
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
# Try vendor and device IDs
|
||||
if gpu_name == "Unknown GPU":
|
||||
try:
|
||||
vendor_path = os.path.join(device_path, 'vendor')
|
||||
device_id_path = os.path.join(device_path, 'device')
|
||||
|
||||
if os.path.exists(vendor_path) and os.path.exists(device_id_path):
|
||||
with open(vendor_path, 'r') as f:
|
||||
vendor = f.read().strip()
|
||||
with open(device_id_path, 'r') as f:
|
||||
device_id = f.read().strip()
|
||||
|
||||
if vendor == '0x1002': # AMD vendor ID
|
||||
gpu_name = f"AMD GPU ({device_id})"
|
||||
except:
|
||||
pass
|
||||
|
||||
gpu_info['cards'].append({
|
||||
'id': card_dir,
|
||||
'name': gpu_name,
|
||||
'path': card_path
|
||||
})
|
||||
|
||||
return gpu_info if gpu_info['cards'] else {'available': False}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting sysfs GPU info: {e}")
|
||||
return {'available': False, 'error': str(e)}
|
||||
|
||||
def _get_fallback_stats(self) -> Dict[str, Any]:
|
||||
"""Get basic GPU stats from /sys filesystem"""
|
||||
try:
|
||||
import os
|
||||
stats = {'available': True, 'cards': []}
|
||||
|
||||
# Try to read basic info from sysfs
|
||||
drm_path = '/sys/class/drm'
|
||||
if os.path.exists(drm_path):
|
||||
card_dirs = [d for d in os.listdir(drm_path) if d.startswith('card') and not d.endswith('-')]
|
||||
|
||||
for card_dir in sorted(card_dirs):
|
||||
card_path = os.path.join(drm_path, card_dir)
|
||||
device_path = os.path.join(card_path, 'device')
|
||||
|
||||
gpu_stats = {
|
||||
'id': card_dir,
|
||||
'temperature': None,
|
||||
'usage': None,
|
||||
'memory_used': None,
|
||||
'memory_total': None
|
||||
}
|
||||
|
||||
# Try to read temperature from hwmon
|
||||
hwmon_path = os.path.join(device_path, 'hwmon')
|
||||
if os.path.exists(hwmon_path):
|
||||
for hwmon_dir in os.listdir(hwmon_path):
|
||||
temp_file = os.path.join(hwmon_path, hwmon_dir, 'temp1_input')
|
||||
if os.path.exists(temp_file):
|
||||
try:
|
||||
with open(temp_file, 'r') as f:
|
||||
temp_millicelsius = int(f.read().strip())
|
||||
gpu_stats['temperature'] = temp_millicelsius / 1000.0
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
# Try to read GPU usage (if available)
|
||||
gpu_busy_file = os.path.join(device_path, 'gpu_busy_percent')
|
||||
if os.path.exists(gpu_busy_file):
|
||||
try:
|
||||
with open(gpu_busy_file, 'r') as f:
|
||||
gpu_stats['usage'] = int(f.read().strip())
|
||||
except:
|
||||
pass
|
||||
|
||||
stats['cards'].append(gpu_stats)
|
||||
|
||||
return stats if stats['cards'] else {'available': False}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting fallback GPU stats: {e}")
|
||||
return {'available': False, 'error': str(e)}
|
||||
|
||||
def get_primary_gpu_stats(self) -> Dict[str, Any]:
|
||||
"""Get stats for the primary/first GPU"""
|
||||
all_stats = self.get_gpu_stats()
|
||||
|
||||
if not all_stats.get('available') or not all_stats.get('cards'):
|
||||
return {
|
||||
'available': False,
|
||||
'usage': 0,
|
||||
'temperature': 0,
|
||||
'memory_percent': 0
|
||||
}
|
||||
|
||||
primary_gpu = all_stats['cards'][0]
|
||||
|
||||
# Calculate memory percentage
|
||||
memory_percent = 0
|
||||
if (primary_gpu.get('memory_used') is not None and
|
||||
primary_gpu.get('memory_total') is not None and
|
||||
primary_gpu['memory_total'] > 0):
|
||||
memory_percent = (primary_gpu['memory_used'] / primary_gpu['memory_total']) * 100
|
||||
|
||||
return {
|
||||
'available': True,
|
||||
'usage': primary_gpu.get('usage', 0) or 0,
|
||||
'temperature': primary_gpu.get('temperature', 0) or 0,
|
||||
'memory_percent': memory_percent,
|
||||
'memory_used': primary_gpu.get('memory_used', 0) or 0,
|
||||
'memory_total': primary_gpu.get('memory_total', 0) or 0
|
||||
}
|
||||
187
src/utils/system_monitor.py
Normal file
187
src/utils/system_monitor.py
Normal file
@@ -0,0 +1,187 @@
|
||||
import psutil
|
||||
import platform
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SystemMonitor:
|
||||
def __init__(self):
|
||||
self.last_update = None
|
||||
self.cache_duration = 2 # seconds
|
||||
self._cached_data = {}
|
||||
|
||||
def get_system_info(self) -> Dict[str, Any]:
|
||||
"""Get static system information"""
|
||||
if not self._cached_data.get('system_info'):
|
||||
try:
|
||||
uname = platform.uname()
|
||||
boot_time = datetime.fromtimestamp(psutil.boot_time())
|
||||
uptime = datetime.now() - boot_time
|
||||
|
||||
self._cached_data['system_info'] = {
|
||||
'os': f"{uname.system}",
|
||||
'kernel': uname.release,
|
||||
'cpu': self._get_cpu_info(),
|
||||
'memory_total': psutil.virtual_memory().total,
|
||||
'uptime': self._format_uptime(uptime),
|
||||
'hostname': uname.node,
|
||||
'architecture': uname.machine
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting system info: {e}")
|
||||
self._cached_data['system_info'] = {}
|
||||
|
||||
return self._cached_data['system_info']
|
||||
|
||||
def get_system_stats(self) -> Dict[str, Any]:
|
||||
"""Get real-time system statistics"""
|
||||
now = time.time()
|
||||
if (self.last_update is None or
|
||||
now - self.last_update > self.cache_duration):
|
||||
|
||||
try:
|
||||
# CPU stats
|
||||
cpu_percent = psutil.cpu_percent(interval=0.1)
|
||||
cpu_count = psutil.cpu_count()
|
||||
cpu_freq = psutil.cpu_freq()
|
||||
|
||||
# Memory stats
|
||||
memory = psutil.virtual_memory()
|
||||
swap = psutil.swap_memory()
|
||||
|
||||
# Disk stats
|
||||
disk = psutil.disk_usage('/')
|
||||
|
||||
# Network stats
|
||||
network = psutil.net_io_counters()
|
||||
|
||||
# Process count
|
||||
process_count = len(psutil.pids())
|
||||
|
||||
# Load average (Unix only)
|
||||
load_avg = None
|
||||
try:
|
||||
load_avg = psutil.getloadavg()
|
||||
except AttributeError:
|
||||
# Windows doesn't have load average
|
||||
pass
|
||||
|
||||
# Temperature (if available)
|
||||
temperatures = self._get_temperatures()
|
||||
|
||||
self._cached_data['stats'] = {
|
||||
'cpu': {
|
||||
'percent': cpu_percent,
|
||||
'count': cpu_count,
|
||||
'frequency': cpu_freq.current if cpu_freq else None,
|
||||
'load_avg': load_avg
|
||||
},
|
||||
'memory': {
|
||||
'total': memory.total,
|
||||
'available': memory.available,
|
||||
'used': memory.used,
|
||||
'percent': memory.percent,
|
||||
'free': memory.free
|
||||
},
|
||||
'swap': {
|
||||
'total': swap.total,
|
||||
'used': swap.used,
|
||||
'percent': swap.percent
|
||||
},
|
||||
'disk': {
|
||||
'total': disk.total,
|
||||
'used': disk.used,
|
||||
'free': disk.free,
|
||||
'percent': (disk.used / disk.total) * 100
|
||||
},
|
||||
'network': {
|
||||
'bytes_sent': network.bytes_sent,
|
||||
'bytes_recv': network.bytes_recv,
|
||||
'packets_sent': network.packets_sent,
|
||||
'packets_recv': network.packets_recv
|
||||
},
|
||||
'processes': {
|
||||
'count': process_count
|
||||
},
|
||||
'temperatures': temperatures,
|
||||
'timestamp': now
|
||||
}
|
||||
|
||||
self.last_update = now
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting system stats: {e}")
|
||||
self._cached_data['stats'] = {}
|
||||
|
||||
return self._cached_data.get('stats', {})
|
||||
|
||||
def get_processes(self, limit: int = 10) -> list:
|
||||
"""Get top processes by CPU usage"""
|
||||
try:
|
||||
processes = []
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'username']):
|
||||
try:
|
||||
processes.append(proc.info)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
|
||||
# Sort by CPU usage
|
||||
processes.sort(key=lambda x: x.get('cpu_percent', 0), reverse=True)
|
||||
return processes[:limit]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting processes: {e}")
|
||||
return []
|
||||
|
||||
def _get_cpu_info(self) -> str:
|
||||
"""Get CPU model name"""
|
||||
try:
|
||||
with open('/proc/cpuinfo', 'r') as f:
|
||||
for line in f:
|
||||
if 'model name' in line:
|
||||
return line.split(':')[1].strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Fallback for non-Linux systems
|
||||
return f"{psutil.cpu_count()} CPU cores"
|
||||
|
||||
def _get_temperatures(self) -> Dict[str, float]:
|
||||
"""Get system temperatures if available"""
|
||||
try:
|
||||
temps = psutil.sensors_temperatures()
|
||||
result = {}
|
||||
|
||||
for name, entries in temps.items():
|
||||
for entry in entries:
|
||||
if entry.current:
|
||||
key = f"{name}_{entry.label}" if entry.label else name
|
||||
result[key] = entry.current
|
||||
|
||||
return result
|
||||
except:
|
||||
return {}
|
||||
|
||||
def _format_uptime(self, uptime: timedelta) -> str:
|
||||
"""Format uptime duration"""
|
||||
days = uptime.days
|
||||
hours, remainder = divmod(uptime.seconds, 3600)
|
||||
minutes, _ = divmod(remainder, 60)
|
||||
|
||||
if days > 0:
|
||||
return f"{days}d {hours}h {minutes}m"
|
||||
elif hours > 0:
|
||||
return f"{hours}h {minutes}m"
|
||||
else:
|
||||
return f"{minutes}m"
|
||||
|
||||
def format_bytes(self, bytes_value: int) -> str:
|
||||
"""Format bytes to human readable format"""
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if bytes_value < 1024.0:
|
||||
return f"{bytes_value:.1f}{unit}"
|
||||
bytes_value /= 1024.0
|
||||
return f"{bytes_value:.1f}PB"
|
||||
Reference in New Issue
Block a user