File size: 9,596 Bytes
eb846d0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
import { useState, useEffect, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Server, ApiResponse } from '@/types';
import { getApiUrl } from '../utils/runtime';

// Configuration options
const CONFIG = {
  // Initialization phase configuration
  startup: {
    maxAttempts: 60, // Maximum number of attempts during initialization
    pollingInterval: 3000, // Polling interval during initialization (3 seconds)
  },
  // Normal operation phase configuration
  normal: {
    pollingInterval: 10000, // Polling interval during normal operation (10 seconds)
  },
};

export const useServerData = () => {
  const { t } = useTranslation();
  const [servers, setServers] = useState<Server[]>([]);
  const [error, setError] = useState<string | null>(null);
  const [refreshKey, setRefreshKey] = useState(0);
  const [isInitialLoading, setIsInitialLoading] = useState(true);
  const [fetchAttempts, setFetchAttempts] = useState(0);

  // Timer reference for polling
  const intervalRef = useRef<NodeJS.Timeout | null>(null);
  // Track current attempt count to avoid dependency cycles
  const attemptsRef = useRef<number>(0);

  // Clear the timer
  const clearTimer = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  };

  // Start normal polling
  const startNormalPolling = useCallback(() => {
    // Ensure no other timers are running
    clearTimer();

    const fetchServers = async () => {
      try {
        const token = localStorage.getItem('mcphub_token');
        const response = await fetch(getApiUrl('/servers'), {
          headers: {
            'x-auth-token': token || '',
          },
        });
        const data = await response.json();

        if (data && data.success && Array.isArray(data.data)) {
          setServers(data.data);
        } else if (data && Array.isArray(data)) {
          setServers(data);
        } else {
          console.error('Invalid server data format:', data);
          setServers([]);
        }

        // Reset error state
        setError(null);
      } catch (err) {
        console.error('Error fetching servers during normal polling:', err);

        // Use friendly error message
        if (!navigator.onLine) {
          setError(t('errors.network'));
        } else if (
          err instanceof TypeError &&
          (err.message.includes('NetworkError') || err.message.includes('Failed to fetch'))
        ) {
          setError(t('errors.serverConnection'));
        } else {
          setError(t('errors.serverFetch'));
        }
      }
    };

    // Execute immediately
    fetchServers();

    // Set up regular polling
    intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval);
  }, [t]);

  useEffect(() => {
    // Reset attempt count
    if (refreshKey > 0) {
      attemptsRef.current = 0;
      setFetchAttempts(0);
    }

    // Initialization phase request function
    const fetchInitialData = async () => {
      try {
        const token = localStorage.getItem('mcphub_token');
        const response = await fetch(getApiUrl('/servers'), {
          headers: {
            'x-auth-token': token || '',
          },
        });
        const data = await response.json();

        // Handle API response wrapper object, extract data field
        if (data && data.success && Array.isArray(data.data)) {
          setServers(data.data);
          setIsInitialLoading(false);
          // Initialization successful, start normal polling
          startNormalPolling();
          return true;
        } else if (data && Array.isArray(data)) {
          // Compatibility handling, if API directly returns array
          setServers(data);
          setIsInitialLoading(false);
          // Initialization successful, start normal polling
          startNormalPolling();
          return true;
        } else {
          // If data format is not as expected, set to empty array
          console.error('Invalid server data format:', data);
          setServers([]);
          setIsInitialLoading(false);
          // Initialization successful but data is empty, start normal polling
          startNormalPolling();
          return true;
        }
      } catch (err) {
        // Increment attempt count, use ref to avoid triggering effect rerun
        attemptsRef.current += 1;
        console.error(`Initial loading attempt ${attemptsRef.current} failed:`, err);

        // Update state for display
        setFetchAttempts(attemptsRef.current);

        // Set appropriate error message
        if (!navigator.onLine) {
          setError(t('errors.network'));
        } else {
          setError(t('errors.initialStartup'));
        }

        // If maximum attempt count is exceeded, give up initialization and switch to normal polling
        if (attemptsRef.current >= CONFIG.startup.maxAttempts) {
          console.log('Maximum startup attempts reached, switching to normal polling');
          setIsInitialLoading(false);
          // Clear initialization polling
          clearTimer();
          // Switch to normal polling mode
          startNormalPolling();
        }

        return false;
      }
    };

    // On component mount, set appropriate polling based on current state
    if (isInitialLoading) {
      // Ensure no other timers are running
      clearTimer();

      // Execute initial request immediately
      fetchInitialData();

      // Set polling interval for initialization phase
      intervalRef.current = setInterval(fetchInitialData, CONFIG.startup.pollingInterval);
      console.log(`Started initial polling with interval: ${CONFIG.startup.pollingInterval}ms`);
    } else {
      // Initialization completed, start normal polling
      startNormalPolling();
    }

    // Cleanup function
    return () => {
      clearTimer();
    };
  }, [refreshKey, t, isInitialLoading, startNormalPolling]);

  // Manually trigger refresh
  const triggerRefresh = () => {
    // Clear current timer
    clearTimer();

    // If in initialization phase, reset initialization state
    if (isInitialLoading) {
      setIsInitialLoading(true);
      attemptsRef.current = 0;
      setFetchAttempts(0);
    }

    // Change in refreshKey will trigger useEffect to run again
    setRefreshKey((prevKey) => prevKey + 1);
  };

  // Server related operations
  const handleServerAdd = () => {
    setRefreshKey((prevKey) => prevKey + 1);
  };

  const handleServerEdit = async (server: Server) => {
    try {
      // Fetch settings to get the full server config before editing
      const token = localStorage.getItem('mcphub_token');
      const response = await fetch(getApiUrl('/settings'), {
        headers: {
          'x-auth-token': token || '',
        },
      });

      const settingsData: ApiResponse<{ mcpServers: Record<string, any> }> = await response.json();

      if (
        settingsData &&
        settingsData.success &&
        settingsData.data &&
        settingsData.data.mcpServers &&
        settingsData.data.mcpServers[server.name]
      ) {
        const serverConfig = settingsData.data.mcpServers[server.name];
        return {
          name: server.name,
          status: server.status,
          tools: server.tools || [],
          config: serverConfig,
        };
      } else {
        console.error('Failed to get server config from settings:', settingsData);
        setError(t('server.invalidConfig', { serverName: server.name }));
        return null;
      }
    } catch (err) {
      console.error('Error fetching server settings:', err);
      setError(err instanceof Error ? err.message : String(err));
      return null;
    }
  };

  const handleServerRemove = async (serverName: string) => {
    try {
      const token = localStorage.getItem('mcphub_token');
      const response = await fetch(getApiUrl(`/servers/${serverName}`), {
        method: 'DELETE',
        headers: {
          'x-auth-token': token || '',
        },
      });
      const result = await response.json();

      if (!response.ok) {
        setError(result.message || t('server.deleteError', { serverName }));
        return false;
      }

      setRefreshKey((prevKey) => prevKey + 1);
      return true;
    } catch (err) {
      setError(t('errors.general') + ': ' + (err instanceof Error ? err.message : String(err)));
      return false;
    }
  };

  const handleServerToggle = async (server: Server, enabled: boolean) => {
    try {
      const token = localStorage.getItem('mcphub_token');
      const response = await fetch(getApiUrl(`/servers/${server.name}/toggle`), {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-auth-token': token || '',
        },
        body: JSON.stringify({ enabled }),
      });

      const result = await response.json();

      if (!response.ok) {
        console.error('Failed to toggle server:', result);
        setError(t('server.toggleError', { serverName: server.name }));
        return false;
      }

      // Update the UI immediately to reflect the change
      setRefreshKey((prevKey) => prevKey + 1);
      return true;
    } catch (err) {
      console.error('Error toggling server:', err);
      setError(err instanceof Error ? err.message : String(err));
      return false;
    }
  };

  return {
    servers,
    error,
    setError,
    isLoading: isInitialLoading,
    fetchAttempts,
    triggerRefresh,
    handleServerAdd,
    handleServerEdit,
    handleServerRemove,
    handleServerToggle,
  };
};