Remove Node.js cache files containing npm vulnerability data for vitest and vite packages
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled

This commit is contained in:
2025-12-15 20:36:06 -05:00
parent 27eed55c9b
commit 68a9b0f390
123 changed files with 13216 additions and 157 deletions
+8
View File
@@ -9,6 +9,10 @@ from .user import User, UserRole
from .host_metrics import HostMetrics
from .alert import Alert
from .app_setting import AppSetting
from .docker_container import DockerContainer
from .docker_image import DockerImage
from .docker_volume import DockerVolume
from .docker_alert import DockerAlert
__all__ = [
"Base",
@@ -23,4 +27,8 @@ __all__ = [
"UserRole",
"HostMetrics",
"AppSetting",
"DockerContainer",
"DockerImage",
"DockerVolume",
"DockerAlert",
]
+13 -1
View File
@@ -112,7 +112,19 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]:
async def init_db() -> None:
"""Create all tables (mostly for dev/tests; migrations should be handled by Alembic)."""
from . import host, task, schedule, schedule_run, log, alert, app_setting # noqa: F401
from . import (
host,
task,
schedule,
schedule_run,
log,
alert,
app_setting,
docker_container,
docker_image,
docker_volume,
docker_alert,
) # noqa: F401
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
+55
View File
@@ -0,0 +1,55 @@
"""Docker Alert model for Homelab Automation."""
from __future__ import annotations
from datetime import datetime
from typing import Optional
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, Index
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from .database import Base
class DockerAlert(Base):
"""Model representing a Docker alert for container issues."""
__tablename__ = "docker_alerts"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
container_name: Mapped[str] = mapped_column(String(255), nullable=False)
severity: Mapped[str] = mapped_column(String(20), nullable=False, default="warning") # warning/error/critical
state: Mapped[str] = mapped_column(String(20), nullable=False, default="open") # open/closed/acknowledged
message: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
opened_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
closed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
acknowledged_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
acknowledged_by: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
last_notified_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
# Relationship to host
host: Mapped["Host"] = relationship("Host", back_populates="docker_alerts")
__table_args__ = (
Index("idx_docker_alerts_state_host", "state", "host_id"),
{"sqlite_autoincrement": True},
)
def __repr__(self) -> str: # pragma: no cover
return f"<DockerAlert id={self.id} container={self.container_name} severity={self.severity}>"
def to_dict(self) -> dict:
"""Convert to dictionary for API responses."""
return {
"id": self.id,
"host_id": self.host_id,
"container_name": self.container_name,
"severity": self.severity,
"state": self.state,
"message": self.message,
"opened_at": self.opened_at.isoformat() if self.opened_at else None,
"closed_at": self.closed_at.isoformat() if self.closed_at else None,
"acknowledged_at": self.acknowledged_at.isoformat() if self.acknowledged_at else None,
"acknowledged_by": self.acknowledged_by,
"last_notified_at": self.last_notified_at.isoformat() if self.last_notified_at else None,
}
+61
View File
@@ -0,0 +1,61 @@
"""Docker Container model for Homelab Automation."""
from __future__ import annotations
from datetime import datetime
from typing import Optional
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, JSON, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from .database import Base
class DockerContainer(Base):
"""Model representing a Docker container on a host."""
__tablename__ = "docker_containers"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
container_id: Mapped[str] = mapped_column(String(64), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
image: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
state: Mapped[str] = mapped_column(String(20), nullable=False, default="unknown") # running/exited/paused/created/dead
status: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # Up 2 hours, Exited (0) 5 minutes ago
health: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # healthy/unhealthy/starting/none
created_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
ports: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
labels: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
compose_project: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # com.docker.compose.project
last_update_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
# Relationship to host
host: Mapped["Host"] = relationship("Host", back_populates="docker_containers")
__table_args__ = (
UniqueConstraint('host_id', 'container_id', name='uq_docker_containers_host_container'),
{"sqlite_autoincrement": True},
)
def __repr__(self) -> str: # pragma: no cover
return f"<DockerContainer id={self.id} name={self.name} state={self.state}>"
def to_dict(self) -> dict:
"""Convert to dictionary for API responses."""
return {
"id": self.id,
"host_id": self.host_id,
"container_id": self.container_id,
"name": self.name,
"image": self.image,
"state": self.state,
"status": self.status,
"health": self.health,
"created_at": self.created_at.isoformat() if self.created_at else None,
"ports": self.ports,
"labels": self.labels,
"compose_project": self.compose_project,
"last_update_at": self.last_update_at.isoformat() if self.last_update_at else None,
}
+50
View File
@@ -0,0 +1,50 @@
"""Docker Image model for Homelab Automation."""
from __future__ import annotations
from datetime import datetime
from typing import Optional
from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, String, JSON, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from .database import Base
class DockerImage(Base):
"""Model representing a Docker image on a host."""
__tablename__ = "docker_images"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
image_id: Mapped[str] = mapped_column(String(64), nullable=False)
repo_tags: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) # ["nginx:latest", "nginx:1.25"]
size: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True)
created: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
last_update_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
# Relationship to host
host: Mapped["Host"] = relationship("Host", back_populates="docker_images")
__table_args__ = (
UniqueConstraint('host_id', 'image_id', name='uq_docker_images_host_image'),
{"sqlite_autoincrement": True},
)
def __repr__(self) -> str: # pragma: no cover
tags = self.repo_tags[0] if self.repo_tags else self.image_id[:12]
return f"<DockerImage id={self.id} tags={tags}>"
def to_dict(self) -> dict:
"""Convert to dictionary for API responses."""
return {
"id": self.id,
"host_id": self.host_id,
"image_id": self.image_id,
"repo_tags": self.repo_tags,
"size": self.size,
"created": self.created.isoformat() if self.created else None,
"last_update_at": self.last_update_at.isoformat() if self.last_update_at else None,
}
+49
View File
@@ -0,0 +1,49 @@
"""Docker Volume model for Homelab Automation."""
from __future__ import annotations
from datetime import datetime
from typing import Optional
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from .database import Base
class DockerVolume(Base):
"""Model representing a Docker volume on a host."""
__tablename__ = "docker_volumes"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
driver: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
mountpoint: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
scope: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # local/global
last_update_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
# Relationship to host
host: Mapped["Host"] = relationship("Host", back_populates="docker_volumes")
__table_args__ = (
UniqueConstraint('host_id', 'name', name='uq_docker_volumes_host_name'),
{"sqlite_autoincrement": True},
)
def __repr__(self) -> str: # pragma: no cover
return f"<DockerVolume id={self.id} name={self.name}>"
def to_dict(self) -> dict:
"""Convert to dictionary for API responses."""
return {
"id": self.id,
"host_id": self.host_id,
"name": self.name,
"driver": self.driver,
"mountpoint": self.mountpoint,
"scope": self.scope,
"last_update_at": self.last_update_at.isoformat() if self.last_update_at else None,
}
+18
View File
@@ -24,12 +24,30 @@ class Host(Base):
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
# Docker-related fields
docker_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("0"))
docker_version: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
docker_status: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # online/offline/error
docker_last_collect_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
bootstrap_statuses: Mapped[List["BootstrapStatus"]] = relationship(
"BootstrapStatus", back_populates="host", cascade="all, delete-orphan"
)
metrics: Mapped[List["HostMetrics"]] = relationship(
"HostMetrics", back_populates="host", cascade="all, delete-orphan"
)
docker_containers: Mapped[List["DockerContainer"]] = relationship(
"DockerContainer", back_populates="host", cascade="all, delete-orphan"
)
docker_images: Mapped[List["DockerImage"]] = relationship(
"DockerImage", back_populates="host", cascade="all, delete-orphan"
)
docker_volumes: Mapped[List["DockerVolume"]] = relationship(
"DockerVolume", back_populates="host", cascade="all, delete-orphan"
)
docker_alerts: Mapped[List["DockerAlert"]] = relationship(
"DockerAlert", back_populates="host", cascade="all, delete-orphan"
)
def __repr__(self) -> str: # pragma: no cover - debug helper
return f"<Host id={self.id} name={self.name} ip={self.ip_address}>"