|
1 |
| -import React, { useState, useEffect, useRef } from "react"; |
| 1 | +"use client"; |
| 2 | + |
| 3 | +import { useEffect, useRef } from "react"; |
2 | 4 | import { ANKI_CONFIG } from "@/config/constants";
|
| 5 | +import { useToast } from "@/contexts/ToastContext"; |
3 | 6 |
|
4 | 7 | interface APIStatus {
|
5 | 8 | available: boolean | null;
|
6 | 9 | name: string;
|
7 | 10 | description: string;
|
8 |
| - show: boolean; |
9 | 11 | }
|
10 | 12 |
|
11 | 13 | export default function StatusChecker() {
|
12 |
| - const [statuses, setStatuses] = useState<APIStatus[]>([ |
13 |
| - { name: "AnkiConnect", description: "Anki synchronization", available: null, show: true }, |
14 |
| - { name: "Card Generator", description: "Card creation API", available: null, show: true }, |
| 14 | + const { showToast, hideAllToasts } = useToast(); |
| 15 | + const statusesRef = useRef<APIStatus[]>([ |
| 16 | + { name: "AnkiConnect", description: "Anki synchronization", available: null }, |
| 17 | + { name: "Card Generator", description: "Card creation API", available: null }, |
15 | 18 | ]);
|
16 | 19 |
|
17 |
| - // Use a ref to store the initial statuses |
18 |
| - const initialStatuses = useRef(statuses); |
| 20 | + const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null); |
| 21 | + const isMounted = useRef(true); |
| 22 | + |
| 23 | + // Define the API status check function |
| 24 | + const checkAPIStatus = async (name: string): Promise<boolean> => { |
| 25 | + try { |
| 26 | + if (name === "AnkiConnect") { |
| 27 | + const response = await fetch(ANKI_CONFIG.API_URL, { |
| 28 | + method: "POST", |
| 29 | + headers: { "Content-Type": "application/json" }, |
| 30 | + body: JSON.stringify({ action: "version", version: 6 }), |
| 31 | + }); |
| 32 | + if (response.ok) { |
| 33 | + const data = await response.json(); |
| 34 | + return !!data.result; |
| 35 | + } |
| 36 | + } |
| 37 | + if (name === "Card Generator") { |
| 38 | + const response = await fetch("http://localhost:8000/create_card/supported_languages"); |
| 39 | + if (response.ok) { |
| 40 | + return true; |
| 41 | + } |
| 42 | + } |
| 43 | + return false; |
| 44 | + } catch (error) { |
| 45 | + console.error(`Error checking ${name}:`, error); |
| 46 | + return false; |
| 47 | + } |
| 48 | + }; |
| 49 | + |
| 50 | + // Cleanup when component unmounts |
| 51 | + useEffect(() => { |
| 52 | + return () => { |
| 53 | + isMounted.current = false; |
| 54 | + if (pollingIntervalRef.current) { |
| 55 | + clearInterval(pollingIntervalRef.current); |
| 56 | + pollingIntervalRef.current = null; |
| 57 | + } |
| 58 | + }; |
| 59 | + }, []); |
| 60 | + |
| 61 | + // Set up the polling mechanism |
| 62 | + const POLLING_INTERVAL = 5000; // 5 seconds between checks |
| 63 | + const SUCCESS_DISPLAY_TIME = 3000; // 3 seconds to display success |
19 | 64 |
|
20 | 65 | useEffect(() => {
|
21 |
| - const checkAPIStatuses = async () => { |
22 |
| - const newStatuses = await Promise.all( |
23 |
| - initialStatuses.current.map(async (status) => { |
24 |
| - try { |
25 |
| - if (status.name === "AnkiConnect") { |
26 |
| - const response = await fetch(ANKI_CONFIG.API_URL, { |
27 |
| - method: "POST", |
28 |
| - headers: { "Content-Type": "application/json" }, |
29 |
| - body: JSON.stringify({ action: "version", version: 6 }), |
30 |
| - }); |
31 |
| - if (response.ok) { |
32 |
| - const data = await response.json(); |
33 |
| - return { ...status, available: !!data.result, show: true }; |
34 |
| - } |
35 |
| - } |
36 |
| - if (status.name === "Card Generator") { |
37 |
| - const response = await fetch("http://localhost:8000/create_card/supported_languages"); |
38 |
| - if (response.ok) { |
39 |
| - return { ...status, available: true, show: true }; |
40 |
| - } |
41 |
| - } |
42 |
| - return { ...status, available: false, show: true }; |
43 |
| - } catch (error) { |
44 |
| - console.error(`Error checking ${status.name}:`, error); |
45 |
| - return { ...status, available: false, show: true }; |
| 66 | + // Function to check all statuses at once |
| 67 | + const checkAllStatuses = async () => { |
| 68 | + if (!isMounted.current) return; |
| 69 | + |
| 70 | + // Create a copy to work with |
| 71 | + const currentStatuses = [...statusesRef.current]; |
| 72 | + const updatedStatuses = [...currentStatuses]; |
| 73 | + const recoveredServices: string[] = []; |
| 74 | + let hasServiceDown = false; |
| 75 | + let statusChanged = false; |
| 76 | + |
| 77 | + // Check each service |
| 78 | + for (let i = 0; i < updatedStatuses.length; i++) { |
| 79 | + const status = updatedStatuses[i]; |
| 80 | + const isAvailable = await checkAPIStatus(status.name); |
| 81 | + |
| 82 | + // If status changed from not available to available (true transition) |
| 83 | + if (status.available === false && isAvailable) { |
| 84 | + updatedStatuses[i] = { ...status, available: true }; |
| 85 | + recoveredServices.push(status.name); |
| 86 | + statusChanged = true; |
| 87 | + } |
| 88 | + // If checking for the first time and it's available |
| 89 | + else if (status.available === null && isAvailable) { |
| 90 | + updatedStatuses[i] = { ...status, available: true }; |
| 91 | + } |
| 92 | + // If service is unavailable |
| 93 | + else if (!isAvailable) { |
| 94 | + if (status.available !== false) { |
| 95 | + statusChanged = true; |
46 | 96 | }
|
47 |
| - }) |
48 |
| - ); |
49 |
| - setStatuses(newStatuses); |
| 97 | + updatedStatuses[i] = { ...status, available: false }; |
| 98 | + hasServiceDown = true; |
| 99 | + } |
| 100 | + } |
50 | 101 |
|
51 |
| - newStatuses.forEach((status, index) => { |
52 |
| - if (status.available === true) { |
| 102 | + // Update statuses ref |
| 103 | + statusesRef.current = updatedStatuses; |
| 104 | + |
| 105 | + // Show appropriate toast notifications |
| 106 | + if (statusChanged) { |
| 107 | + // Clear existing toasts when status changes |
| 108 | + hideAllToasts(); |
| 109 | + |
| 110 | + // If any service was recovered, show a success toast for it |
| 111 | + if (recoveredServices.length > 0) { |
| 112 | + // Show success toast for specifically recovered services |
| 113 | + showToast({ |
| 114 | + type: 'success', |
| 115 | + title: `Service${recoveredServices.length > 1 ? 's' : ''} Recovered`, |
| 116 | + message: `${recoveredServices.join(", ")} ${recoveredServices.length > 1 ? 'are' : 'is'} now available.`, |
| 117 | + duration: SUCCESS_DISPLAY_TIME // Show longer so users notice it |
| 118 | + }); |
| 119 | + } |
| 120 | + |
| 121 | + // If we still have services down, show an error toast after a brief delay |
| 122 | + // This prevents the toasts from appearing simultaneously |
| 123 | + if (hasServiceDown) { |
53 | 124 | setTimeout(() => {
|
54 |
| - setStatuses((prev) => { |
55 |
| - const updated = [...prev]; |
56 |
| - updated[index] = { ...status, show: false }; |
57 |
| - return updated; |
| 125 | + if (!isMounted.current) return; |
| 126 | + |
| 127 | + const unavailableServices = statusesRef.current |
| 128 | + .filter(s => s.available === false) |
| 129 | + .map(s => s.name) |
| 130 | + .join(", "); |
| 131 | + |
| 132 | + showToast({ |
| 133 | + type: 'error', |
| 134 | + title: 'Service Interruption', |
| 135 | + message: `${unavailableServices} ${statusesRef.current.filter(s => s.available === false).length > 1 ? 'are' : 'is'} unavailable. Please check your connections.`, |
| 136 | + duration: 0 // Keep until resolved |
58 | 137 | });
|
59 |
| - }, 3000); |
| 138 | + }, recoveredServices.length > 0 ? 300 : 0); // Small delay if we just showed a recovery toast |
| 139 | + } else if (recoveredServices.length > 0) { |
| 140 | + // If all services are up after some were down, show an additional "all clear" notification |
| 141 | + setTimeout(() => { |
| 142 | + if (!isMounted.current) return; |
| 143 | + |
| 144 | + showToast({ |
| 145 | + type: 'info', |
| 146 | + title: 'All Systems Operational', |
| 147 | + message: 'All services are now running properly.', |
| 148 | + duration: SUCCESS_DISPLAY_TIME |
| 149 | + }); |
| 150 | + }, 300); // Small delay after the recovery toast |
60 | 151 | }
|
61 |
| - }); |
| 152 | + } |
| 153 | + }; |
| 154 | + |
| 155 | + // Run initial check |
| 156 | + checkAllStatuses(); |
| 157 | + |
| 158 | + // Set up single polling interval (only if not already set) |
| 159 | + if (!pollingIntervalRef.current) { |
| 160 | + pollingIntervalRef.current = setInterval(checkAllStatuses, POLLING_INTERVAL); |
| 161 | + } |
| 162 | + |
| 163 | + // Cleanup on effect change |
| 164 | + return () => { |
| 165 | + if (pollingIntervalRef.current) { |
| 166 | + clearInterval(pollingIntervalRef.current); |
| 167 | + pollingIntervalRef.current = null; |
| 168 | + } |
62 | 169 | };
|
| 170 | + }, [showToast, hideAllToasts]); // Include toast functions as dependencies |
63 | 171 |
|
64 |
| - checkAPIStatuses(); |
65 |
| - }, []); // Empty dependency array since we use initialStatuses.current |
66 |
| - |
67 |
| - return ( |
68 |
| - <div className="space-y-0"> |
69 |
| - {statuses.map((status, index) => ( |
70 |
| - <div |
71 |
| - key={index} |
72 |
| - className={`transform transition-all duration-500 ease-in-out overflow-hidden |
73 |
| - ${status.show ? "max-h-24 mb-4 opacity-100 translate-y-0" : "max-h-0 mb-0 opacity-0 -translate-y-4"} |
74 |
| - ${status.available === null |
75 |
| - ? "bg-yellow-100 text-yellow-800" |
76 |
| - : status.available |
77 |
| - ? "bg-green-100 text-green-800" |
78 |
| - : "bg-red-100 text-red-800" |
79 |
| - }`} |
80 |
| - > |
81 |
| - <div className="p-4 text-center rounded"> |
82 |
| - {status.available === null && <p>Checking {status.name} availability...</p>} |
83 |
| - {status.available === true && ( |
84 |
| - <p>{status.name} is available! ({status.description})</p> |
85 |
| - )} |
86 |
| - {status.available === false && ( |
87 |
| - <p> |
88 |
| - <strong>{status.name} is unavailable.</strong> Ensure the {status.description} is running and accessible. |
89 |
| - </p> |
90 |
| - )} |
91 |
| - </div> |
92 |
| - </div> |
93 |
| - ))} |
94 |
| - </div> |
95 |
| - ); |
| 172 | + // This component doesn't render anything visible on its own now |
| 173 | + return null; |
96 | 174 | }
|
0 commit comments