Fix UI issues
This commit is contained in:
@@ -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"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user