import React, { useEffect, useReducer, useRef, useState } from 'react'; import { createRoot } from 'react-dom/client'; import './styles.css'; import { api } from './api.js'; import { ClientOverviewPage } from './components/ClientOverviewPage.jsx'; import { BootStatePage, StaleBanner } from './components/SyncStatus.jsx'; import { compatibleSnapshot, harborReducer, initialHarborState, } from './state/harborReducer.js'; function App() { const previewReady = new URLSearchParams(window.location.search).has('preview-ready'); const [{ snapshot: state, pendingServerId: pendingTag, transport }, dispatch] = useReducer( harborReducer, initialHarborState, ); const [subscriptionUrl, setSubscriptionUrl] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState(''); const pollGeneration = useRef(0); function setPendingTag(serverId) { dispatch({ type: 'select-server', serverId }); } async function loadState({ retry = false } = {}) { if (retry) dispatch({ type: 'retry-sync' }); const generation = pollGeneration.current; try { const snapshot = await api.state(); if (!compatibleSnapshot(snapshot)) { const incompatible = new Error('Ожидался Harbor state apiVersion 1'); incompatible.code = 'INCOMPATIBLE_API'; throw incompatible; } if (generation === pollGeneration.current) { dispatch({ type: 'sync-succeeded', snapshot, receivedAt: new Date().toISOString() }); } } catch (requestError) { if (generation === pollGeneration.current) { dispatch({ type: 'sync-failed', error: requestError }); } } } useEffect(() => { loadState(); const timer = setInterval(loadState, 5000); return () => clearInterval(timer); }, []); useEffect(() => { if (!state?.mode) return; const isGateway = state.mode === 'gateway'; document.title = isGateway ? 'Harbor Gateway' : 'Harbor Connect'; document.getElementById('harbor-favicon').href = isGateway ? '/harbor-gateway.svg?v=2' : '/harbor-connect.svg?v=2'; }, [state?.mode]); async function run(action) { setBusy(true); setError(''); try { return await applyMutation(action); } catch (err) { setError(err.message); throw err; } finally { setBusy(false); } } async function applyMutation(action) { pollGeneration.current += 1; const result = await action(); if (!result?.state) throw new Error('Harbor API не вернул state snapshot'); if (!compatibleSnapshot(result.state)) throw new Error('Harbor API не вернул state snapshot v1'); dispatch({ type: 'sync-succeeded', snapshot: result.state, receivedAt: new Date().toISOString(), }); return result; } async function fetchSubscription() { return run(async () => { const data = await api.subscription.fetch(subscriptionUrl); dispatch({ type: 'clear-pending-server' }); return data; }); } async function refreshSubscription() { try { return await applyMutation(api.subscription.refresh); } catch (refreshError) { setError(refreshError.message); throw refreshError; } } async function forgetSubscription() { return run(async () => { const data = await api.subscription.forget(); setSubscriptionUrl(''); dispatch({ type: 'clear-pending-server' }); return data; }); } if (!state) return loadState({ retry: true })} />; return (
loadState({ retry: true })} />
run(() => api.apply(tag))} onRestart={() => run(api.singbox.restart)} onStop={() => run(api.singbox.stop)} onSetGatewayAuto={(enabled) => run(() => api.gatewayAuto.setEnabled(enabled))} />
); } createRoot(document.getElementById('root')).render();