From 02f0c91a15108fdbffbe053d4315214e3c8febdb Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Mon, 29 Dec 2025 19:33:35 +0100 Subject: [PATCH] Fix UI issues --- .../domain/forecasting/DemandChart.tsx | 19 +++--- frontend/src/locales/en/production.json | 2 + frontend/src/locales/es/production.json | 2 + frontend/src/locales/eu/production.json | 2 + .../app/analytics/ProductionAnalyticsPage.tsx | 16 ++--- .../analytics/forecasting/ForecastingPage.tsx | 14 +++-- .../app/services/production_service.py | 2 +- services/recipes/app/api/internal_demo.py | 5 +- .../training/app/api/websocket_operations.py | 58 ++++++++++++++++++- 9 files changed, 94 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/domain/forecasting/DemandChart.tsx b/frontend/src/components/domain/forecasting/DemandChart.tsx index 6d54a3ee..d6742b87 100644 --- a/frontend/src/components/domain/forecasting/DemandChart.tsx +++ b/frontend/src/components/domain/forecasting/DemandChart.tsx @@ -377,26 +377,31 @@ const DemandChart: React.FC = ({ iconType="line" /> - {/* Confidence interval area */} + {/* Confidence interval area - rendered as a range between lower and upper bounds */} {showConfidenceInterval && ( )} {showConfidenceInterval && ( )} diff --git a/frontend/src/locales/en/production.json b/frontend/src/locales/en/production.json index 82420d3d..adb640b6 100644 --- a/frontend/src/locales/en/production.json +++ b/frontend/src/locales/en/production.json @@ -29,6 +29,8 @@ "active_equipment": "Active Equipment", "one_in_maintenance": "1 in maintenance", "excellent_standards": "excellent standards", + "current_capacity": "Current capacity", + "completion_rate": "Completion rate", "yield_performance_leaderboard": "Yield Performance Leaderboard", "product_yield_rankings_trends": "Product yield rankings and trends", "no_yield_data": "No yield data available", diff --git a/frontend/src/locales/es/production.json b/frontend/src/locales/es/production.json index 475f2938..49810709 100644 --- a/frontend/src/locales/es/production.json +++ b/frontend/src/locales/es/production.json @@ -29,6 +29,8 @@ "active_equipment": "Equipos Activos", "one_in_maintenance": "1 en mantenimiento", "excellent_standards": "Estándares excelentes", + "current_capacity": "Capacidad actual", + "completion_rate": "Tasa de finalización", "planned_batches": "Lotes Planificados", "batches": "lotes", "best": "mejor", diff --git a/frontend/src/locales/eu/production.json b/frontend/src/locales/eu/production.json index 09fe2f80..dc6c0fc1 100644 --- a/frontend/src/locales/eu/production.json +++ b/frontend/src/locales/eu/production.json @@ -29,6 +29,8 @@ "active_equipment": "Ekipo Aktiboak", "one_in_maintenance": "1 mantentze lanetan", "excellent_standards": "Estandar bikainak", + "current_capacity": "Uneko ahalmena", + "completion_rate": "Amaiera tasa", "planned_batches": "Planifikatutako Sortak", "batches": "sortak", "best": "onena", diff --git a/frontend/src/pages/app/analytics/ProductionAnalyticsPage.tsx b/frontend/src/pages/app/analytics/ProductionAnalyticsPage.tsx index c528a58b..986baa7d 100644 --- a/frontend/src/pages/app/analytics/ProductionAnalyticsPage.tsx +++ b/frontend/src/pages/app/analytics/ProductionAnalyticsPage.tsx @@ -109,28 +109,28 @@ const ProductionAnalyticsPage: React.FC = () => { stats={[ { title: t('stats.overall_efficiency'), - value: dashboard?.efficiency_percentage ? `${dashboard.efficiency_percentage.toFixed(1)}%` : '94%', + value: `${(dashboard?.efficiency_percentage ?? 0).toFixed(1)}%`, variant: 'success' as const, icon: Target, subtitle: t('stats.vs_target_95') }, { - title: t('stats.average_cost_per_unit'), - value: '€2.45', + title: t('stats.capacity_utilization'), + value: `${(dashboard?.capacity_utilization ?? 0).toFixed(1)}%`, variant: 'info' as const, icon: DollarSign, - subtitle: t('stats.down_3_vs_last_week') + subtitle: t('stats.current_capacity') }, { - title: t('stats.active_equipment'), - value: '8/9', + title: t('stats.on_time_completion'), + value: `${(dashboard?.on_time_completion_rate ?? 0).toFixed(1)}%`, variant: 'warning' as const, icon: Settings, - subtitle: t('stats.one_in_maintenance') + subtitle: t('stats.completion_rate') }, { title: t('stats.quality_score'), - value: dashboard?.average_quality_score ? `${dashboard.average_quality_score.toFixed(1)}/10` : '9.2/10', + value: `${(dashboard?.average_quality_score ?? 0).toFixed(1)}/10`, variant: 'success' as const, icon: Award, subtitle: t('stats.excellent_standards') diff --git a/frontend/src/pages/app/analytics/forecasting/ForecastingPage.tsx b/frontend/src/pages/app/analytics/forecasting/ForecastingPage.tsx index da20b3aa..184af116 100644 --- a/frontend/src/pages/app/analytics/forecasting/ForecastingPage.tsx +++ b/frontend/src/pages/app/analytics/forecasting/ForecastingPage.tsx @@ -1,5 +1,5 @@ import React, { useState, useMemo } from 'react'; -import { Calendar, TrendingUp, AlertTriangle, BarChart3, Settings, Loader, Zap, Brain, Target, Activity } from 'lucide-react'; +import { Calendar, TrendingUp, AlertTriangle, BarChart3, Settings, Loader, Zap, Brain, Target, Activity, Package } from 'lucide-react'; import { Button, Card, Badge, StatusCard, getStatusColor } from '../../../../components/ui'; import { AnalyticsPageLayout, AnalyticsCard } from '../../../../components/analytics'; import { LoadingSpinner } from '../../../../components/ui'; @@ -137,7 +137,8 @@ const ForecastingPage: React.FC = () => { // Use current forecast data from multi-day API response const forecasts = currentForecastData; - const isLoading = ingredientsLoading || modelsLoading || isGenerating; + // Separate initial data loading from forecast generation + const isInitialLoading = ingredientsLoading || modelsLoading; const hasError = ingredientsError || modelsError; // Calculate metrics from real data @@ -189,7 +190,8 @@ const ForecastingPage: React.FC = () => { // Loading and error states - using project patterns - if (isLoading || !tenantId) { + // Only show full-page loading for initial data fetch, not for forecast generation + if (isInitialLoading || !tenantId) { return (
@@ -220,7 +222,7 @@ const ForecastingPage: React.FC = () => { description="Sistema inteligente de predicción de demanda basado en IA" subscriptionLoading={false} hasAccess={true} - dataLoading={isLoading} + dataLoading={isInitialLoading} stats={[ { title: 'Ingredientes con Modelos', @@ -430,7 +432,7 @@ const ForecastingPage: React.FC = () => { data={forecasts} product={selectedProduct} period={forecastPeriod} - loading={isLoading} + loading={isGenerating} error={hasError ? 'Error al cargar las predicciones' : null} height={450} title="" @@ -472,7 +474,7 @@ const ForecastingPage: React.FC = () => { )} {/* Help Section - Only when no models available */} - {!isLoading && !hasError && products.length === 0 && ( + {!isInitialLoading && !hasError && products.length === 0 && (
diff --git a/services/production/app/services/production_service.py b/services/production/app/services/production_service.py index 93f392d0..f8ae1bf4 100644 --- a/services/production/app/services/production_service.py +++ b/services/production/app/services/production_service.py @@ -436,7 +436,7 @@ class ProductionService: ) ) recent_quality_result = await batch_repo.session.execute(recent_quality_query) - average_quality_score = recent_quality_result.scalar() or 8.5 # Default fallback + average_quality_score = recent_quality_result.scalar() or 0.0 return ProductionDashboardSummary( active_batches=len(active_batches), diff --git a/services/recipes/app/api/internal_demo.py b/services/recipes/app/api/internal_demo.py index ddc334d7..614f620b 100644 --- a/services/recipes/app/api/internal_demo.py +++ b/services/recipes/app/api/internal_demo.py @@ -441,7 +441,8 @@ async def get_recipe_count( _: bool = Depends(verify_internal_api_key) ): """ - Get count of active recipes for onboarding status check. + Get count of recipes for onboarding status check. + Counts DRAFT and ACTIVE recipes (excludes ARCHIVED/DISCONTINUED). Internal endpoint for tenant service. """ try: @@ -452,7 +453,7 @@ async def get_recipe_count( select(func.count()).select_from(Recipe) .where( Recipe.tenant_id == UUID(tenant_id), - Recipe.status == RecipeStatus.ACTIVE + Recipe.status.in_([RecipeStatus.DRAFT, RecipeStatus.ACTIVE, RecipeStatus.TESTING]) ) ) diff --git a/services/training/app/api/websocket_operations.py b/services/training/app/api/websocket_operations.py index 54e6d120..a41ab544 100644 --- a/services/training/app/api/websocket_operations.py +++ b/services/training/app/api/websocket_operations.py @@ -9,12 +9,20 @@ import structlog from app.websocket.manager import websocket_manager from shared.auth.jwt_handler import JWTHandler from app.core.config import settings +from app.services.training_service import EnhancedTrainingService +from shared.database.base import create_database_manager logger = structlog.get_logger() router = APIRouter(tags=["websocket"]) +def get_enhanced_training_service(): + """Create EnhancedTrainingService instance""" + database_manager = create_database_manager(settings.DATABASE_URL, "training-service") + return EnhancedTrainingService(database_manager) + + @router.websocket("/api/v1/tenants/{tenant_id}/training/jobs/{job_id}/live") async def training_progress_websocket( websocket: WebSocket, @@ -68,6 +76,44 @@ async def training_progress_websocket( # Connect to WebSocket manager await websocket_manager.connect(job_id, websocket) + # Helper function to send current job status + async def send_current_status(): + """Fetch and send the current job status to the client""" + try: + training_service = get_enhanced_training_service() + status_info = await training_service.get_training_status(job_id) + + if status_info and not status_info.get("error"): + # Map status to WebSocket message type + ws_type = "progress" + if status_info.get("status") == "completed": + ws_type = "completed" + elif status_info.get("status") == "failed": + ws_type = "failed" + + await websocket.send_json({ + "type": ws_type, + "job_id": job_id, + "data": { + "progress": status_info.get("progress", 0), + "current_step": status_info.get("current_step"), + "status": status_info.get("status"), + "products_total": status_info.get("products_total", 0), + "products_completed": status_info.get("products_completed", 0), + "products_failed": status_info.get("products_failed", 0), + "estimated_time_remaining_seconds": status_info.get("estimated_time_remaining_seconds"), + "message": status_info.get("message") + } + }) + logger.info("Sent current job status to client", + job_id=job_id, + status=status_info.get("status"), + progress=status_info.get("progress")) + except Exception as e: + logger.error("Failed to send current job status", + job_id=job_id, + error=str(e)) + try: # Send connection confirmation await websocket.send_json({ @@ -76,21 +122,29 @@ async def training_progress_websocket( "message": "Connected to training progress stream" }) + # Immediately send current job status after connection + # This handles the race condition where training completes before WebSocket connects + await send_current_status() + # Keep connection alive and handle client messages ping_count = 0 while True: try: - # Receive messages from client (ping, etc.) + # Receive messages from client (ping, get_status, etc.) data = await websocket.receive_text() # Handle ping/pong if data == "ping": await websocket.send_text("pong") ping_count += 1 - logger.info("WebSocket ping/pong", + logger.debug("WebSocket ping/pong", job_id=job_id, ping_count=ping_count, connection_healthy=True) + # Handle get_status request + elif data == "get_status": + await send_current_status() + logger.info("Status requested by client", job_id=job_id) except WebSocketDisconnect: logger.info("Client disconnected", job_id=job_id)