diff --git a/web/src/components/Health/InfraStatusCard.jsx b/web/src/components/Health/InfraStatusCard.jsx
new file mode 100644
index 0000000..d4da7c8
--- /dev/null
+++ b/web/src/components/Health/InfraStatusCard.jsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import TitleCard from '../TitleCard';
+import { FaDatabase, FaServer, FaFlask } from 'react-icons/fa';
+import ServiceStatus from '../ServiceStatus';
+
+const infrastructureServicesMap = [
+ { key: 'database', name: 'CockroachDB', icon: FaDatabase },
+ { key: 'cache', name: 'Redis Cache', icon: FaServer },
+ { key: 'storage', name: 'MinIO S3', icon: FaServer },
+ { key: 'ai_service', name: 'MONAI FastAPI (Instance)', icon: FaFlask },
+];
+
+const InfraStatusCard = ({ serviceData }) => {
+ return (
+
+
Infrastructure Status
+
+ {infrastructureServicesMap.map((service) => {
+ const svc = serviceData?.[service.key];
+ if (!svc) return null;
+ return (
+
+ );
+ })}
+
+
+ );
+};
+
+export default InfraStatusCard;
\ No newline at end of file
diff --git a/web/src/components/Health/ModelEndpointList.jsx b/web/src/components/Health/ModelEndpointList.jsx
new file mode 100644
index 0000000..44a8c95
--- /dev/null
+++ b/web/src/components/Health/ModelEndpointList.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { FaFlask, FaCheckCircle, FaTimesCircle } from 'react-icons/fa';
+import ServiceStatus from '../ServiceStatus';
+
+const ModelEndpointsStatus = ({ modelData }) => {
+ const models = modelData.models || [];
+
+ if (models.length === 0) {
+ return ไม่พบ Model ที่มีสถานะ ACTIVE
;
+ }
+
+ return (
+
+
+
+ สถานะ Model Endpoints ({models.length} รายการ)
+
+
+ {models.map((model) => (
+
+ ))}
+
+
+ );
+};
+
+export default ModelEndpointsStatus;
\ No newline at end of file
diff --git a/web/src/components/ServiceStatus.jsx b/web/src/components/ServiceStatus.jsx
new file mode 100644
index 0000000..d05616a
--- /dev/null
+++ b/web/src/components/ServiceStatus.jsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { FaCheckCircle, FaTimesCircle, FaDatabase, FaServer, FaFlask } from 'react-icons/fa';
+import PropTypes from 'prop-types'; // สำหรับการตรวจสอบ Props (Best Practice)
+
+// Helper Function: Mapping Icon
+const IconMap = {
+ FaDatabase,
+ FaServer,
+ FaFlask,
+ FaCheckCircle,
+ FaTimesCircle,
+};
+
+export default function ServiceStatus({ name, status, details, icon: Icon }) {
+ const isUp = status === 'UP' || status === 'Healthy';
+ const colorClass = isUp ? 'text-success' : 'text-error';
+ const badgeClass = isUp ? 'badge-success' : 'badge-error';
+
+ // Icon สำหรับแสดงสถานะ (ใช้ Check/Times Circle เป็น Fallback)
+ const StatusIcon = Icon || (isUp ? FaCheckCircle : FaTimesCircle);
+
+ return (
+
+
+ {/* Icon ของ Service */}
+
+ {/* ชื่อ Service */}
+ {name}
+
+
+
+ {/* Badge แสดงสถานะ */}
+ {status}
+ {/* รายละเอียด (Truncate เพื่อป้องกัน overflow) */}
+ {details}
+
+
+ );
+}
+
+// กำหนด PropTypes เพื่อเพิ่มความแข็งแกร่งในการพัฒนา
+ServiceStatus.propTypes = {
+ name: PropTypes.string.isRequired,
+ status: PropTypes.string.isRequired,
+ details: PropTypes.string.isRequired,
+ icon: PropTypes.elementType,
+};
\ No newline at end of file
diff --git a/web/src/config/sidebarRoutes.jsx b/web/src/config/sidebarRoutes.jsx
index a70ff52..2bb6ae1 100644
--- a/web/src/config/sidebarRoutes.jsx
+++ b/web/src/config/sidebarRoutes.jsx
@@ -1,86 +1,31 @@
-import { FaTachometerAlt, FaCog, FaDatabase, FaCogs, FaProjectDiagram, FaFlask,
- FaClipboardList, FaHeartbeat } from 'react-icons/fa';
-
+import { FaTachometerAlt, FaCog, FaDatabase, FaHeartbeat, FaFlask } from 'react-icons/fa';
const routes = [
{
path: '/dashboard',
- icon: ,
+ icon: ,
name: 'แดชบอร์ด/ภาพรวม',
- // ไม่ต้องระบุ requiredRole (viewer/admin เข้าถึงได้เสมอ)
},
// ----------------================--
- // กลุ่ม: Data & MLOps
+ // กลุ่ม: Data & AI/MLOps (เหลือแค่ Model Ops)
// ----------------------------------
{
path: '',
- icon: ,
- name: 'ข้อมูล & AI/MLOps', // ปรับชื่อเล็กน้อย
- // Roles: ใครก็ตามที่จัดการข้อมูล/โมเดล ควรเข้าถึงได้
- requiredRole: ['viewer', 'admin', 'manager'],
- submenu: [
- // เพิ่ม Endpoint สำหรับเรียกใช้ AI Inference Proxy
- {
- path:
- '/dashboard/inference-run',
- name: 'AI Inference (Run)', // ชื่อสำหรับเรียกใช้ AI จริง
- requiredRole: ['viewer', 'admin', 'manager'], // ผู้ใช้ทั่วไปก็ควรเรียกได้
- },
- {
- path:
- '/dashboard/model-registry',
- name: 'Model Registry & Control', // รวม CRUD และ Control ไว้ในหน้าเดียว
- // Roles: จำกัดเฉพาะผู้ที่มีสิทธิ์จัดการโมเดล (Admin/Manager)
- requiredRole: ['manager', 'admin'],
- },
- {
- path:
- '/dashboard/datasets',
- name: 'Dataset Catalog (CKAN)',
- requiredRole: ['viewer', 'admin', 'manager'],
- },
- {
- path:
- '/dashboard/lineage',
- name: 'Dataset Lineage',
- requiredRole: ['viewer', 'admin', 'manager'],
- },
- // Note: ลบ /dashboard/predict ออก เนื่องจาก /inference-run ควรจะครอบคลุม
- ],
- },
-
- // ----------------------------------
- // กลุ่ม: Pipelines & Logs
- // ----------------------------------
- {
- path: '',
- icon: ,
- name: 'Pipelines & Log',
+ icon: ,
+ name: 'AI Model Operations',
requiredRole: ['viewer', 'admin', 'manager'],
submenu: [
{
- path:
- '/dashboard/pipeline/trigger',
- name: 'Pipeline Control',
- // Roles: จำกัดเฉพาะผู้ที่สั่งรันได้ (Admin/Manager)
+ path: '/dashboard/inference-run',
+ name: 'AI Inference (Run)',
+ requiredRole: ['viewer', 'admin', 'manager'],
+ },
+ {
+ path: '/dashboard/model-registry',
+ name: 'Model Registry & Control',
requiredRole: ['manager', 'admin'],
},
- {
- path:
- '/dashboard/pipeline/logs',
- name: 'Pipeline Logs',
- requiredRole: ['viewer', 'admin', 'manager'],
- },
- {
- path:
- '/dashboard/pipeline/status',
- name: 'Pipeline Run Status',
- requiredRole: ['viewer', 'admin', 'manager'],
- },
],
},
@@ -89,20 +34,16 @@ const routes = [
// ----------------------------------
{
path: '/dashboard/health',
- icon: ,
+ icon: ,
name: 'สถานะระบบ (Health)',
requiredRole: ['viewer', 'admin', 'manager'],
},
{
path: '/dashboard/settings',
- icon: ,
+ icon: ,
name: 'ตั้งค่าระบบ',
- requiredRole: ['admin'], // สำหรับ Superuser/Admin เท่านั้น
+ requiredRole: ['admin'],
},
-
];
-
export default routes;
\ No newline at end of file
diff --git a/web/src/pages/system/Health.jsx b/web/src/pages/system/Health.jsx
new file mode 100644
index 0000000..b3a5e46
--- /dev/null
+++ b/web/src/pages/system/Health.jsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import TitleCard from '../../components/TitleCard';
+import { useSystemHealth } from '../../services/healthApi';
+import { FaTimesCircle, FaSyncAlt, FaCheckCircle } from 'react-icons/fa';
+
+// Imports Components ย่อย
+import InfraStatusCard from '../../components/Health/InfraStatusCard';
+import ModelEndpointsStatus from '../../components/Health/ModelEndpointList';
+
+
+export default function SystemHealth() {
+ const { data, isLoading, isError, isFetching } = useSystemHealth();
+
+ // ตรวจสอบสถานะภาพรวม
+ const overallStatus = data?.status || (isError ? 'Error' : 'Loading');
+ const overallColor = overallStatus === 'Healthy' ? 'alert-success' : overallStatus === 'Degraded' ? 'alert-warning' : 'alert-error';
+ const lastChecked = data?.last_checked ? new Date(data.last_checked).toLocaleTimeString() : 'N/A';
+
+ if (isError) {
+ return
+ ไม่สามารถเชื่อมต่อกับ Health API ได้ (Backend อาจหยุดทำงาน)
+
+ }
+
+ return (
+ : null}
+ >
+
+ {/* 1. สถานะภาพรวม */}
+
+
+ {overallStatus === 'Healthy' ? : }
+ สถานะภาพรวม: {overallStatus}
+
+
+
+ {/* 2. Infrastructure Services Card */}
+ {data?.services && }
+
+ {/* 3. Model Endpoint List */}
+ {data?.services?.model_endpoints && }
+
+ {/* เวลาตรวจสอบ */}
+
+ {isLoading && !data ? "กำลังตรวจสอบสถานะครั้งแรก..." : `อัปเดตล่าสุด: ${lastChecked}`}
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/src/routes/pageRoutes.jsx b/web/src/routes/pageRoutes.jsx
index d5a0b34..2650131 100644
--- a/web/src/routes/pageRoutes.jsx
+++ b/web/src/routes/pageRoutes.jsx
@@ -2,6 +2,7 @@ import Dashboard from '../pages/Dashboard';
import BlankPage from '../pages/BlankPage';
import ModelRegistry from '../pages/data/ModelRegistry';
import InferenceRun from '../pages/data/InferenceRun';
+import SystemHealth from '../pages/system/Health';
// Array ของเส้นทางย่อยภายใต้ /dashboard/
const pageRoutes = [
@@ -22,6 +23,12 @@ const pageRoutes = [
path: 'inference-run',
element: ,
},
+ // --- สถานะระบบ (Health) ---
+ {
+ // Path: /dashboard/health
+ path: 'health',
+ element: ,
+ },
// Fallback สำหรับเส้นทางที่ไม่ตรงกับเมนูใดๆ
{
path: '*',
diff --git a/web/src/services/healthApi.js b/web/src/services/healthApi.js
new file mode 100644
index 0000000..9cf5228
--- /dev/null
+++ b/web/src/services/healthApi.js
@@ -0,0 +1,35 @@
+import { useQuery } from '@tanstack/react-query';
+import axiosClient from './axiosClient';
+import { addToast } from '../features/toast/toastSlice';
+import { useDispatch } from 'react-redux';
+
+const REFETCH_INTERVAL = 15000; // ดึงข้อมูลใหม่ทุก 15 วินาที
+
+/**
+ * Hook สำหรับดึงสถานะ Health Check ของระบบทั้งหมด
+ * Endpoint: GET /api/v1/health/
+ */
+export const useSystemHealth = () => {
+ const dispatch = useDispatch();
+
+ return useQuery({
+ queryKey: ['systemHealth'],
+ queryFn: async () => {
+ const response = await axiosClient.get('/api/v1/health/');
+
+ // แจ้งเตือนเมื่อสถานะกลับมา UP จาก Degraded
+ if (response.data.status === 'Healthy') {
+ // dispatch(addToast({ message: 'System Health Status: Healthy', type: 'info' }));
+ }
+ return response.data;
+ },
+ // ตั้งค่า Polling ให้ดึงข้อมูลใหม่ทุก 15 วินาที
+ refetchInterval: REFETCH_INTERVAL,
+ staleTime: 5000,
+
+ // จัดการ Error เมื่อไม่สามารถเชื่อมต่อ API ได้เลย (เช่น 500 หรือ Network Failure)
+ onError: () => {
+ dispatch(addToast({ message: 'Network Error: Could not reach Health API', type: 'error' }));
+ }
+ });
+};
\ No newline at end of file