Improve the frontend and repository layer

This commit is contained in:
Urtzi Alfaro
2025-10-23 07:44:54 +02:00
parent 8d30172483
commit 07c33fa578
112 changed files with 14726 additions and 2733 deletions

View File

@@ -93,36 +93,18 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin):
try:
self._checks_performed += 1
# Use a simpler query with timeout and connection management
from sqlalchemy import text
simplified_query = text("""
SELECT
pb.tenant_id,
DATE(pb.planned_start_time) as planned_date,
COUNT(*) as batch_count,
SUM(pb.planned_quantity) as total_planned,
'capacity_check' as capacity_status,
100.0 as capacity_percentage -- Default value for processing
FROM production_batches pb
WHERE pb.planned_start_time >= CURRENT_DATE
AND pb.planned_start_time <= CURRENT_DATE + INTERVAL '3 days'
AND pb.status IN ('planned', 'in_progress')
GROUP BY pb.tenant_id, DATE(pb.planned_start_time)
HAVING COUNT(*) > 10 -- Alert if more than 10 batches per day
ORDER BY total_planned DESC
LIMIT 20 -- Limit results to prevent excessive processing
""")
# Use timeout and proper session handling
try:
from app.repositories.production_alert_repository import ProductionAlertRepository
async with self.db_manager.get_session() as session:
alert_repo = ProductionAlertRepository(session)
# Set statement timeout to prevent long-running queries
await session.execute(text("SET statement_timeout = '30s'"))
result = await session.execute(simplified_query)
capacity_issues = result.fetchall()
await alert_repo.set_statement_timeout('30s')
capacity_issues = await alert_repo.get_capacity_issues()
for issue in capacity_issues:
await self._process_capacity_issue(issue.tenant_id, issue)
await self._process_capacity_issue(issue['tenant_id'], issue)
except asyncio.TimeoutError:
logger.warning("Capacity check timed out", service=self.config.SERVICE_NAME)
@@ -203,36 +185,14 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin):
try:
self._checks_performed += 1
# Import text function at the beginning
from sqlalchemy import text
# Simplified query with timeout and proper error handling
query = text("""
SELECT
pb.id, pb.tenant_id, pb.product_name, pb.batch_number,
pb.planned_end_time as planned_completion_time, pb.actual_start_time,
pb.actual_end_time as estimated_completion_time, pb.status,
EXTRACT(minutes FROM (NOW() - pb.planned_end_time)) as delay_minutes,
COALESCE(pb.priority::text, 'medium') as priority_level,
1 as affected_orders -- Default to 1 since we can't count orders
FROM production_batches pb
WHERE pb.status = 'in_progress'
AND pb.planned_end_time < NOW()
AND pb.planned_end_time > NOW() - INTERVAL '24 hours'
ORDER BY
CASE COALESCE(pb.priority::text, 'MEDIUM')
WHEN 'URGENT' THEN 1 WHEN 'HIGH' THEN 2 ELSE 3
END,
delay_minutes DESC
LIMIT 50 -- Limit results to prevent excessive processing
""")
try:
from app.repositories.production_alert_repository import ProductionAlertRepository
async with self.db_manager.get_session() as session:
alert_repo = ProductionAlertRepository(session)
# Set statement timeout
await session.execute(text("SET statement_timeout = '30s'"))
result = await session.execute(query)
delays = result.fetchall()
await alert_repo.set_statement_timeout('30s')
delays = await alert_repo.get_production_delays()
for delay in delays:
await self._process_production_delay(delay)
@@ -300,44 +260,16 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin):
"""Check for quality control issues (alerts)"""
try:
self._checks_performed += 1
# Fixed query using actual quality_checks table structure
query = """
SELECT
qc.id, qc.tenant_id, qc.batch_id, qc.check_type as test_type,
qc.quality_score as result_value,
qc.target_weight as min_acceptable,
(qc.target_weight * (1 + qc.tolerance_percentage/100)) as max_acceptable,
CASE
WHEN qc.pass_fail = false AND qc.defect_count > 5 THEN 'critical'
WHEN qc.pass_fail = false THEN 'major'
ELSE 'minor'
END as qc_severity,
qc.created_at,
pb.product_name, pb.batch_number,
COUNT(*) OVER (PARTITION BY qc.batch_id) as total_failures
FROM quality_checks qc
JOIN production_batches pb ON pb.id = qc.batch_id
WHERE qc.pass_fail = false -- Use pass_fail instead of status
AND qc.created_at > NOW() - INTERVAL '4 hours'
AND qc.corrective_action_needed = true -- Use this instead of acknowledged
ORDER BY
CASE
WHEN qc.pass_fail = false AND qc.defect_count > 5 THEN 1
WHEN qc.pass_fail = false THEN 2
ELSE 3
END,
qc.created_at DESC
"""
from sqlalchemy import text
from app.repositories.production_alert_repository import ProductionAlertRepository
async with self.db_manager.get_session() as session:
result = await session.execute(text(query))
quality_issues = result.fetchall()
alert_repo = ProductionAlertRepository(session)
quality_issues = await alert_repo.get_quality_issues()
for issue in quality_issues:
await self._process_quality_issue(issue)
except Exception as e:
# Skip quality checks if tables don't exist (graceful degradation)
if "does not exist" in str(e) or "column" in str(e).lower() and "does not exist" in str(e).lower():
@@ -380,16 +312,14 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin):
# Mark as acknowledged to avoid duplicates - using proper session management
try:
from sqlalchemy import text
from app.repositories.production_alert_repository import ProductionAlertRepository
async with self.db_manager.get_session() as session:
await session.execute(
text("UPDATE quality_checks SET acknowledged = true WHERE id = :id"),
{"id": issue['id']}
)
await session.commit()
alert_repo = ProductionAlertRepository(session)
await alert_repo.mark_quality_check_acknowledged(issue['id'])
except Exception as e:
logger.error("Failed to update quality check acknowledged status",
quality_check_id=str(issue.get('id')),
logger.error("Failed to update quality check acknowledged status",
quality_check_id=str(issue.get('id')),
error=str(e))
# Don't raise here to avoid breaking the main flow
@@ -402,49 +332,28 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin):
"""Check equipment status and maintenance requirements (alerts)"""
try:
self._checks_performed += 1
# Query equipment that needs attention
query = """
SELECT
e.id, e.tenant_id, e.name, e.type, e.status,
e.efficiency_percentage, e.uptime_percentage,
e.last_maintenance_date, e.next_maintenance_date,
e.maintenance_interval_days,
EXTRACT(DAYS FROM (e.next_maintenance_date - NOW())) as days_to_maintenance,
COUNT(ea.id) as active_alerts
FROM equipment e
LEFT JOIN alerts ea ON ea.equipment_id = e.id
AND ea.is_active = true
AND ea.is_resolved = false
WHERE e.is_active = true
AND e.tenant_id = $1
GROUP BY e.id, e.tenant_id, e.name, e.type, e.status,
e.efficiency_percentage, e.uptime_percentage,
e.last_maintenance_date, e.next_maintenance_date,
e.maintenance_interval_days
ORDER BY e.next_maintenance_date ASC
"""
from app.repositories.production_alert_repository import ProductionAlertRepository
tenants = await self.get_active_tenants()
for tenant_id in tenants:
try:
from sqlalchemy import text
# Use a separate session for each tenant to avoid connection blocking
async with self.db_manager.get_session() as session:
result = await session.execute(text(query), {"tenant_id": tenant_id})
equipment_list = result.fetchall()
alert_repo = ProductionAlertRepository(session)
equipment_list = await alert_repo.get_equipment_status(tenant_id)
for equipment in equipment_list:
# Process each equipment item in a non-blocking manner
await self._process_equipment_issue(equipment)
except Exception as e:
logger.error("Error checking equipment status",
tenant_id=str(tenant_id),
logger.error("Error checking equipment status",
tenant_id=str(tenant_id),
error=str(e))
# Continue processing other tenants despite this error
except Exception as e:
logger.error("Equipment status check failed", error=str(e))
self._errors_count += 1
@@ -530,61 +439,28 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin):
"""Generate production efficiency recommendations"""
try:
self._checks_performed += 1
# Analyze production patterns for efficiency opportunities
query = """
WITH efficiency_analysis AS (
SELECT
pb.tenant_id, pb.product_name,
AVG(EXTRACT(minutes FROM (pb.actual_completion_time - pb.actual_start_time))) as avg_production_time,
AVG(pb.planned_duration_minutes) as avg_planned_duration,
COUNT(*) as batch_count,
AVG(pb.yield_percentage) as avg_yield,
EXTRACT(hour FROM pb.actual_start_time) as start_hour
FROM production_batches pb
WHERE pb.status = 'COMPLETED'
AND pb.actual_completion_time > CURRENT_DATE - INTERVAL '30 days'
AND pb.tenant_id = $1
GROUP BY pb.tenant_id, pb.product_name, EXTRACT(hour FROM pb.actual_start_time)
HAVING COUNT(*) >= 3
),
recommendations AS (
SELECT *,
CASE
WHEN avg_production_time > avg_planned_duration * 1.2 THEN 'reduce_production_time'
WHEN avg_yield < 85 THEN 'improve_yield'
WHEN start_hour BETWEEN 14 AND 16 AND avg_production_time > avg_planned_duration * 1.1 THEN 'avoid_afternoon_production'
ELSE null
END as recommendation_type,
(avg_production_time - avg_planned_duration) / avg_planned_duration * 100 as efficiency_loss_percent
FROM efficiency_analysis
)
SELECT * FROM recommendations
WHERE recommendation_type IS NOT NULL
AND efficiency_loss_percent > 10
ORDER BY efficiency_loss_percent DESC
"""
from app.repositories.production_alert_repository import ProductionAlertRepository
tenants = await self.get_active_tenants()
for tenant_id in tenants:
try:
from sqlalchemy import text
# Use a separate session per tenant to avoid connection blocking
async with self.db_manager.get_session() as session:
result = await session.execute(text(query), {"tenant_id": tenant_id})
recommendations = result.fetchall()
alert_repo = ProductionAlertRepository(session)
recommendations = await alert_repo.get_efficiency_recommendations(tenant_id)
for rec in recommendations:
# Process each recommendation individually
await self._generate_efficiency_recommendation(tenant_id, rec)
except Exception as e:
logger.error("Error generating efficiency recommendations",
tenant_id=str(tenant_id),
logger.error("Error generating efficiency recommendations",
tenant_id=str(tenant_id),
error=str(e))
# Continue with other tenants despite this error
except Exception as e:
logger.error("Efficiency recommendations failed", error=str(e))
self._errors_count += 1
@@ -659,41 +535,26 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin):
async def generate_energy_recommendations(self):
"""Generate energy optimization recommendations"""
try:
# Analyze energy consumption patterns
query = """
SELECT
e.tenant_id, e.name as equipment_name, e.type,
AVG(ec.energy_consumption_kwh) as avg_energy,
EXTRACT(hour FROM ec.recorded_at) as hour_of_day,
COUNT(*) as readings_count
FROM equipment e
JOIN energy_consumption ec ON ec.equipment_id = e.id
WHERE ec.recorded_at > CURRENT_DATE - INTERVAL '30 days'
AND e.tenant_id = $1
GROUP BY e.tenant_id, e.id, EXTRACT(hour FROM ec.recorded_at)
HAVING COUNT(*) >= 10
ORDER BY avg_energy DESC
"""
from app.repositories.production_alert_repository import ProductionAlertRepository
tenants = await self.get_active_tenants()
for tenant_id in tenants:
try:
from sqlalchemy import text
# Use a separate session per tenant to avoid connection blocking
async with self.db_manager.get_session() as session:
result = await session.execute(text(query), {"tenant_id": tenant_id})
energy_data = result.fetchall()
alert_repo = ProductionAlertRepository(session)
energy_data = await alert_repo.get_energy_consumption_patterns(tenant_id)
# Analyze for peak hours and optimization opportunities
await self._analyze_energy_patterns(tenant_id, energy_data)
except Exception as e:
logger.error("Error generating energy recommendations",
tenant_id=str(tenant_id),
logger.error("Error generating energy recommendations",
tenant_id=str(tenant_id),
error=str(e))
# Continue with other tenants despite this error
except Exception as e:
logger.error("Energy recommendations failed", error=str(e))
self._errors_count += 1
@@ -839,23 +700,14 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin):
async def get_affected_production_batches(self, ingredient_id: str) -> List[str]:
"""Get production batches affected by ingredient shortage"""
try:
query = """
SELECT DISTINCT pb.id
FROM production_batches pb
JOIN recipe_ingredients ri ON ri.recipe_id = pb.recipe_id
WHERE ri.ingredient_id = $1
AND pb.status = 'in_progress'
AND pb.planned_completion_time > NOW()
"""
from sqlalchemy import text
from app.repositories.production_alert_repository import ProductionAlertRepository
async with self.db_manager.get_session() as session:
result_rows = await session.execute(text(query), {"ingredient_id": ingredient_id})
result = result_rows.fetchall()
return [str(row['id']) for row in result]
alert_repo = ProductionAlertRepository(session)
return await alert_repo.get_affected_production_batches(ingredient_id)
except Exception as e:
logger.error("Error getting affected production batches",
ingredient_id=ingredient_id,
logger.error("Error getting affected production batches",
ingredient_id=ingredient_id,
error=str(e))
return []