Fix TypeScript build errors - remove unused imports

This commit is contained in:
ImpulsiveFPS 2026-02-01 18:42:00 +01:00
parent ecc1676fe5
commit 16c7d09a44
21 changed files with 305 additions and 41 deletions

View File

@ -1,9 +1,9 @@
# Multi-stage build for IPMI Fan Control # Multi-stage build for IPMI Fan Control
# Backend stage # Backend stage
FROM python:3.11-slim as backend-builder FROM python:3.11-slim AS backend-builder
WORKDIR /app/backend WORKDIR /app
# Install system dependencies for ipmitool # Install system dependencies for ipmitool
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
@ -18,16 +18,16 @@ COPY backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Copy backend source # Copy backend source
COPY backend/ . COPY backend/ ./backend/
# Frontend stage # Frontend stage
FROM node:20-alpine as frontend-builder FROM node:20-alpine AS frontend-builder
WORKDIR /app/frontend WORKDIR /app/frontend
# Install dependencies # Install dependencies
COPY frontend/package*.json ./ COPY frontend/package*.json ./
RUN npm ci RUN npm install
# Copy frontend source and build # Copy frontend source and build
COPY frontend/ . COPY frontend/ .
@ -56,10 +56,9 @@ COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
# Create data directory # Create data directory
RUN mkdir -p /app/data RUN mkdir -p /app/data
# Environment variables # Set Python path to include backend directory
ENV PYTHONPATH=/app/backend ENV PYTHONPATH=/app
ENV DATA_DIR=/app/data ENV DATA_DIR=/app/data
ENV SECRET_KEY=change-me-in-production
# Expose port # Expose port
EXPOSE 8000 EXPOSE 8000
@ -68,5 +67,5 @@ EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')" || exit 1 CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')" || exit 1
# Start command # Start command - use absolute module path
CMD ["python", "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["python", "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 IPMI Fan Control
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -12,7 +12,7 @@ from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from config import settings from backend.config import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -10,7 +10,7 @@ from sqlalchemy import (
from sqlalchemy.orm import declarative_base, sessionmaker, relationship, Session from sqlalchemy.orm import declarative_base, sessionmaker, relationship, Session
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from config import settings, DATA_DIR from backend.config import settings, DATA_DIR
# Create engine # Create engine
if settings.DATABASE_URL.startswith("sqlite"): if settings.DATABASE_URL.startswith("sqlite"):

View File

@ -9,12 +9,12 @@ from enum import Enum
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from database import ( from backend.database import (
Server, FanCurve, SensorData, FanData, SystemLog, Server, FanCurve, SensorData, FanData, SystemLog,
get_db, SessionLocal get_db, SessionLocal
) )
from ipmi_client import IPMIClient, TemperatureReading from backend.ipmi_client import IPMIClient, TemperatureReading
from config import settings from backend.config import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -168,7 +168,7 @@ class FanController:
return return
# Create IPMI client # Create IPMI client
from auth import decrypt_password from backend.auth import decrypt_password
client = IPMIClient( client = IPMIClient(
host=server.host, host=server.host,
username=server.username, username=server.username,

View File

@ -7,7 +7,7 @@ from typing import List, Dict, Optional, Tuple, Any
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from config import settings from backend.config import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -11,15 +11,15 @@ from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from config import settings from backend.config import settings
from database import init_db, get_db, is_setup_complete, set_setup_complete, User, Server, FanCurve, SystemLog, SensorData, FanData from backend.database import init_db, get_db, is_setup_complete, set_setup_complete, User, Server, FanCurve, SystemLog, SensorData, FanData
from auth import ( from backend.auth import (
verify_password, get_password_hash, create_access_token, verify_password, get_password_hash, create_access_token,
decode_access_token, encrypt_password, decrypt_password decode_access_token, encrypt_password, decrypt_password
) )
from ipmi_client import IPMIClient from backend.ipmi_client import IPMIClient
from fan_control import fan_controller, FanCurveManager, initialize_fan_controller, shutdown_fan_controller from backend.fan_control import fan_controller, FanCurveManager, initialize_fan_controller, shutdown_fan_controller
from schemas import ( from backend.schemas import (
UserCreate, UserLogin, UserResponse, Token, TokenData, UserCreate, UserLogin, UserResponse, Token, TokenData,
ServerCreate, ServerUpdate, ServerResponse, ServerDetailResponse, ServerStatusResponse, ServerCreate, ServerUpdate, ServerResponse, ServerDetailResponse, ServerStatusResponse,
FanCurveCreate, FanCurveResponse, FanCurvePoint, FanCurveCreate, FanCurveResponse, FanCurvePoint,

View File

@ -5,6 +5,7 @@ pydantic==2.5.3
pydantic-settings==2.1.0 pydantic-settings==2.1.0
python-jose[cryptography]==3.3.0 python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4 passlib[bcrypt]==1.7.4
cryptography==42.0.0
python-multipart==0.0.6 python-multipart==0.0.6
aiofiles==23.2.1 aiofiles==23.2.1
httpx==0.26.0 httpx==0.26.0
@ -12,3 +13,5 @@ apscheduler==3.10.4
psutil==5.9.8 psutil==5.9.8
asyncpg==0.29.0 asyncpg==0.29.0
aiosqlite==0.19.0 aiosqlite==0.19.0
pytest==7.4.4
pytest-asyncio==0.23.3

View File

@ -1,11 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Entry point for running the application directly.""" """Entry point for running the application directly."""
import uvicorn import uvicorn
from config import settings from backend.config import settings
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run( uvicorn.run(
"main:app", "backend.main:app",
host="0.0.0.0", host="0.0.0.0",
port=8000, port=8000,
reload=settings.DEBUG, reload=settings.DEBUG,

View File

@ -8,18 +8,18 @@ services:
ports: ports:
- "8000:8000" - "8000:8000"
environment: environment:
- SECRET_KEY=${SECRET_KEY:-your-secure-secret-key-change-in-production} - SECRET_KEY=${SECRET_KEY:-dev-secret-key-change-in-production}
- DATA_DIR=/app/data - DATA_DIR=/app/data
- PANIC_TIMEOUT_SECONDS=${PANIC_TIMEOUT_SECONDS:-60} - PANIC_TIMEOUT_SECONDS=${PANIC_TIMEOUT_SECONDS:-60}
- PANIC_FAN_SPEED=${PANIC_FAN_SPEED:-100} - PANIC_FAN_SPEED=${PANIC_FAN_SPEED:-100}
- DEBUG=${DEBUG:-true}
- PYTHONUNBUFFERED=1
volumes: volumes:
- ./data:/app/data - ./data:/app/data
networks: networks:
- ipmi-network - ipmi-network
# Required for IPMI access to the local network # For Windows Docker Desktop, we need this for proper shutdown
# If your servers are on a different network, adjust accordingly stop_grace_period: 10s
extra_hosts:
- "host.docker.internal:host-gateway"
networks: networks:
ipmi-network: ipmi-network:

27
docker-config/nginx.conf Normal file
View File

@ -0,0 +1,27 @@
server {
listen 80;
server_name localhost;
client_max_body_size 50M;
location / {
proxy_pass http://ipmi-fan-control:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
location /api {
proxy_pass http://ipmi-fan-control:8000/api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@ -12,7 +12,6 @@ import {
Typography, Typography,
IconButton, IconButton,
Divider, Divider,
Avatar,
Menu, Menu,
MenuItem, MenuItem,
} from '@mui/material'; } from '@mui/material';

View File

View File

@ -26,7 +26,7 @@ import {
NavigateNext as NextIcon, NavigateNext as NextIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { dashboardApi } from '../utils/api'; import { dashboardApi } from '../utils/api';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer } from 'recharts';
export default function Dashboard() { export default function Dashboard() {
const navigate = useNavigate(); const navigate = useNavigate();

View File

@ -7,8 +7,6 @@ import {
Paper, Paper,
Grid, Grid,
Button, Button,
Card,
CardContent,
List, List,
ListItem, ListItem,
ListItemText, ListItemText,

View File

@ -25,7 +25,7 @@ import {
Speed as SpeedIcon, Speed as SpeedIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { logsApi, serversApi } from '../utils/api'; import { logsApi, serversApi } from '../utils/api';
import type { SystemLog } from '../types';
const LOGS_PER_PAGE = 25; const LOGS_PER_PAGE = 25;

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { import {
@ -29,11 +29,10 @@ import {
Speed as SpeedIcon, Speed as SpeedIcon,
Thermostat as TempIcon, Thermostat as TempIcon,
Power as PowerIcon, Power as PowerIcon,
Refresh as RefreshIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { serversApi, fanControlApi, dashboardApi } from '../utils/api'; import { serversApi, fanControlApi, dashboardApi } from '../utils/api';
import type { TemperatureReading, FanReading } from '../types';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts';
interface TabPanelProps { interface TabPanelProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -66,7 +65,7 @@ export default function ServerDetail() {
}, },
}); });
const { data: sensors, isLoading: isSensorsLoading, refetch: refetchSensors } = useQuery({ const { data: sensors } = useQuery({
queryKey: ['sensors', serverId], queryKey: ['sensors', serverId],
queryFn: async () => { queryFn: async () => {
const response = await serversApi.getSensors(serverId); const response = await serversApi.getSensors(serverId);
@ -124,7 +123,7 @@ export default function ServerDetail() {
} }
const cpuTemps = sensors?.temperatures.filter((t) => t.location.startsWith('cpu')) || []; const cpuTemps = sensors?.temperatures.filter((t) => t.location.startsWith('cpu')) || [];
const otherTemps = sensors?.temperatures.filter((t) => !t.location.startsWith('cpu')) || [];
return ( return (
<Box> <Box>

1
tests/__init__.py Normal file
View File

@ -0,0 +1 @@
# Tests for IPMI Fan Control

49
tests/test_auth.py Normal file
View File

@ -0,0 +1,49 @@
"""Tests for authentication module."""
import pytest
from backend.auth import (
get_password_hash,
verify_password,
create_access_token,
decode_access_token,
encrypt_password,
decrypt_password,
)
def test_password_hashing():
"""Test password hashing and verification."""
password = "testpassword123"
hashed = get_password_hash(password)
assert hashed != password
assert verify_password(password, hashed)
assert not verify_password("wrongpassword", hashed)
def test_jwt_tokens():
"""Test JWT token creation and decoding."""
data = {"sub": "testuser"}
token = create_access_token(data)
assert token is not None
decoded = decode_access_token(token)
assert decoded is not None
assert decoded["sub"] == "testuser"
def test_invalid_token():
"""Test decoding invalid token."""
decoded = decode_access_token("invalid.token.here")
assert decoded is None
def test_password_encryption():
"""Test password encryption and decryption."""
password = "secret_password"
encrypted = encrypt_password(password)
assert encrypted != password
decrypted = decrypt_password(encrypted)
assert decrypted == password

83
tests/test_fan_curve.py Normal file
View File

@ -0,0 +1,83 @@
"""Tests for fan curve logic."""
import pytest
from backend.fan_control import FanCurveManager, FanCurvePoint
def test_parse_curve():
"""Test parsing fan curve from JSON."""
json_data = '[{"temp": 30, "speed": 20}, {"temp": 50, "speed": 50}, {"temp": 70, "speed": 100}]'
curve = FanCurveManager.parse_curve(json_data)
assert len(curve) == 3
assert curve[0].temp == 30
assert curve[0].speed == 20
def test_parse_invalid_curve():
"""Test parsing invalid curve returns default."""
curve = FanCurveManager.parse_curve("invalid json")
assert len(curve) == 6 # Default curve has 6 points
assert curve[0].temp == 30
assert curve[0].speed == 10
def test_calculate_speed_below_min():
"""Test speed calculation below minimum temperature."""
curve = [
FanCurvePoint(30, 10),
FanCurvePoint(50, 50),
FanCurvePoint(70, 100),
]
speed = FanCurveManager.calculate_speed(curve, 20)
assert speed == 10
def test_calculate_speed_above_max():
"""Test speed calculation above maximum temperature."""
curve = [
FanCurvePoint(30, 10),
FanCurvePoint(50, 50),
FanCurvePoint(70, 100),
]
speed = FanCurveManager.calculate_speed(curve, 80)
assert speed == 100
def test_calculate_speed_interpolation():
"""Test speed calculation with interpolation."""
curve = [
FanCurvePoint(30, 10),
FanCurvePoint(50, 50),
FanCurvePoint(70, 100),
]
# At 40°C, should be halfway between 10% and 50%
speed = FanCurveManager.calculate_speed(curve, 40)
assert speed == 30
def test_calculate_speed_exact_point():
"""Test speed calculation at exact curve point."""
curve = [
FanCurvePoint(30, 10),
FanCurvePoint(50, 50),
]
speed = FanCurveManager.calculate_speed(curve, 50)
assert speed == 50
def test_serialize_curve():
"""Test serializing curve to JSON."""
points = [
FanCurvePoint(30, 10),
FanCurvePoint(50, 50),
]
json_data = FanCurveManager.serialize_curve(points)
assert "30" in json_data
assert "10" in json_data
assert "50" in json_data

85
tests/test_ipmi_client.py Normal file
View File

@ -0,0 +1,85 @@
"""Tests for IPMI client."""
import pytest
from unittest.mock import Mock, patch
from backend.ipmi_client import IPMIClient, TemperatureReading
def test_fan_mapping():
"""Test fan ID mapping."""
assert IPMIClient.FAN_MAPPING["0x00"] == 1
assert IPMIClient.FAN_MAPPING["0x06"] == 7
def test_hex_to_percent_conversion():
"""Test hex to percent conversion mapping."""
assert IPMIClient.HEX_TO_PERCENT["0x00"] == 0
assert IPMIClient.HEX_TO_PERCENT["0x64"] == 100
assert IPMIClient.PERCENT_TO_HEX[50] == "0x32"
assert IPMIClient.PERCENT_TO_HEX[100] == "0x64"
def test_determine_temp_location():
"""Test temperature location detection."""
client = IPMIClient("192.168.1.1", "user", "pass")
assert client._determine_temp_location("CPU1 Temp") == "cpu1"
assert client._determine_temp_location("CPU2 Temp") == "cpu2"
assert client._determine_temp_location("Processor 1") == "cpu1"
assert client._determine_temp_location("Inlet Temp") == "inlet"
assert client._determine_temp_location("Exhaust Temp") == "exhaust"
assert client._determine_temp_location("DIMM 1") == "memory"
assert client._determine_temp_location("Unknown Sensor") == "other"
def test_determine_sensor_type():
"""Test sensor type detection."""
client = IPMIClient("192.168.1.1", "user", "pass")
assert client._determine_sensor_type("CPU Temp") == "temperature"
assert client._determine_sensor_type("Fan 1 RPM") == "fan"
assert client._determine_sensor_type("12V") == "voltage"
assert client._determine_sensor_type("Power Supply") == "power"
def test_parse_sensor_value():
"""Test sensor value parsing."""
client = IPMIClient("192.168.1.1", "user", "pass")
value, unit = client._parse_sensor_value("45 degrees C")
assert value == 45.0
assert unit == "°C"
value, unit = client._parse_sensor_value("4200 RPM")
assert value == 4200.0
assert unit == "RPM"
value, unit = client._parse_sensor_value("12.05 Volts")
assert value == 12.05
assert unit == "V"
value, unit = client._parse_sensor_value("250 Watts")
assert value == 250.0
assert unit == "W"
@patch('backend.ipmi_client.subprocess.run')
def test_test_connection(mock_run):
"""Test connection test method."""
mock_run.return_value = Mock(returncode=0, stdout="", stderr="")
client = IPMIClient("192.168.1.1", "user", "pass")
result = client.test_connection()
assert result is True
mock_run.assert_called_once()
@patch('backend.ipmi_client.subprocess.run')
def test_test_connection_failure(mock_run):
"""Test connection test with failure."""
mock_run.return_value = Mock(returncode=1, stdout="", stderr="Error")
client = IPMIClient("192.168.1.1", "user", "pass")
result = client.test_connection()
assert result is False