From 4489dfb2d62e836b5415b9ead409851bef995eaf Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 12 May 2026 15:28:20 +0700 Subject: [PATCH] chore: enhance frontend configuration for backend integration - 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. --- .gitea/workflows/docker.yml | 4 +++ Dockerfile.frontend | 6 ++++ app/(main)/settings/page.tsx | 33 +++++++++++++++++++--- lib/backend-url.ts | 32 +++++++++++++++++++++ lib/data-source.tsx | 55 +++++++++++++++++++++++------------- next.config.ts | 11 ++++++++ 6 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 lib/backend-url.ts diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml index c165d0f..8c82a1d 100644 --- a/.gitea/workflows/docker.yml +++ b/.gitea/workflows/docker.yml @@ -125,6 +125,10 @@ jobs: file: Dockerfile.frontend platforms: linux/amd64 push: true + build-args: | + BACKEND_INTERNAL_URL=http://backend:8000 + NEXT_PUBLIC_BACKEND_URL=same-origin + NEXT_PUBLIC_DEFAULT_DATA_SOURCE=live tags: | ${{ env.IMAGE_NAME }}:latest ${{ env.IMAGE_NAME }}:${{ gitea.sha }} diff --git a/Dockerfile.frontend b/Dockerfile.frontend index 9b262c2..01ad06f 100644 --- a/Dockerfile.frontend +++ b/Dockerfile.frontend @@ -8,6 +8,12 @@ COPY packages/contracts/package.json packages/contracts/ RUN npm ci --workspace=@mmapp/contracts --include-workspace-root --ignore-scripts FROM deps AS build +ARG BACKEND_INTERNAL_URL=http://backend:8000 +ARG NEXT_PUBLIC_BACKEND_URL=same-origin +ARG NEXT_PUBLIC_DEFAULT_DATA_SOURCE=live +ENV BACKEND_INTERNAL_URL=$BACKEND_INTERNAL_URL +ENV NEXT_PUBLIC_BACKEND_URL=$NEXT_PUBLIC_BACKEND_URL +ENV NEXT_PUBLIC_DEFAULT_DATA_SOURCE=$NEXT_PUBLIC_DEFAULT_DATA_SOURCE COPY packages/contracts packages/contracts COPY next.config.ts tsconfig.json postcss.config.mjs components.json ./ COPY app app diff --git a/app/(main)/settings/page.tsx b/app/(main)/settings/page.tsx index bdad05f..4314e81 100644 --- a/app/(main)/settings/page.tsx +++ b/app/(main)/settings/page.tsx @@ -825,7 +825,7 @@ export default function SettingsPage() { const [copied, setCopied] = useState(null) // data source - const { mode, setMode, backendUrl, setBackendUrl, backendStatus, checkBackend } = useDataSource() + const { mode, setMode, backendUrl, setBackendUrl, backendUrlLocked, backendStatus, checkBackend } = useDataSource() const evo = useEvoBGP() const [evoBaseDraft, setEvoBaseDraft] = useState("") const [evoEnabledDraft, setEvoEnabledDraft] = useState(false) @@ -834,6 +834,17 @@ export default function SettingsPage() { const [evoSaveErr, setEvoSaveErr] = useState(null) const [urlDraft, setUrlDraft] = useState(backendUrl) const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl]) + + useEffect(() => { + setUrlDraft(backendUrl) + }, [backendUrl]) + + const commitBackendUrl = useCallback(() => { + const normalized = urlDraft.trim().replace(/\/$/, "") + setUrlDraft(normalized) + setBackendUrl(normalized) + void checkBackend() + }, [urlDraft, setBackendUrl, checkBackend]) const [evoTestResult, setEvoTestResult] = useState<{ ok: boolean; message: string } | null>(null) const [evoBusy, setEvoBusy] = useState<"test" | "refresh" | null>(null) const [showEvoKey, setShowEvoKey] = useState(false) @@ -984,17 +995,31 @@ export default function SettingsPage() { {/* backend URL — visible in both modes so user can configure before switching */} - +
setUrlDraft(e.target.value)} - onBlur={() => { setBackendUrl(urlDraft.replace(/\/$/, "")) }} + onBlur={commitBackendUrl} + onKeyDown={e => { + if (e.key === "Enter") { + e.preventDefault() + commitBackendUrl() + } + }} + readOnly={backendUrlLocked} placeholder="http://localhost:8000" /> diff --git a/lib/backend-url.ts b/lib/backend-url.ts new file mode 100644 index 0000000..1736a1d --- /dev/null +++ b/lib/backend-url.ts @@ -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 +} diff --git a/lib/data-source.tsx b/lib/data-source.tsx index 44d9b86..0d7d247 100644 --- a/lib/data-source.tsx +++ b/lib/data-source.tsx @@ -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 @@ -22,31 +30,35 @@ const DataSourceContext = createContext(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("mock") - const [backendUrl, setBackendUrlState] = useState(DEFAULT_URL) + const [mode, setModeState] = useState(() => + typeof window === "undefined" ? defaultDataSourceMode() : readStoredMode(), + ) + const [backendUrl, setBackendUrlState] = useState(() => + typeof window === "undefined" ? LOCAL_DEFAULT_BACKEND_URL : readStoredBackendUrl(), + ) const [backendStatus, setBackendStatus] = useState(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 ( - + {children} ) diff --git a/next.config.ts b/next.config.ts index 68a6c64..53316c1 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,18 @@ import type { NextConfig } from "next"; +const backendInternalUrl = (process.env.BACKEND_INTERNAL_URL ?? "http://127.0.0.1:8000").replace( + /\/$/, + "", +); + const nextConfig: NextConfig = { output: "standalone", + async rewrites() { + return [ + { source: "/health", destination: `${backendInternalUrl}/health` }, + { source: "/api/:path*", destination: `${backendInternalUrl}/api/:path*` }, + ]; + }, }; export default nextConfig;