@@ -320,9 +369,15 @@ export default function MachineDetailPage() {
| {COUNTER_SOURCES.find((s) => s.value === part.counter_source)?.label || part.counter_source} |
-
+ {part.counter_source === 'auto_time' || part.lifecycle_type === 'date' ? (
+
+ timer
+
+ ) : (
+
+ )}
diff --git a/dashboard/app/globals.css b/dashboard/app/globals.css
index eda102e..8659d98 100644
--- a/dashboard/app/globals.css
+++ b/dashboard/app/globals.css
@@ -1922,6 +1922,50 @@ a {
font-size: 18px;
}
+.spec-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16px;
+ padding: 16px 0;
+}
+
+.spec-item {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.spec-item-full {
+ grid-column: 1 / -1;
+}
+
+.spec-label {
+ font-size: 12px;
+ color: var(--md-on-surface-variant);
+ font-weight: 500;
+}
+
+.spec-value {
+ font-size: 14px;
+ color: var(--md-on-surface);
+}
+
+.btn-icon-disabled {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ opacity: 0.4;
+ cursor: default;
+}
+
+.btn-icon-disabled .material-symbols-outlined {
+ font-size: 18px;
+ color: var(--md-on-surface-variant);
+}
+
/* ===== Responsive ===== */
@media (max-width: 768px) {
.topnav-center {
@@ -1970,6 +2014,53 @@ a {
.part-table td {
padding: 8px 10px;
}
+
+ .spec-grid {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 12px;
+ }
+
+ .page-container {
+ padding: 16px;
+ }
+
+ .detail-header {
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .modal-content.modal-lg {
+ width: 95vw;
+ max-width: none;
+ }
+
+ .part-table-wrap {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .history-table {
+ font-size: 12px;
+ }
+
+ .alarm-card {
+ padding: 12px;
+ }
+
+ .alarm-card-header {
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .section-header {
+ flex-direction: column;
+ gap: 8px;
+ align-items: flex-start;
+ }
+
+ .inspection-summary-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
}
@media (max-width: 480px) {
@@ -1995,4 +2086,48 @@ a {
.tenant-grid {
grid-template-columns: 1fr;
}
+
+ .spec-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .page-container {
+ padding: 12px;
+ }
+
+ .detail-title {
+ font-size: 18px;
+ }
+
+ .detail-subtitle {
+ font-size: 12px;
+ }
+
+ .part-table th:nth-child(n+6),
+ .part-table td:nth-child(n+6) {
+ display: none;
+ }
+
+ .btn-primary,
+ .btn-outline {
+ font-size: 13px;
+ padding: 8px 14px;
+ }
+
+ .modal-content {
+ margin: 8px;
+ max-height: 95vh;
+ }
+
+ .modal-content.modal-sm {
+ width: 95vw;
+ }
+
+ .topnav {
+ padding: 0 12px;
+ }
+
+ .inspection-summary-grid {
+ grid-template-columns: 1fr;
+ }
}
diff --git a/dashboard/lib/types.ts b/dashboard/lib/types.ts
index af698c1..126bb62 100644
--- a/dashboard/lib/types.ts
+++ b/dashboard/lib/types.ts
@@ -27,6 +27,12 @@ export interface Machine {
name: string;
equipment_code: string;
model: string | null;
+ manufacturer: string | null;
+ installation_date: string | null;
+ location: string | null;
+ rated_capacity: string | null;
+ power_rating: string | null;
+ description: string | null;
parts_count: number;
created_at: string | null;
updated_at: string | null;
@@ -146,12 +152,13 @@ export interface PartReplacementLog {
export interface Alarm {
id: string;
tenant_id: string;
- equipment_part_id: string;
- machine_id: string;
- alarm_type: 'threshold' | 'critical';
+ equipment_part_id: string | null;
+ machine_id: string | null;
+ inspection_session_id: string | null;
+ alarm_type: 'threshold' | 'critical' | 'inspection_fail';
severity: 'warning' | 'critical';
message: string;
- lifecycle_pct_at_trigger: number;
+ lifecycle_pct_at_trigger: number | null;
is_acknowledged: boolean;
acknowledged_by: string | null;
acknowledged_at: string | null;
diff --git a/scripts/seed.py b/scripts/seed.py
index e57d1d0..2a756df 100644
--- a/scripts/seed.py
+++ b/scripts/seed.py
@@ -78,12 +78,30 @@ MACHINES = {
"name": "CVD Chamber #1",
"equipment_code": "SF-CVD-001",
"model": "AMAT Centura 5200",
+ "manufacturer": "Applied Materials",
+ "location": "클린룸 Bay 3",
+ "rated_capacity": "200mm 웨이퍼",
+ "power_rating": "30kW",
+ "description": "화학기상증착 장비. SiO2, Si3N4 박막 증착용.",
+ },
+ {
+ "name": "Etcher DRY-A",
+ "equipment_code": "SF-ETCH-001",
+ "model": "LAM 9600",
+ "manufacturer": "Lam Research",
+ "location": "클린룸 Bay 5",
+ "rated_capacity": "200mm 웨이퍼",
+ "power_rating": "25kW",
+ "description": "건식 식각 장비. Poly-Si, Metal 식각용.",
},
- {"name": "Etcher DRY-A", "equipment_code": "SF-ETCH-001", "model": "LAM 9600"},
{
"name": "Sputter MC-200",
"equipment_code": "SF-SPT-001",
"model": "Ulvac SH-450",
+ "manufacturer": "ULVAC",
+ "location": "클린룸 Bay 7",
+ "rated_capacity": "Ti/TiN 300mm",
+ "power_rating": "20kW",
},
],
"enkid": [
@@ -91,16 +109,29 @@ MACHINES = {
"name": "CNC 선반 #1",
"equipment_code": "EK-CNC-001",
"model": "두산 LYNX 220",
+ "manufacturer": "두산공작기계",
+ "location": "1공장 A라인",
+ "rated_capacity": "최대 가공경 220mm",
+ "power_rating": "15kW (주축 모터)",
+ "description": "2축 CNC 선반. 알루미늄/스틸 가공용.",
},
{
"name": "프레스 500T",
"equipment_code": "EK-PRS-001",
"model": "현대위아 HF-500",
+ "manufacturer": "현대위아",
+ "location": "1공장 B라인",
+ "rated_capacity": "500톤",
+ "power_rating": "37kW",
},
{
"name": "용접 로봇 #1",
"equipment_code": "EK-WLD-001",
"model": "화낙 ARC Mate 120",
+ "manufacturer": "FANUC",
+ "location": "2공장 용접 셀 1",
+ "rated_capacity": "6축 120kg 가반하중",
+ "power_rating": "6kVA",
},
],
"alpet": [
@@ -108,16 +139,29 @@ MACHINES = {
"name": "반응기 R-101",
"equipment_code": "AP-RCT-101",
"model": "Custom Reactor 5000L",
+ "manufacturer": "한화솔루션 엔지니어링",
+ "location": "1플랜트 반응 구역",
+ "rated_capacity": "5,000L (SUS316L)",
+ "power_rating": "22kW (교반기)",
+ "description": "교반식 배치 반응기. HCl 합성 공정.",
},
{
"name": "증류탑 D-201",
"equipment_code": "AP-DST-201",
"model": "Sulzer Packed Tower",
+ "manufacturer": "Sulzer",
+ "location": "1플랜트 분리 구역",
+ "rated_capacity": "처리량 2,000 kg/h",
+ "power_rating": "리보일러 150kW",
},
{
"name": "열교환기 E-301",
"equipment_code": "AP-HEX-301",
"model": "Alfa Laval M10-BW",
+ "manufacturer": "Alfa Laval",
+ "location": "1플랜트 유틸리티",
+ "rated_capacity": "열전달 500kW",
+ "power_rating": "펌프 7.5kW",
},
],
}
@@ -183,6 +227,16 @@ PARTS = {
"auto_time",
5000,
),
+ (
+ "교정 인증서",
+ "CAL-SPT-01",
+ "인증",
+ "date",
+ 365,
+ 80,
+ "manual",
+ 0,
+ ),
],
},
"enkid": {
@@ -262,6 +316,16 @@ PARTS = {
"auto_time",
1500,
),
+ (
+ "안전 검사 인증",
+ "CERT-WLD-01",
+ "인증",
+ "date",
+ 180,
+ 85,
+ "manual",
+ 0,
+ ),
],
},
"alpet": {
@@ -323,6 +387,16 @@ PARTS = {
15000,
),
("압력 센서", "PS-HEX-01", "계측", "hours", 5000, 90, "auto_time", 2000),
+ (
+ "내압 검사 인증",
+ "CERT-HEX-01",
+ "인증",
+ "date",
+ 730,
+ 85,
+ "manual",
+ 0,
+ ),
],
},
}
@@ -654,6 +728,7 @@ async def seed():
machine_map[(tenant_id, m["name"])] = row.id
continue
machine_id = uuid.uuid4()
+ install_date = NOW - timedelta(days=365 * 2)
db.add(
Machine(
id=machine_id,
@@ -661,6 +736,12 @@ async def seed():
name=m["name"],
equipment_code=m["equipment_code"],
model=m["model"],
+ manufacturer=m.get("manufacturer"),
+ installation_date=install_date,
+ location=m.get("location"),
+ rated_capacity=m.get("rated_capacity"),
+ power_rating=m.get("power_rating"),
+ description=m.get("description"),
)
)
machine_map[(tenant_id, m["name"])] = machine_id
diff --git a/src/api/alarms.py b/src/api/alarms.py
index efd96fa..3b71a63 100644
--- a/src/api/alarms.py
+++ b/src/api/alarms.py
@@ -8,7 +8,16 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from src.database.config import get_db
-from src.database.models import Alarm, EquipmentPart, PartCounter, Machine
+from src.database.models import (
+ Alarm,
+ EquipmentPart,
+ PartCounter,
+ Machine,
+ InspectionSession,
+ InspectionRecord,
+ InspectionTemplate,
+ InspectionTemplateItem,
+)
from src.auth.models import TokenData
from src.auth.dependencies import require_auth, verify_tenant_access
@@ -27,12 +36,19 @@ def _alarm_to_dict(alarm: Alarm) -> dict:
return {
"id": str(alarm.id),
"tenant_id": str(alarm.tenant_id),
- "equipment_part_id": str(alarm.equipment_part_id),
- "machine_id": str(alarm.machine_id),
+ "equipment_part_id": str(alarm.equipment_part_id)
+ if alarm.equipment_part_id
+ else None,
+ "machine_id": str(alarm.machine_id) if alarm.machine_id else None,
+ "inspection_session_id": str(alarm.inspection_session_id)
+ if alarm.inspection_session_id
+ else None,
"alarm_type": str(alarm.alarm_type),
"severity": str(alarm.severity),
"message": str(alarm.message),
- "lifecycle_pct_at_trigger": float(alarm.lifecycle_pct_at_trigger),
+ "lifecycle_pct_at_trigger": float(alarm.lifecycle_pct_at_trigger)
+ if alarm.lifecycle_pct_at_trigger is not None
+ else None,
"is_acknowledged": bool(alarm.is_acknowledged),
"acknowledged_by": str(alarm.acknowledged_by)
if alarm.acknowledged_by
@@ -182,3 +198,33 @@ async def generate_alarms_for_part(
lifecycle_pct_at_trigger=pct,
)
db.add(alarm)
+
+
+async def generate_alarms_for_inspection(
+ db: AsyncSession,
+ tenant_id: str,
+ session: InspectionSession,
+ fail_records: list,
+ template_name: str,
+):
+ if not fail_records:
+ return
+
+ fail_count = len(fail_records)
+ severity = "critical" if fail_count >= 3 else "warning"
+
+ for record, item_name, spec_info in fail_records:
+ message = (
+ f"검사 '{template_name}'의 '{item_name}' 항목이 불합격입니다 ({spec_info})"
+ )
+ if len(message) > 500:
+ message = message[:497] + "..."
+
+ alarm = Alarm(
+ tenant_id=tenant_id,
+ inspection_session_id=session.id,
+ alarm_type="inspection_fail",
+ severity=severity,
+ message=message,
+ )
+ db.add(alarm)
diff --git a/src/api/equipment_parts.py b/src/api/equipment_parts.py
index d7387ac..e1cc63d 100644
--- a/src/api/equipment_parts.py
+++ b/src/api/equipment_parts.py
@@ -53,6 +53,38 @@ def _format_ts(val) -> Optional[str]:
return val.isoformat() if hasattr(val, "isoformat") else str(val)
+def _compute_auto_counter(part: EquipmentPart, counter: PartCounter) -> tuple:
+ source = str(part.counter_source or "manual")
+ lt = str(part.lifecycle_type)
+ limit = float(part.lifecycle_limit) if part.lifecycle_limit else 0
+
+ if source == "auto_time" and lt == "hours" and limit > 0:
+ ref = counter.last_reset_at or part.installed_at
+ if ref:
+ now = datetime.now(timezone.utc)
+ if ref.tzinfo is None:
+ from datetime import timezone as tz
+
+ ref = ref.replace(tzinfo=tz.utc)
+ elapsed_hours = (now - ref).total_seconds() / 3600
+ pct = round((elapsed_hours / limit) * 100, 1)
+ return (round(elapsed_hours, 1), pct)
+
+ if lt == "date" and limit > 0:
+ ref = counter.last_reset_at or part.installed_at
+ if ref:
+ now = datetime.now(timezone.utc)
+ if ref.tzinfo is None:
+ from datetime import timezone as tz
+
+ ref = ref.replace(tzinfo=tz.utc)
+ elapsed_days = (now - ref).total_seconds() / 86400
+ pct = round((elapsed_days / limit) * 100, 1)
+ return (round(elapsed_days, 1), pct)
+
+ return (float(counter.current_value or 0), float(counter.lifecycle_pct or 0))
+
+
def _part_to_dict(p: EquipmentPart, counter: Optional[PartCounter] = None) -> dict:
result = {
"id": str(p.id),
@@ -71,9 +103,10 @@ def _part_to_dict(p: EquipmentPart, counter: Optional[PartCounter] = None) -> di
"updated_at": _format_ts(p.updated_at),
}
if counter:
+ cv, pct = _compute_auto_counter(p, counter)
result["counter"] = {
- "current_value": float(counter.current_value or 0),
- "lifecycle_pct": float(counter.lifecycle_pct or 0),
+ "current_value": cv,
+ "lifecycle_pct": pct,
"last_reset_at": _format_ts(counter.last_reset_at),
"last_updated_at": _format_ts(counter.last_updated_at),
"version": int(counter.version or 0),
@@ -337,6 +370,14 @@ async def update_counter(
if not counter:
raise HTTPException(status_code=404, detail="카운터를 찾을 수 없습니다.")
+ source = str(part.counter_source or "manual")
+ lt = str(part.lifecycle_type)
+ if source == "auto_time" or lt == "date":
+ raise HTTPException(
+ status_code=400,
+ detail="자동 계산 부품(auto_time/date)은 수동으로 카운터를 업데이트할 수 없습니다.",
+ )
+
if body.value < 0:
raise HTTPException(status_code=400, detail="카운터 값은 0 이상이어야 합니다.")
diff --git a/src/api/inspections.py b/src/api/inspections.py
index 4fb0537..8234a9f 100644
--- a/src/api/inspections.py
+++ b/src/api/inspections.py
@@ -17,6 +17,7 @@ from src.database.models import (
)
from src.auth.models import TokenData
from src.auth.dependencies import require_auth, verify_tenant_access
+from src.api.alarms import generate_alarms_for_inspection
router = APIRouter(prefix="/api/{tenant_id}/inspections", tags=["inspections"])
@@ -471,11 +472,50 @@ async def complete_inspection(
if body and body.notes is not None:
session.notes = body.notes
+ fail_records_data = []
+ records_with_items_stmt = (
+ select(InspectionRecord)
+ .options(selectinload(InspectionRecord.template_item))
+ .where(
+ InspectionRecord.session_id == inspection_id,
+ InspectionRecord.is_pass == False,
+ )
+ )
+ fail_result = await db.execute(records_with_items_stmt)
+ fail_records = fail_result.scalars().all()
+
+ template_stmt = select(InspectionTemplate).where(
+ InspectionTemplate.id == session.template_id
+ )
+ tmpl = (await db.execute(template_stmt)).scalar_one_or_none()
+ template_name = str(tmpl.name) if tmpl else "알 수 없는 템플릿"
+
+ for r in fail_records:
+ ti = r.template_item
+ item_name = str(ti.name) if ti else "?"
+ spec_parts = []
+ if ti and ti.spec_min is not None:
+ spec_parts.append(f"최소: {ti.spec_min}")
+ if ti and ti.spec_max is not None:
+ spec_parts.append(f"최대: {ti.spec_max}")
+ measured = ""
+ if r.value_numeric is not None:
+ measured = f"측정값: {r.value_numeric}"
+ elif r.value_boolean is not None:
+ measured = f"측정값: {'합격' if r.value_boolean else '불합격'}"
+ spec_info = ", ".join(filter(None, [measured] + spec_parts))
+ fail_records_data.append((r, item_name, spec_info))
+
+ await db.flush()
+ await generate_alarms_for_inspection(
+ db, tenant_id, session, fail_records_data, template_name
+ )
+
await db.commit()
await db.refresh(session)
pass_count = sum(1 for r in session.records if r.is_pass is True)
- fail_count = sum(1 for r in session.records if r.is_pass is False)
+ fail_count_val = sum(1 for r in session.records if r.is_pass is False)
total = len(session.records)
return {
@@ -484,8 +524,8 @@ async def complete_inspection(
"summary": {
"total": total,
"pass_count": pass_count,
- "fail_count": fail_count,
- "no_judgment": total - pass_count - fail_count,
+ "fail_count": fail_count_val,
+ "no_judgment": total - pass_count - fail_count_val,
},
}
diff --git a/src/api/machines.py b/src/api/machines.py
index 9d0975a..1925a6a 100644
--- a/src/api/machines.py
+++ b/src/api/machines.py
@@ -19,12 +19,24 @@ class MachineCreate(BaseModel):
name: str
equipment_code: str = ""
model: Optional[str] = None
+ manufacturer: Optional[str] = None
+ installation_date: Optional[str] = None
+ location: Optional[str] = None
+ rated_capacity: Optional[str] = None
+ power_rating: Optional[str] = None
+ description: Optional[str] = None
class MachineUpdate(BaseModel):
name: Optional[str] = None
equipment_code: Optional[str] = None
model: Optional[str] = None
+ manufacturer: Optional[str] = None
+ installation_date: Optional[str] = None
+ location: Optional[str] = None
+ rated_capacity: Optional[str] = None
+ power_rating: Optional[str] = None
+ description: Optional[str] = None
class MachineResponse(BaseModel):
@@ -33,6 +45,12 @@ class MachineResponse(BaseModel):
name: str
equipment_code: str
model: Optional[str]
+ manufacturer: Optional[str] = None
+ installation_date: Optional[str] = None
+ location: Optional[str] = None
+ rated_capacity: Optional[str] = None
+ power_rating: Optional[str] = None
+ description: Optional[str] = None
parts_count: int = 0
created_at: Optional[str] = None
updated_at: Optional[str] = None
@@ -57,6 +75,12 @@ def _machine_to_response(m: Machine, parts_count: int = 0) -> MachineResponse:
name=str(m.name),
equipment_code=str(m.equipment_code or ""),
model=str(m.model) if m.model else None,
+ manufacturer=str(m.manufacturer) if m.manufacturer else None,
+ installation_date=_format_ts(m.installation_date),
+ location=str(m.location) if m.location else None,
+ rated_capacity=str(m.rated_capacity) if m.rated_capacity else None,
+ power_rating=str(m.power_rating) if m.power_rating else None,
+ description=str(m.description) if m.description else None,
parts_count=parts_count,
created_at=_format_ts(m.created_at),
updated_at=_format_ts(m.updated_at),
@@ -97,11 +121,26 @@ async def create_machine(
):
verify_tenant_access(tenant_id, current_user)
+ install_dt = None
+ if body.installation_date:
+ from datetime import datetime as dt
+
+ try:
+ install_dt = dt.fromisoformat(body.installation_date.replace("Z", "+00:00"))
+ except ValueError:
+ pass
+
machine = Machine(
tenant_id=tenant_id,
name=body.name,
equipment_code=body.equipment_code,
model=body.model,
+ manufacturer=body.manufacturer,
+ installation_date=install_dt,
+ location=body.location,
+ rated_capacity=body.rated_capacity,
+ power_rating=body.power_rating,
+ description=body.description,
)
db.add(machine)
await db.commit()
@@ -154,6 +193,12 @@ async def get_machine(
name=str(machine.name),
equipment_code=str(machine.equipment_code or ""),
model=str(machine.model) if machine.model else None,
+ manufacturer=str(machine.manufacturer) if machine.manufacturer else None,
+ installation_date=_format_ts(machine.installation_date),
+ location=str(machine.location) if machine.location else None,
+ rated_capacity=str(machine.rated_capacity) if machine.rated_capacity else None,
+ power_rating=str(machine.power_rating) if machine.power_rating else None,
+ description=str(machine.description) if machine.description else None,
parts_count=len(active_parts),
parts=parts_data,
created_at=_format_ts(machine.created_at),
@@ -187,6 +232,25 @@ async def update_machine(
machine.equipment_code = body.equipment_code
if body.model is not None:
machine.model = body.model
+ if body.manufacturer is not None:
+ machine.manufacturer = body.manufacturer
+ if body.installation_date is not None:
+ from datetime import datetime as dt
+
+ try:
+ machine.installation_date = dt.fromisoformat(
+ body.installation_date.replace("Z", "+00:00")
+ )
+ except ValueError:
+ pass
+ if body.location is not None:
+ machine.location = body.location
+ if body.rated_capacity is not None:
+ machine.rated_capacity = body.rated_capacity
+ if body.power_rating is not None:
+ machine.power_rating = body.power_rating
+ if body.description is not None:
+ machine.description = body.description
await db.commit()
await db.refresh(machine)
diff --git a/src/database/models.py b/src/database/models.py
index a1bfd4f..0202199 100644
--- a/src/database/models.py
+++ b/src/database/models.py
@@ -62,6 +62,12 @@ class Machine(Base):
name = Column(String(100), nullable=False)
equipment_code = Column(String(50), default="")
model = Column(String(100), nullable=True)
+ manufacturer = Column(String(100), nullable=True)
+ installation_date = Column(TIMESTAMP(timezone=True), nullable=True)
+ location = Column(String(200), nullable=True)
+ rated_capacity = Column(String(100), nullable=True)
+ power_rating = Column(String(100), nullable=True)
+ description = Column(Text, nullable=True)
created_at = Column(TIMESTAMP(timezone=True), default=utcnow)
updated_at = Column(TIMESTAMP(timezone=True), default=utcnow, onupdate=utcnow)
@@ -313,13 +319,18 @@ class Alarm(Base):
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(String(50), ForeignKey("tenants.id"), nullable=False)
equipment_part_id = Column(
- UUID(as_uuid=True), ForeignKey("equipment_parts.id"), nullable=False
+ UUID(as_uuid=True), ForeignKey("equipment_parts.id"), nullable=True
)
- machine_id = Column(UUID(as_uuid=True), ForeignKey("machines.id"), nullable=False)
- alarm_type = Column(String(30), nullable=False) # threshold | critical
- severity = Column(String(20), nullable=False) # warning | critical
+ machine_id = Column(UUID(as_uuid=True), ForeignKey("machines.id"), nullable=True)
+ inspection_session_id = Column(
+ UUID(as_uuid=True),
+ ForeignKey("inspection_sessions.id"),
+ nullable=True,
+ )
+ alarm_type = Column(String(30), nullable=False)
+ severity = Column(String(20), nullable=False)
message = Column(String(500), nullable=False)
- lifecycle_pct_at_trigger = Column(Float, nullable=False)
+ lifecycle_pct_at_trigger = Column(Float, nullable=True)
is_acknowledged = Column(Boolean, default=False)
acknowledged_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
acknowledged_at = Column(TIMESTAMP(timezone=True), nullable=True)
@@ -328,6 +339,7 @@ class Alarm(Base):
tenant = relationship("Tenant")
equipment_part = relationship("EquipmentPart")
machine = relationship("Machine")
+ inspection_session = relationship("InspectionSession")
acknowledged_user = relationship("User")
__table_args__ = (
|