feat(mcp): подготовить сервер и доступ к приёмке
This commit is contained in:
commit
bf3d50e1ff
3835 files changed
+537122
No files matched your search
@@ -0,0 +1,145 @@
|
||||
"use client"
|
||||
|
||||
import { createContext, useContext, useEffect, useState, useCallback, useMemo, useRef } from "react"
|
||||
import {
|
||||
configuredBackendUrl,
|
||||
defaultDataSourceMode,
|
||||
isBackendUrlLocked,
|
||||
isMockDataSourceAvailable,
|
||||
LOCAL_DEFAULT_BACKEND_URL,
|
||||
resolveStoredBackendUrl,
|
||||
} from "@/lib/backend-url"
|
||||
import { getBackendHealth } from "@/shared/api/backend-health"
|
||||
|
||||
// ── types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type DataSourceMode = "mock" | "live"
|
||||
|
||||
/** Режим «Живые» в настройках: моковые данные не подставлять. */
|
||||
export function isLiveDataSourceMode(mode: DataSourceMode): boolean {
|
||||
return mode === "live"
|
||||
}
|
||||
|
||||
interface DataSourceContextValue {
|
||||
mode: DataSourceMode
|
||||
setMode: (m: DataSourceMode) => void
|
||||
backendUrl: string
|
||||
setBackendUrl: (url: string) => void
|
||||
backendUrlLocked: boolean
|
||||
mockModeAvailable: boolean
|
||||
/** localStorage для mode/backendUrl применён на клиенте (после первого paint). */
|
||||
prefsHydrated: boolean
|
||||
/** undefined = не проверялось, true = OK, false = недоступен */
|
||||
backendStatus: boolean | undefined
|
||||
backendChecking: boolean
|
||||
checkBackend: () => Promise<void>
|
||||
}
|
||||
|
||||
// ── context ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const DataSourceContext = createContext<DataSourceContextValue | null>(null)
|
||||
|
||||
const LS_MODE = "routerlists:data-source"
|
||||
const LS_BACKEND = "routerlists:backend-url"
|
||||
|
||||
function readStoredMode(): DataSourceMode {
|
||||
if (!isMockDataSourceAvailable()) return "live"
|
||||
if (typeof window === "undefined") return defaultDataSourceMode()
|
||||
const stored = localStorage.getItem(LS_MODE) as DataSourceMode | null
|
||||
if (stored === "live") return "live"
|
||||
if (stored === "mock") return "mock"
|
||||
return defaultDataSourceMode()
|
||||
}
|
||||
|
||||
function initialBackendUrl(): string {
|
||||
if (typeof window === "undefined") {
|
||||
const cfg = configuredBackendUrl()
|
||||
if (cfg.kind === "same-origin") return ""
|
||||
if (cfg.kind === "fixed") return cfg.url
|
||||
return LOCAL_DEFAULT_BACKEND_URL
|
||||
}
|
||||
return resolveStoredBackendUrl(localStorage.getItem(LS_BACKEND))
|
||||
}
|
||||
|
||||
function normalizeBackendUrl(url: string): string {
|
||||
return url.trim().replace(/\/$/, "")
|
||||
}
|
||||
|
||||
export function DataSourceProvider({ children }: { children: React.ReactNode }) {
|
||||
const [mode, setModeState] = useState<DataSourceMode>(defaultDataSourceMode)
|
||||
const [backendUrl, setBackendUrlState] = useState(initialBackendUrl)
|
||||
const [prefsHydrated, setPrefsHydrated] = useState(false)
|
||||
const healthToken = useMemo(() => Symbol(backendUrl), [backendUrl])
|
||||
const [health, setHealth] = useState<{ token: symbol; value: boolean | undefined; checking: boolean }>(() => ({ token: healthToken, value: undefined, checking: false }))
|
||||
const backendStatus = health.token === healthToken ? health.value : undefined
|
||||
const backendChecking = health.token === healthToken && health.checking
|
||||
const healthSession = useRef<{token:symbol;active:boolean;revision:number;controller?:AbortController}>({token:healthToken,active:true,revision:0})
|
||||
useEffect(() => {
|
||||
const scope = {token:healthToken,active:true,revision:0,controller:undefined as AbortController | undefined}
|
||||
healthSession.current = scope
|
||||
return () => { scope.active = false; scope.controller?.abort() }
|
||||
}, [healthToken])
|
||||
const backendUrlLocked = isBackendUrlLocked()
|
||||
const mockModeAvailable = isMockDataSourceAvailable()
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
queueMicrotask(() => {
|
||||
if (!active) return
|
||||
const storedMode = readStoredMode()
|
||||
const url = resolveStoredBackendUrl(localStorage.getItem(LS_BACKEND))
|
||||
setModeState(storedMode)
|
||||
setBackendUrlState(url)
|
||||
setPrefsHydrated(true)
|
||||
})
|
||||
return () => { active = false }
|
||||
}, [])
|
||||
|
||||
const setMode = useCallback((m: DataSourceMode) => {
|
||||
if (!mockModeAvailable && m === "mock") return
|
||||
setModeState(m)
|
||||
localStorage.setItem(LS_MODE, m)
|
||||
}, [mockModeAvailable])
|
||||
|
||||
const setBackendUrl = useCallback((url: string) => {
|
||||
if (backendUrlLocked) return
|
||||
const normalized = normalizeBackendUrl(url)
|
||||
setBackendUrlState(normalized)
|
||||
localStorage.setItem(LS_BACKEND, normalized)
|
||||
}, [backendUrlLocked])
|
||||
|
||||
const checkBackend = useCallback(async () => {
|
||||
const scope = healthSession.current
|
||||
if (!scope.active || scope.token !== healthToken) return
|
||||
const revision = ++scope.revision
|
||||
scope.controller?.abort()
|
||||
const controller = new AbortController(); scope.controller = controller
|
||||
const current = () => scope.active && scope.revision === revision && !controller.signal.aborted
|
||||
setHealth({token:healthToken,value:undefined,checking:true})
|
||||
try {
|
||||
await getBackendHealth(backendUrl, controller.signal)
|
||||
if (current()) setHealth({token:healthToken,value:true,checking:false})
|
||||
} catch {
|
||||
if (current()) setHealth({token:healthToken,value:false,checking:false})
|
||||
}
|
||||
}, [backendUrl, healthToken])
|
||||
|
||||
useEffect(() => {
|
||||
if (!prefsHydrated || mode !== "live") return
|
||||
queueMicrotask(() => { void checkBackend() })
|
||||
}, [prefsHydrated, mode, checkBackend])
|
||||
|
||||
return (
|
||||
<DataSourceContext.Provider
|
||||
value={{ mode, setMode, backendUrl, setBackendUrl, backendUrlLocked, mockModeAvailable, prefsHydrated, backendStatus, backendChecking, checkBackend }}
|
||||
>
|
||||
{children}
|
||||
</DataSourceContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useDataSource(): DataSourceContextValue {
|
||||
const ctx = useContext(DataSourceContext)
|
||||
if (!ctx) throw new Error("useDataSource must be used inside <DataSourceProvider>")
|
||||
return ctx
|
||||
}
|
||||
Reference in new issue
Block a user