Full-stack burn-in orchestration dashboard (Stages 1–6d complete): FastAPI backend, SQLite/WAL, SSE live dashboard, mock TrueNAS server, SMTP/webhook notifications, batch burn-in, settings UI, audit log, stats page, cancel SMART/burn-in, drag-to-reorder stages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
104 lines
2.9 KiB
Python
104 lines
2.9 KiB
Python
from pydantic import BaseModel, field_validator, model_validator
|
|
|
|
|
|
class SmartTestState(BaseModel):
|
|
state: str = "idle"
|
|
percent: int | None = None
|
|
eta_seconds: int | None = None
|
|
eta_timestamp: str | None = None
|
|
started_at: str | None = None
|
|
finished_at: str | None = None
|
|
error_text: str | None = None
|
|
|
|
|
|
_VALID_STAGE_NAMES = frozenset({"surface_validate", "short_smart", "long_smart"})
|
|
|
|
|
|
class StartBurninRequest(BaseModel):
|
|
drive_ids: list[int]
|
|
operator: str
|
|
run_surface: bool = True
|
|
run_short: bool = True
|
|
run_long: bool = True
|
|
stage_order: list[str] | None = None # custom execution order, e.g. ["short_smart","long_smart","surface_validate"]
|
|
|
|
@field_validator("operator")
|
|
@classmethod
|
|
def validate_operator(cls, v: str) -> str:
|
|
v = v.strip()
|
|
if not v:
|
|
raise ValueError("operator must not be empty")
|
|
return v
|
|
|
|
@model_validator(mode="after")
|
|
def validate_stages(self) -> "StartBurninRequest":
|
|
if not (self.run_surface or self.run_short or self.run_long):
|
|
raise ValueError("At least one stage must be selected")
|
|
if self.stage_order is not None:
|
|
for s in self.stage_order:
|
|
if s not in _VALID_STAGE_NAMES:
|
|
raise ValueError(f"Invalid stage name in stage_order: {s!r}")
|
|
return self
|
|
|
|
@property
|
|
def profile(self) -> str:
|
|
_MAP = {
|
|
(True, True, True): "full",
|
|
(True, True, False): "surface_short",
|
|
(True, False, True): "surface_long",
|
|
(True, False, False): "surface",
|
|
(False, True, True): "short_long",
|
|
(False, True, False): "short",
|
|
(False, False, True): "long",
|
|
}
|
|
return _MAP[(self.run_surface, self.run_short, self.run_long)]
|
|
|
|
|
|
class CancelBurninRequest(BaseModel):
|
|
operator: str = "unknown"
|
|
|
|
|
|
class BurninStageResponse(BaseModel):
|
|
id: int
|
|
stage_name: str
|
|
state: str
|
|
percent: int = 0
|
|
started_at: str | None = None
|
|
finished_at: str | None = None
|
|
error_text: str | None = None
|
|
|
|
|
|
class BurninJobResponse(BaseModel):
|
|
id: int
|
|
drive_id: int
|
|
profile: str
|
|
state: str
|
|
percent: int = 0
|
|
stage_name: str | None = None
|
|
operator: str
|
|
created_at: str
|
|
started_at: str | None = None
|
|
finished_at: str | None = None
|
|
error_text: str | None = None
|
|
stages: list[BurninStageResponse] = []
|
|
|
|
|
|
class DriveResponse(BaseModel):
|
|
id: int
|
|
devname: str
|
|
serial: str | None = None
|
|
model: str | None = None
|
|
size_bytes: int | None = None
|
|
temperature_c: int | None = None
|
|
smart_health: str = "UNKNOWN"
|
|
last_polled_at: str
|
|
is_stale: bool
|
|
smart_short: SmartTestState
|
|
smart_long: SmartTestState
|
|
notes: str | None = None
|
|
location: str | None = None
|
|
|
|
|
|
class UpdateDriveRequest(BaseModel):
|
|
notes: str | None = None
|
|
location: str | None = None
|