diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py index 892fb89..59f0e68 100644 --- a/backend/app/api/routes/auth.py +++ b/backend/app/api/routes/auth.py @@ -26,7 +26,7 @@ def _load_credentials() -> tuple[str, str]: @router.post("/login", response_model=TokenResponse) -async def login(body: LoginRequest): +async def login(body: LoginRequest) -> TokenResponse: username, password_hash = _load_credentials() if body.username != username or not verify_password(body.password, password_hash): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") diff --git a/backend/app/api/routes/canvas.py b/backend/app/api/routes/canvas.py index cdfdd9b..2d4f143 100644 --- a/backend/app/api/routes/canvas.py +++ b/backend/app/api/routes/canvas.py @@ -1,4 +1,5 @@ from datetime import UTC, datetime +from typing import Any from fastapi import APIRouter, Depends from sqlalchemy import select @@ -8,21 +9,29 @@ from app.api.deps import get_current_user from app.db.database import get_db from app.db.models import CanvasState, Edge, Node from app.schemas.canvas import CanvasSaveRequest, CanvasStateResponse +from app.schemas.edges import EdgeResponse +from app.schemas.nodes import NodeResponse router = APIRouter() @router.get("", response_model=CanvasStateResponse) -async def load_canvas(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): +async def load_canvas(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> CanvasStateResponse: nodes = (await db.execute(select(Node))).scalars().all() edges = (await db.execute(select(Edge))).scalars().all() state = await db.get(CanvasState, 1) - viewport = state.viewport if state else {"x": 0, "y": 0, "zoom": 1} - return CanvasStateResponse(nodes=list(nodes), edges=list(edges), viewport=viewport) + viewport: dict[str, Any] = state.viewport if state else {"x": 0, "y": 0, "zoom": 1} + return CanvasStateResponse( + nodes=[NodeResponse.model_validate(n) for n in nodes], + edges=[EdgeResponse.model_validate(e) for e in edges], + viewport=viewport, + ) @router.post("/save") -async def save_canvas(body: CanvasSaveRequest, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): +async def save_canvas( + body: CanvasSaveRequest, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user) +) -> dict[str, bool]: incoming_node_ids = {n.id for n in body.nodes} incoming_edge_ids = {e.id for e in body.edges} @@ -42,19 +51,19 @@ async def save_canvas(body: CanvasSaveRequest, db: AsyncSession = Depends(get_db # Upsert nodes for node_data in body.nodes: - node = await db.get(Node, node_data.id) - if node: + db_node = await db.get(Node, node_data.id) + if db_node: for field, value in node_data.model_dump().items(): - setattr(node, field, value) + setattr(db_node, field, value) else: db.add(Node(**node_data.model_dump())) # Upsert edges for edge_data in body.edges: - edge = await db.get(Edge, edge_data.id) - if edge: + db_edge = await db.get(Edge, edge_data.id) + if db_edge: for field, value in edge_data.model_dump().items(): - setattr(edge, field, value) + setattr(db_edge, field, value) else: db.add(Edge(**edge_data.model_dump())) diff --git a/backend/app/api/routes/edges.py b/backend/app/api/routes/edges.py index c546bac..ba49256 100644 --- a/backend/app/api/routes/edges.py +++ b/backend/app/api/routes/edges.py @@ -11,13 +11,13 @@ router = APIRouter() @router.get("", response_model=list[EdgeResponse]) -async def list_edges(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): +async def list_edges(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> list[Edge]: result = await db.execute(select(Edge)) - return result.scalars().all() + return list(result.scalars().all()) @router.post("", response_model=EdgeResponse, status_code=status.HTTP_201_CREATED) -async def create_edge(body: EdgeCreate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): +async def create_edge(body: EdgeCreate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> Edge: edge = Edge(**body.model_dump()) db.add(edge) await db.commit() @@ -26,7 +26,7 @@ async def create_edge(body: EdgeCreate, db: AsyncSession = Depends(get_db), _: s @router.delete("/{edge_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_edge(edge_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): +async def delete_edge(edge_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> None: edge = await db.get(Edge, edge_id) if not edge: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Edge not found") @@ -37,7 +37,7 @@ async def delete_edge(edge_id: str, db: AsyncSession = Depends(get_db), _: str = @router.patch("/{edge_id}", response_model=EdgeResponse) async def update_edge( edge_id: str, body: EdgeUpdate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user) -): +) -> Edge: edge = await db.get(Edge, edge_id) if not edge: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Edge not found") diff --git a/backend/app/api/routes/nodes.py b/backend/app/api/routes/nodes.py index d7025f1..a773995 100644 --- a/backend/app/api/routes/nodes.py +++ b/backend/app/api/routes/nodes.py @@ -11,13 +11,13 @@ router = APIRouter() @router.get("", response_model=list[NodeResponse]) -async def list_nodes(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): +async def list_nodes(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> list[Node]: result = await db.execute(select(Node)) - return result.scalars().all() + return list(result.scalars().all()) @router.post("", response_model=NodeResponse, status_code=status.HTTP_201_CREATED) -async def create_node(body: NodeCreate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): +async def create_node(body: NodeCreate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> Node: node = Node(**body.model_dump()) db.add(node) await db.commit() @@ -26,7 +26,7 @@ async def create_node(body: NodeCreate, db: AsyncSession = Depends(get_db), _: s @router.get("/{node_id}", response_model=NodeResponse) -async def get_node(node_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): +async def get_node(node_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> Node: node = await db.get(Node, node_id) if not node: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Node not found") @@ -36,7 +36,7 @@ async def get_node(node_id: str, db: AsyncSession = Depends(get_db), _: str = De @router.patch("/{node_id}", response_model=NodeResponse) async def update_node( node_id: str, body: NodeUpdate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user) -): +) -> Node: node = await db.get(Node, node_id) if not node: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Node not found") @@ -48,7 +48,7 @@ async def update_node( @router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_node(node_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): +async def delete_node(node_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> None: node = await db.get(Node, node_id) if not node: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Node not found") diff --git a/backend/app/api/routes/scan.py b/backend/app/api/routes/scan.py index aa6a4ba..dd1aec4 100644 --- a/backend/app/api/routes/scan.py +++ b/backend/app/api/routes/scan.py @@ -1,4 +1,5 @@ import logging +from typing import Any import yaml from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException @@ -26,8 +27,8 @@ router = APIRouter() def _load_ranges() -> list[str]: try: with open(settings.config_path) as f: - cfg = yaml.safe_load(f) - return cfg.get("scanner", {}).get("ranges", []) + cfg: dict[str, Any] = yaml.safe_load(f) or {} + return list(cfg.get("scanner", {}).get("ranges", [])) except Exception: return [] @@ -42,7 +43,7 @@ async def trigger_scan( background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user), -): +) -> ScanRun: ranges = _load_ranges() run = ScanRun(status="running", ranges=ranges) db.add(run) @@ -53,15 +54,15 @@ async def trigger_scan( @router.get("/pending", response_model=list[PendingDeviceResponse]) -async def list_pending(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): +async def list_pending(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> list[PendingDevice]: result = await db.execute(select(PendingDevice).where(PendingDevice.status == "pending")) - return result.scalars().all() + return list(result.scalars().all()) @router.get("/hidden", response_model=list[PendingDeviceResponse]) -async def list_hidden(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): +async def list_hidden(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> list[PendingDevice]: result = await db.execute(select(PendingDevice).where(PendingDevice.status == "hidden")) - return result.scalars().all() + return list(result.scalars().all()) @router.post("/pending/{device_id}/approve", response_model=dict) @@ -70,7 +71,7 @@ async def approve_device( node_data: NodeCreate, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user), -): +) -> dict[str, Any]: device = await db.get(PendingDevice, device_id) if device: device.status = "approved" @@ -82,7 +83,9 @@ async def approve_device( @router.post("/pending/{device_id}/hide") -async def hide_device(device_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): +async def hide_device( + device_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user) +) -> dict[str, bool]: device = await db.get(PendingDevice, device_id) if device: device.status = "hidden" @@ -91,7 +94,9 @@ async def hide_device(device_id: str, db: AsyncSession = Depends(get_db), _: str @router.post("/pending/{device_id}/ignore") -async def ignore_device(device_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): +async def ignore_device( + device_id: str, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user) +) -> dict[str, bool]: device = await db.get(PendingDevice, device_id) if device: await db.delete(device) @@ -100,13 +105,13 @@ async def ignore_device(device_id: str, db: AsyncSession = Depends(get_db), _: s @router.get("/runs", response_model=list[ScanRunResponse]) -async def list_runs(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): +async def list_runs(db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)) -> list[ScanRun]: result = await db.execute(select(ScanRun).order_by(ScanRun.started_at.desc()).limit(20)) - return result.scalars().all() + return list(result.scalars().all()) @router.get("/config", response_model=ScanConfig) -async def get_scan_config(_: str = Depends(get_current_user)): +async def get_scan_config(_: str = Depends(get_current_user)) -> ScanConfig: try: with open(settings.config_path) as f: cfg = yaml.safe_load(f) @@ -118,7 +123,7 @@ async def get_scan_config(_: str = Depends(get_current_user)): @router.post("/config", response_model=ScanConfig) -async def update_scan_config(payload: ScanConfig, _: str = Depends(get_current_user)): +async def update_scan_config(payload: ScanConfig, _: str = Depends(get_current_user)) -> ScanConfig: try: with open(settings.config_path) as f: cfg = yaml.safe_load(f) or {} diff --git a/backend/app/api/routes/status.py b/backend/app/api/routes/status.py index 214c380..c7524a3 100644 --- a/backend/app/api/routes/status.py +++ b/backend/app/api/routes/status.py @@ -9,7 +9,7 @@ _connections: list[WebSocket] = [] @router.websocket("/ws/status") -async def ws_status(websocket: WebSocket): +async def ws_status(websocket: WebSocket) -> None: await websocket.accept() _connections.append(websocket) try: @@ -19,7 +19,7 @@ async def ws_status(websocket: WebSocket): _connections.remove(websocket) -async def broadcast_status(node_id: str, status: str, checked_at: str, response_time_ms: int | None = None): +async def broadcast_status(node_id: str, status: str, checked_at: str, response_time_ms: int | None = None) -> None: payload = json.dumps({ "node_id": node_id, "status": status, diff --git a/backend/app/core/scheduler.py b/backend/app/core/scheduler.py index abd9fe9..dfe2376 100644 --- a/backend/app/core/scheduler.py +++ b/backend/app/core/scheduler.py @@ -28,19 +28,19 @@ async def _run_status_checks() -> None: if not node.check_method: continue try: - result = await check_node(node.check_method, node.check_target, node.ip) + check_result = await check_node(node.check_method, node.check_target, node.ip) async with AsyncSessionLocal() as db: n = await db.get(Node, node.id) if n: - n.status = result["status"] - n.response_time_ms = result["response_time_ms"] - n.last_seen = datetime.now(UTC) if result["status"] == "online" else n.last_seen + n.status = check_result["status"] + n.response_time_ms = check_result["response_time_ms"] + n.last_seen = datetime.now(UTC) if check_result["status"] == "online" else n.last_seen await db.commit() await broadcast_status( node_id=node.id, - status=result["status"], + status=check_result["status"], checked_at=datetime.now(UTC).isoformat(), - response_time_ms=result["response_time_ms"], + response_time_ms=check_result["response_time_ms"], ) except Exception as exc: logger.error("Status check failed for node %s: %s", node.id, exc) diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 5a9b967..fba4b9b 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -9,22 +9,23 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def verify_password(plain: str, hashed: str) -> bool: - return pwd_context.verify(plain, hashed) + return bool(pwd_context.verify(plain, hashed)) def hash_password(password: str) -> str: - return pwd_context.hash(password) + return str(pwd_context.hash(password)) def create_access_token(subject: str) -> str: expire = datetime.now(UTC) + timedelta(minutes=settings.access_token_expire_minutes) payload = {"sub": subject, "exp": expire} - return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm) + return str(jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)) def decode_token(token: str) -> str | None: try: payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm]) - return payload.get("sub") + sub = payload.get("sub") + return str(sub) if sub is not None else None except JWTError: return None diff --git a/backend/app/db/database.py b/backend/app/db/database.py index 653e995..f4532ff 100644 --- a/backend/app/db/database.py +++ b/backend/app/db/database.py @@ -1,3 +1,4 @@ +from collections.abc import AsyncGenerator from contextlib import suppress from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine @@ -31,6 +32,6 @@ async def init_db() -> None: await conn.exec_driver_sql("ALTER TABLE edges ADD COLUMN path_style TEXT") -async def get_db(): +async def get_db() -> AsyncGenerator[AsyncSession, None]: async with AsyncSessionLocal() as session: yield session diff --git a/backend/app/db/models.py b/backend/app/db/models.py index 4af7884..0726db2 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -1,5 +1,6 @@ import uuid from datetime import UTC, datetime +from typing import Any from sqlalchemy import JSON, Boolean, DateTime, Float, ForeignKey, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -28,13 +29,13 @@ class Node(Base): status: Mapped[str] = mapped_column(String, default="unknown") check_method: Mapped[str | None] = mapped_column(String) check_target: Mapped[str | None] = mapped_column(String) - services: Mapped[list] = mapped_column(JSON, default=list) + services: Mapped[list[Any]] = mapped_column(JSON, default=list) notes: Mapped[str | None] = mapped_column(Text) pos_x: Mapped[float] = mapped_column(Float, default=0) pos_y: Mapped[float] = mapped_column(Float, default=0) parent_id: Mapped[str | None] = mapped_column(String, ForeignKey("nodes.id")) container_mode: Mapped[bool] = mapped_column(Boolean, default=False) - custom_colors: Mapped[dict | None] = mapped_column(JSON, nullable=True) + custom_colors: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) response_time_ms: Mapped[int | None] = mapped_column(Integer) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now) @@ -63,7 +64,7 @@ class CanvasState(Base): __tablename__ = "canvas_state" id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1) - viewport: Mapped[dict] = mapped_column(JSON, default=dict) + viewport: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) saved_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now) @@ -75,7 +76,7 @@ class PendingDevice(Base): mac: Mapped[str | None] = mapped_column(String) hostname: Mapped[str | None] = mapped_column(String) os: Mapped[str | None] = mapped_column(String) - services: Mapped[list] = mapped_column(JSON, default=list) + services: Mapped[list[Any]] = mapped_column(JSON, default=list) suggested_type: Mapped[str | None] = mapped_column(String) status: Mapped[str] = mapped_column(String, default="pending") discovered_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now) @@ -86,7 +87,7 @@ class ScanRun(Base): id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) status: Mapped[str] = mapped_column(String, default="running") - ranges: Mapped[list] = mapped_column(JSON, default=list) + ranges: Mapped[list[str]] = mapped_column(JSON, default=list) devices_found: Mapped[int] = mapped_column(Integer, default=0) started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) diff --git a/backend/app/main.py b/backend/app/main.py index f66a36e..f01819e 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,4 +1,6 @@ +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager +from typing import Any from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -10,7 +12,7 @@ from app.db.database import init_db @asynccontextmanager -async def lifespan(app: FastAPI): +async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: await init_db() start_scheduler() yield @@ -40,5 +42,5 @@ app.include_router(status.router, prefix="/api/v1/status", tags=["status"]) @app.get("/api/v1/health") -async def health(): +async def health() -> dict[str, Any]: return {"status": "ok"} diff --git a/backend/app/schemas/canvas.py b/backend/app/schemas/canvas.py index 1e8c614..87c31d9 100644 --- a/backend/app/schemas/canvas.py +++ b/backend/app/schemas/canvas.py @@ -21,7 +21,7 @@ class NodeSave(BaseModel): notes: str | None = None parent_id: str | None = None container_mode: bool = False - custom_colors: dict | None = None + custom_colors: dict[str, Any] | None = None pos_x: float = 0 pos_y: float = 0 @@ -41,10 +41,10 @@ class EdgeSave(BaseModel): class CanvasSaveRequest(BaseModel): nodes: list[NodeSave] = [] edges: list[EdgeSave] = [] - viewport: dict = {} + viewport: dict[str, Any] = {} class CanvasStateResponse(BaseModel): nodes: list[NodeResponse] edges: list[EdgeResponse] - viewport: dict + viewport: dict[str, Any] diff --git a/backend/app/schemas/nodes.py b/backend/app/schemas/nodes.py index 48fd3e7..55b8084 100644 --- a/backend/app/schemas/nodes.py +++ b/backend/app/schemas/nodes.py @@ -20,7 +20,7 @@ class NodeBase(BaseModel): pos_y: float = 0 parent_id: str | None = None container_mode: bool = False - custom_colors: dict | None = None + custom_colors: dict[str, Any] | None = None class NodeCreate(NodeBase): @@ -42,7 +42,7 @@ class NodeUpdate(BaseModel): pos_x: float | None = None pos_y: float | None = None container_mode: bool | None = None - custom_colors: dict | None = None + custom_colors: dict[str, Any] | None = None class NodeResponse(NodeBase): diff --git a/backend/app/services/fingerprint.py b/backend/app/services/fingerprint.py index fa85c5f..caa47ec 100644 --- a/backend/app/services/fingerprint.py +++ b/backend/app/services/fingerprint.py @@ -2,11 +2,12 @@ import json import re from pathlib import Path +from typing import Any -_SIGNATURES: list[dict] | None = None +_SIGNATURES: list[dict[str, Any]] | None = None -def _load() -> list[dict]: +def _load() -> list[dict[str, Any]]: global _SIGNATURES if _SIGNATURES is None: path = Path(__file__).parent.parent.parent / "data" / "service_signatures.json" @@ -15,7 +16,7 @@ def _load() -> list[dict]: return _SIGNATURES -def match_port(port: int, protocol: str, banner: str | None = None) -> dict | None: +def match_port(port: int, protocol: str, banner: str | None = None) -> dict[str, Any] | None: """Return the first signature matching port+protocol, optionally banner.""" for sig in _load(): if sig["port"] != port or sig["protocol"] != protocol: @@ -26,7 +27,7 @@ def match_port(port: int, protocol: str, banner: str | None = None) -> dict | No return None -def fingerprint_ports(open_ports: list[dict]) -> list[dict]: +def fingerprint_ports(open_ports: list[dict[str, Any]]) -> list[dict[str, Any]]: """ Given a list of {port, protocol, banner?} dicts, return matched services. Unknown ports are included as unknown_service. @@ -53,7 +54,7 @@ def fingerprint_ports(open_ports: list[dict]) -> list[dict]: return results -def suggest_node_type(open_ports: list[dict]) -> str: +def suggest_node_type(open_ports: list[dict[str, Any]]) -> str: """Suggest a node type based on the most specific matched signature.""" priority = ["proxmox", "nas", "router", "lxc", "vm", "server", "ap", "iot", "switch"] found: set[str] = set() diff --git a/backend/app/services/scanner.py b/backend/app/services/scanner.py index 5b8ad98..17c96e4 100644 --- a/backend/app/services/scanner.py +++ b/backend/app/services/scanner.py @@ -2,6 +2,7 @@ import logging import socket from datetime import UTC, datetime +from typing import Any from sqlalchemy.ext.asyncio import AsyncSession @@ -11,14 +12,14 @@ from app.services.fingerprint import fingerprint_ports, suggest_node_type logger = logging.getLogger(__name__) try: - import nmap # type: ignore[import-untyped] + import nmap _NMAP_AVAILABLE = True except ImportError: _NMAP_AVAILABLE = False logger.warning("python-nmap not available — scanner will run in mock mode") -def _nmap_scan(target: str) -> list[dict]: +def _nmap_scan(target: str) -> list[dict[str, Any]]: """Run nmap -sV --open on target, return list of host dicts.""" if not _NMAP_AVAILABLE: return _mock_scan(target) @@ -64,13 +65,13 @@ def _extract_os(nm: object, host: str) -> str | None: try: osmatch = nm[host].get("osmatch", []) # type: ignore[index] if osmatch: - return osmatch[0]["name"] + return str(osmatch[0]["name"]) except Exception: pass return None -def _mock_scan(target: str) -> list[dict]: +def _mock_scan(target: str) -> list[dict[str, Any]]: """Return fake results for dev/test environments without nmap.""" return [ { diff --git a/backend/app/services/status_checker.py b/backend/app/services/status_checker.py index 4aa1d67..90ff6c0 100644 --- a/backend/app/services/status_checker.py +++ b/backend/app/services/status_checker.py @@ -3,13 +3,14 @@ import asyncio import logging import socket import time +from typing import Any import httpx logger = logging.getLogger(__name__) -async def check_node(check_method: str, target: str | None, ip: str | None) -> dict: +async def check_node(check_method: str, target: str | None, ip: str | None) -> dict[str, Any]: """ Run the appropriate check and return {status, response_time_ms}. status is one of: online, offline, unknown. diff --git a/backend/requirements.txt b/backend/requirements.txt index 8c48b09..3eaef79 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -13,6 +13,7 @@ python-multipart==0.0.22 apscheduler==3.10.4 python-nmap==0.7.1 pyyaml==6.0.2 +types-PyYAML==6.0.12.20240917 websockets==13.1 httpx==0.27.2