Fix UI issues

This commit is contained in:
Urtzi Alfaro
2025-12-29 19:33:35 +01:00
parent c1dedfa44f
commit 02f0c91a15
9 changed files with 94 additions and 26 deletions

View File

@@ -377,26 +377,31 @@ const DemandChart: React.FC<DemandChartProps> = ({
iconType="line" iconType="line"
/> />
{/* Confidence interval area */} {/* Confidence interval area - rendered as a range between lower and upper bounds */}
{showConfidenceInterval && ( {showConfidenceInterval && (
<Area <Area
type="monotone" type="monotone"
dataKey="confidenceUpper" dataKey="confidenceUpper"
stackId={1} stroke="#10b981"
stroke="none" strokeWidth={1}
strokeOpacity={0.3}
fill="url(#confidenceGradient)" fill="url(#confidenceGradient)"
fillOpacity={0.4} fillOpacity={0.3}
name="Límite Superior" name="Límite Superior"
legendType="none"
/> />
)} )}
{showConfidenceInterval && ( {showConfidenceInterval && (
<Area <Area
type="monotone" type="monotone"
dataKey="confidenceLower" dataKey="confidenceLower"
stackId={1} stroke="#10b981"
stroke="none" strokeWidth={1}
fill="#ffffff" strokeOpacity={0.3}
fill="var(--bg-primary, #1a1a2e)"
fillOpacity={1}
name="Límite Inferior" name="Límite Inferior"
legendType="none"
/> />
)} )}

View File

@@ -29,6 +29,8 @@
"active_equipment": "Active Equipment", "active_equipment": "Active Equipment",
"one_in_maintenance": "1 in maintenance", "one_in_maintenance": "1 in maintenance",
"excellent_standards": "excellent standards", "excellent_standards": "excellent standards",
"current_capacity": "Current capacity",
"completion_rate": "Completion rate",
"yield_performance_leaderboard": "Yield Performance Leaderboard", "yield_performance_leaderboard": "Yield Performance Leaderboard",
"product_yield_rankings_trends": "Product yield rankings and trends", "product_yield_rankings_trends": "Product yield rankings and trends",
"no_yield_data": "No yield data available", "no_yield_data": "No yield data available",

View File

@@ -29,6 +29,8 @@
"active_equipment": "Equipos Activos", "active_equipment": "Equipos Activos",
"one_in_maintenance": "1 en mantenimiento", "one_in_maintenance": "1 en mantenimiento",
"excellent_standards": "Estándares excelentes", "excellent_standards": "Estándares excelentes",
"current_capacity": "Capacidad actual",
"completion_rate": "Tasa de finalización",
"planned_batches": "Lotes Planificados", "planned_batches": "Lotes Planificados",
"batches": "lotes", "batches": "lotes",
"best": "mejor", "best": "mejor",

View File

@@ -29,6 +29,8 @@
"active_equipment": "Ekipo Aktiboak", "active_equipment": "Ekipo Aktiboak",
"one_in_maintenance": "1 mantentze lanetan", "one_in_maintenance": "1 mantentze lanetan",
"excellent_standards": "Estandar bikainak", "excellent_standards": "Estandar bikainak",
"current_capacity": "Uneko ahalmena",
"completion_rate": "Amaiera tasa",
"planned_batches": "Planifikatutako Sortak", "planned_batches": "Planifikatutako Sortak",
"batches": "sortak", "batches": "sortak",
"best": "onena", "best": "onena",

View File

@@ -109,28 +109,28 @@ const ProductionAnalyticsPage: React.FC = () => {
stats={[ stats={[
{ {
title: t('stats.overall_efficiency'), 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, variant: 'success' as const,
icon: Target, icon: Target,
subtitle: t('stats.vs_target_95') subtitle: t('stats.vs_target_95')
}, },
{ {
title: t('stats.average_cost_per_unit'), title: t('stats.capacity_utilization'),
value: '€2.45', value: `${(dashboard?.capacity_utilization ?? 0).toFixed(1)}%`,
variant: 'info' as const, variant: 'info' as const,
icon: DollarSign, icon: DollarSign,
subtitle: t('stats.down_3_vs_last_week') subtitle: t('stats.current_capacity')
}, },
{ {
title: t('stats.active_equipment'), title: t('stats.on_time_completion'),
value: '8/9', value: `${(dashboard?.on_time_completion_rate ?? 0).toFixed(1)}%`,
variant: 'warning' as const, variant: 'warning' as const,
icon: Settings, icon: Settings,
subtitle: t('stats.one_in_maintenance') subtitle: t('stats.completion_rate')
}, },
{ {
title: t('stats.quality_score'), 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, variant: 'success' as const,
icon: Award, icon: Award,
subtitle: t('stats.excellent_standards') subtitle: t('stats.excellent_standards')

View File

@@ -1,5 +1,5 @@
import React, { useState, useMemo } from 'react'; 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 { Button, Card, Badge, StatusCard, getStatusColor } from '../../../../components/ui';
import { AnalyticsPageLayout, AnalyticsCard } from '../../../../components/analytics'; import { AnalyticsPageLayout, AnalyticsCard } from '../../../../components/analytics';
import { LoadingSpinner } from '../../../../components/ui'; import { LoadingSpinner } from '../../../../components/ui';
@@ -137,7 +137,8 @@ const ForecastingPage: React.FC = () => {
// Use current forecast data from multi-day API response // Use current forecast data from multi-day API response
const forecasts = currentForecastData; const forecasts = currentForecastData;
const isLoading = ingredientsLoading || modelsLoading || isGenerating; // Separate initial data loading from forecast generation
const isInitialLoading = ingredientsLoading || modelsLoading;
const hasError = ingredientsError || modelsError; const hasError = ingredientsError || modelsError;
// Calculate metrics from real data // Calculate metrics from real data
@@ -189,7 +190,8 @@ const ForecastingPage: React.FC = () => {
// Loading and error states - using project patterns // 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 ( return (
<div className="flex items-center justify-center min-h-64"> <div className="flex items-center justify-center min-h-64">
<LoadingSpinner text="Cargando datos de predicción..." /> <LoadingSpinner text="Cargando datos de predicción..." />
@@ -220,7 +222,7 @@ const ForecastingPage: React.FC = () => {
description="Sistema inteligente de predicción de demanda basado en IA" description="Sistema inteligente de predicción de demanda basado en IA"
subscriptionLoading={false} subscriptionLoading={false}
hasAccess={true} hasAccess={true}
dataLoading={isLoading} dataLoading={isInitialLoading}
stats={[ stats={[
{ {
title: 'Ingredientes con Modelos', title: 'Ingredientes con Modelos',
@@ -430,7 +432,7 @@ const ForecastingPage: React.FC = () => {
data={forecasts} data={forecasts}
product={selectedProduct} product={selectedProduct}
period={forecastPeriod} period={forecastPeriod}
loading={isLoading} loading={isGenerating}
error={hasError ? 'Error al cargar las predicciones' : null} error={hasError ? 'Error al cargar las predicciones' : null}
height={450} height={450}
title="" title=""
@@ -472,7 +474,7 @@ const ForecastingPage: React.FC = () => {
)} )}
{/* Help Section - Only when no models available */} {/* Help Section - Only when no models available */}
{!isLoading && !hasError && products.length === 0 && ( {!isInitialLoading && !hasError && products.length === 0 && (
<Card className="p-6 text-center"> <Card className="p-6 text-center">
<div className="py-8"> <div className="py-8">
<Brain className="h-12 w-12 text-[var(--color-info)] mx-auto mb-4" /> <Brain className="h-12 w-12 text-[var(--color-info)] mx-auto mb-4" />

View File

@@ -436,7 +436,7 @@ class ProductionService:
) )
) )
recent_quality_result = await batch_repo.session.execute(recent_quality_query) 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( return ProductionDashboardSummary(
active_batches=len(active_batches), active_batches=len(active_batches),

View File

@@ -441,7 +441,8 @@ async def get_recipe_count(
_: bool = Depends(verify_internal_api_key) _: 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. Internal endpoint for tenant service.
""" """
try: try:
@@ -452,7 +453,7 @@ async def get_recipe_count(
select(func.count()).select_from(Recipe) select(func.count()).select_from(Recipe)
.where( .where(
Recipe.tenant_id == UUID(tenant_id), Recipe.tenant_id == UUID(tenant_id),
Recipe.status == RecipeStatus.ACTIVE Recipe.status.in_([RecipeStatus.DRAFT, RecipeStatus.ACTIVE, RecipeStatus.TESTING])
) )
) )

View File

@@ -9,12 +9,20 @@ import structlog
from app.websocket.manager import websocket_manager from app.websocket.manager import websocket_manager
from shared.auth.jwt_handler import JWTHandler from shared.auth.jwt_handler import JWTHandler
from app.core.config import settings 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() logger = structlog.get_logger()
router = APIRouter(tags=["websocket"]) 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") @router.websocket("/api/v1/tenants/{tenant_id}/training/jobs/{job_id}/live")
async def training_progress_websocket( async def training_progress_websocket(
websocket: WebSocket, websocket: WebSocket,
@@ -68,6 +76,44 @@ async def training_progress_websocket(
# Connect to WebSocket manager # Connect to WebSocket manager
await websocket_manager.connect(job_id, websocket) 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: try:
# Send connection confirmation # Send connection confirmation
await websocket.send_json({ await websocket.send_json({
@@ -76,21 +122,29 @@ async def training_progress_websocket(
"message": "Connected to training progress stream" "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 # Keep connection alive and handle client messages
ping_count = 0 ping_count = 0
while True: while True:
try: try:
# Receive messages from client (ping, etc.) # Receive messages from client (ping, get_status, etc.)
data = await websocket.receive_text() data = await websocket.receive_text()
# Handle ping/pong # Handle ping/pong
if data == "ping": if data == "ping":
await websocket.send_text("pong") await websocket.send_text("pong")
ping_count += 1 ping_count += 1
logger.info("WebSocket ping/pong", logger.debug("WebSocket ping/pong",
job_id=job_id, job_id=job_id,
ping_count=ping_count, ping_count=ping_count,
connection_healthy=True) 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: except WebSocketDisconnect:
logger.info("Client disconnected", job_id=job_id) logger.info("Client disconnected", job_id=job_id)