chore: enhance frontend configuration for backend integration
Docker images / backend-image (push) Successful in 39s
Docker images / frontend-image (push) Successful in 1m36s
Docker images / updater-image (push) Successful in 37s
Docker images / notify-webhook (push) Has been skipped

- Added environment variables for backend URLs in the Dockerfile to facilitate dynamic configuration.
- Updated Next.js configuration to include rewrites for health and API endpoints, improving backend connectivity.
- Modified settings page to handle backend URL locking and normalization, enhancing user experience and data integrity.
This commit is contained in:
Denozordec
2026-05-12 15:28:20 +07:00
parent 4b240d70d0
commit 4489dfb2d6
6 changed files with 117 additions and 24 deletions
+32
View File
@@ -0,0 +1,32 @@
export const LOCAL_DEFAULT_BACKEND_URL = "http://localhost:8000"
export type ConfiguredBackendUrl =
| { kind: "local" }
| { kind: "fixed"; url: string }
| { kind: "same-origin" }
export function configuredBackendUrl(): ConfiguredBackendUrl {
const raw = process.env.NEXT_PUBLIC_BACKEND_URL
if (raw === undefined || raw === "local") return { kind: "local" }
if (raw === "" || raw === "same-origin") return { kind: "same-origin" }
return { kind: "fixed", url: raw.replace(/\/$/, "") }
}
export function defaultDataSourceMode(): "mock" | "live" {
return process.env.NEXT_PUBLIC_DEFAULT_DATA_SOURCE === "live" ? "live" : "mock"
}
export function isBackendUrlLocked(): boolean {
const cfg = configuredBackendUrl()
return cfg.kind === "same-origin" || cfg.kind === "fixed"
}
export function resolveStoredBackendUrl(stored: string | null): string {
const cfg = configuredBackendUrl()
if (cfg.kind === "fixed") return cfg.url
if (cfg.kind === "same-origin" && typeof window !== "undefined") {
return window.location.origin
}
const trimmed = stored?.trim().replace(/\/$/, "")
return trimmed || LOCAL_DEFAULT_BACKEND_URL
}
+35 -20
View File
@@ -1,6 +1,13 @@
"use client"
import { createContext, useContext, useEffect, useState, useCallback } from "react"
import {
configuredBackendUrl,
defaultDataSourceMode,
isBackendUrlLocked,
LOCAL_DEFAULT_BACKEND_URL,
resolveStoredBackendUrl,
} from "@/lib/backend-url"
// ── types ─────────────────────────────────────────────────────────────────────
@@ -11,6 +18,7 @@ interface DataSourceContextValue {
setMode: (m: DataSourceMode) => void
backendUrl: string
setBackendUrl: (url: string) => void
backendUrlLocked: boolean
/** undefined = не проверялось, true = OK, false = недоступен */
backendStatus: boolean | undefined
checkBackend: () => Promise<void>
@@ -22,31 +30,35 @@ const DataSourceContext = createContext<DataSourceContextValue | null>(null)
const LS_MODE = "routerlists:data-source"
const LS_BACKEND = "routerlists:backend-url"
const DEFAULT_URL = "http://localhost:8000"
function readStoredMode(): DataSourceMode {
if (typeof window === "undefined") return "mock"
if (typeof window === "undefined") return defaultDataSourceMode()
const stored = localStorage.getItem(LS_MODE) as DataSourceMode | null
return stored === "mock" || stored === "live" ? stored : "mock"
return stored === "mock" || stored === "live" ? stored : defaultDataSourceMode()
}
function readStoredBackendUrl(): string {
if (typeof window === "undefined") return DEFAULT_URL
return localStorage.getItem(LS_BACKEND) ?? DEFAULT_URL
if (typeof window === "undefined") 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>("mock")
const [backendUrl, setBackendUrlState] = useState(DEFAULT_URL)
const [mode, setModeState] = useState<DataSourceMode>(() =>
typeof window === "undefined" ? defaultDataSourceMode() : readStoredMode(),
)
const [backendUrl, setBackendUrlState] = useState(() =>
typeof window === "undefined" ? LOCAL_DEFAULT_BACKEND_URL : readStoredBackendUrl(),
)
const [backendStatus, setBackendStatus] = useState<boolean | undefined>(undefined)
const backendUrlLocked = isBackendUrlLocked()
useEffect(() => {
queueMicrotask(() => {
const nextMode = readStoredMode()
const nextBackendUrl = readStoredBackendUrl()
setModeState(nextMode)
setBackendUrlState(nextBackendUrl)
})
if (configuredBackendUrl().kind !== "same-origin") return
setBackendUrlState(window.location.origin)
}, [])
const setMode = useCallback((m: DataSourceMode) => {
@@ -55,27 +67,30 @@ export function DataSourceProvider({ children }: { children: React.ReactNode })
}, [])
const setBackendUrl = useCallback((url: string) => {
setBackendUrlState(url)
localStorage.setItem(LS_BACKEND, url)
}, [])
if (backendUrlLocked) return
const normalized = normalizeBackendUrl(url)
setBackendUrlState(normalized)
localStorage.setItem(LS_BACKEND, normalized)
}, [backendUrlLocked])
const checkBackend = useCallback(async () => {
const url = (localStorage.getItem(LS_BACKEND) ?? DEFAULT_URL).replace(/\/$/, "")
const url = normalizeBackendUrl(backendUrl)
try {
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3000) })
setBackendStatus(res.ok)
} catch {
setBackendStatus(false)
}
}, [])
}, [backendUrl])
// Auto-check when mode switches to live (defer to avoid set-state-in-effect rule on async loader)
useEffect(() => {
if (mode === "live") queueMicrotask(() => { void checkBackend() })
}, [mode, checkBackend])
return (
<DataSourceContext.Provider value={{ mode, setMode, backendUrl, setBackendUrl, backendStatus, checkBackend }}>
<DataSourceContext.Provider
value={{ mode, setMode, backendUrl, setBackendUrl, backendUrlLocked, backendStatus, checkBackend }}
>
{children}
</DataSourceContext.Provider>
)